njin
Engine 2D gọn nhẹ để tự tay làm game theo ý mình. Vibe coding cũng ổn luôn
Loading...
Searching...
No Matches
Thế giới ngoài trời 3D

Trang này dựng một vùng đất ngoài trời: địa hình đồi núi có va chạm, cỏ đung đưa theo gió, đá và cây rải theo luật, hồ hay biển có sóng và vật nổi, bầu trời theo giờ trong ngày, mây, mưa, tuyết và sương. Mọi thứ khai báo trong njin_world3d.h, chạy trên OpenGL 3.3 như phần 3D còn lại, và nhận ánh sáng, bóng đổ, đèn và sương mù của Đồ họa 3D.

Cần biết trước: Đồ họa 3D (begin_3d(), ánh sáng, vật lý 3D).

Địa hình (lớp cỏ và lớp đá theo độ dốc), đá rải, hồ có sóng Gerstner, bọt ở bờ, thùng nổi và bầu trời có mây

Địa hình

terrain3d_create() dựng một lưới độ cao vuông: resolution x resolution mẫu phủ hình vuông cạnh size mét, góc nhỏ nhất ở origin. Độ cao lấy từ một trong ba nguồn:

Nguồn Trường Ghi chú
Mảng của game heights Mét, theo hàng (z) rồi cột (x)
Ảnh độ cao heightmap Ảnh xám (kênh đỏ) hoặc file .r16/.raw 16 bit. Ảnh 8 bit chỉ có 256 bậc: đặt smooth
Nhiễu noise njin::noise_desc của Sinh bản đồ tự động, tính theo mét; kéo giãn 0..1 rồi nhân height_scale
const njin::terrain3d_handle ground = njin::terrain3d_create(ctx, {.origin = {-256, 0, -256},
.size = 512.0f,
.resolution = 513,
.height_scale = 45.0f});
terrain3d_handle terrain3d_create(context &ctx, const terrain3d_desc &desc)
Tạo địa hình.
Định danh của một địa hình, tạo bởi terrain3d_create().
Definition _types.h:229

Lớp bề mặt. Tối đa 4 lớp (njin::terrain3d_layer): mỗi lớp một ảnh lặp lại theo vị trí thế giới (mỗi ô tile mét) hoặc một màu trơn, có normal map nếu muốn. Bật auto_splat (mặc định) thì engine tự tính lớp nào phủ chỗ nào theo quy tắc của lớp: khoảng độ cao (min_height, max_height) và khoảng độ dốc (min_slope, max_slope), mép mờ trong blend. Lớp sau đè lên lớp trước, nên lớp 0 là lớp phủ rộng nhất (cỏ), rồi đá ở chỗ dốc, tuyết trên cao. Tắt auto_splat thì dùng ảnh splatmap (kênh đỏ, lục, lam, alpha là lớp 0 đến 3). Chỗ dốc đứng ảnh được chiếu từ ba phía nên vách đá không bị kéo dài.

Mức chi tiết. Lưới được chia thành các mảnh chunk_quads x chunk_quads ô. Mảnh ngoài tầm nhìn bị bỏ; mảnh xa hơn lod_distance vẽ với một nửa số ô mỗi cạnh, xa gấp đôi nữa thì một nửa nữa, tới lod_levels mức. Mép mỗi mảnh có "váy" thả xuống, nên hai mảnh khác mức chi tiết không để lộ khe. Pháp tuyến luôn lấy từ lưới đầy đủ, nên ánh sáng không đổi khi mảnh đổi mức.

Hỏi địa hình. terrain3d_height() cho độ cao tại (x, z) đúng như lưới vẽ ở mức chi tiết cao nhất và như body va chạm: cả ba chia mỗi ô vuông thành hai tam giác theo cùng một đường chéo. terrain3d_normal() cho pháp tuyến, terrain3d_layer_weight() cho biết đang đứng trên lớp gì (đổi tiếng bước chân, bụi khi chạy).

Va chạm. Với collision (mặc định) địa hình có một body tĩnh là height field của Jolt: nhân vật, xe, vật động đứng trên nó, raycast chạm nó. terrain3d_body() trả body đó để nhận ra trong kết quả raycast.

Sửa lúc chạy. terrain3d_edit() nâng, hạ, san phẳng hay làm mượt một vùng tròn (njin::terrain3d_brush), terrain3d_paint() vẽ thêm một lớp. Mọi thứ theo ngay: lưới vẽ, body va chạm, lớp tự phủ (đồi đắp cao thành dốc thì lộ đá), cỏ mọc lại, đá và cây rải trên vùng đó đổi độ cao theo mặt đất. Vật đang nằm trên chỗ đất được nâng lên cũng được nâng theo, không bị đất mới nuốt mất.

// Một quả bom tạo hố: hạ 2 m ở tâm, mép mờ.
njin::terrain3d_edit(ctx, ground, {.kind = njin::terrain3d_lower, .center = blast, .radius = 4.0f, .strength = 2.0f});
void terrain3d_edit(context &ctx, terrain3d_handle handle, const terrain3d_brush &brush)
Sửa độ cao địa hình bằng một nét: lưới vẽ, body va chạm, lớp tự phủ, cỏ và vật rải trên vùng đó đều t...
@ terrain3d_lower
Hạ xuống strength mét ở tâm.
Definition njin_world3d.h:155

Cỏ và vật rải

grass3d_create() trồng cỏ trên một lớp của địa hình (layer, -1 là mọi nơi), không mọc chỗ dốc hơn max_slope, thành đám theo patchiness. Mỗi ngọn là vài tam giác vẽ bằng instancing; cỏ chỉ được tạo cho vùng quanh camera (draw_distance), thưa và thấp dần về xa, nên địa hình rộng mấy cũng chỉ tốn cho phần gần. Cỏ đung đưa theo gió: wind3d_set(), hoặc gió của thời tiết khi vẽ trời bằng draw_sky3d(). Cỏ nhận bóng đổ; đổ bóng thì bật cast_shadows (tốn).

scatter3d_create() rải một model (đá, cây, bụi) lên địa hình theo luật: mật độ, khoảng cách tối thiểu spacing, khoảng độ cao, độ dốc, lớp, cụm theo nhiễu. Vật nghiêng theo mặt đất theo align, lún xuống sink, to nhỏ ngẫu nhiên. Chỗ đặt chọn một lần theo seed, vẽ bằng draw_instanced3d() theo vùng: bỏ vùng ngoài tầm nhìn, dùng far_model cho vùng xa hơn lod_distance. Cây cần va chạm thì lấy vị trí bằng scatter3d_transforms() rồi tạo body cho từng cây.

const njin::scatter3d_handle trees = njin::scatter3d_create(ctx, {.terrain = ground,
.model = pine,
.far_model = pine_low,
.density = 0.004f,
.spacing = 6.0f,
.max_slope = 25.0f,
.patchiness = 0.8f});
scatter3d_handle scatter3d_create(context &ctx, const scatter3d_desc &desc)
Rải vật lên địa hình.
Định danh của một lớp vật rải trên địa hình (đá, cây), tạo bởi scatter3d_create().
Definition _types.h:250

Nước

water3d_create() tạo một mặt nước ở độ cao level: hồ hình chữ nhật (center, size), hoặc biển trải tới chân trời khi size là {0, 0} (lưới đi theo camera, dày ở gần và thưa ở xa). Mặt nước gồm tối đa 8 sóng Gerstner (njin::water3d_wave): đỉnh nhọn, đáy bẹt, sóng dài đi nhanh hơn sóng ngắn như sóng nước sâu.

Gắn terrain thì nước biết độ sâu từng chỗ: màu từ shallow_color tới deep_color, chỗ nông trong suốt thấy đáy (clarity), bọt ở bờ (foam_width). Nước phản chiếu bầu trời của draw_sky3d() (cả mây), có nắng lấp lánh và nhận bóng đổ. Nước vẽ sau mọi hình đục, như kính.

water3d_height() và water3d_normal() tính đúng công thức sóng của shader, nên vật trong game đặt theo mặt nước khớp với hình vẽ. water3d_float() cho một body động nổi: mỗi bước vật lý, phần thể tích dưới mặt sóng đẩy nó lên (buoyancy 1 là lơ lửng, lớn hơn là nổi), nước cản chuyển động và xoay, flow kéo theo dòng chảy.

const njin::water3d_handle sea = njin::water3d_create(ctx, {.level = 0.0f, .terrain = island});
njin::water3d_float(ctx, sea, raft, {.buoyancy = 1.8f});
void water3d_float(context &ctx, water3d_handle water, body3d_handle body, const buoyancy3d &buoyancy={})
Cho body động nổi trên mặt nước: mỗi bước vật lý, phần thể tích dưới mặt sóng (tại tâm của body) đẩy ...
water3d_handle water3d_create(context &ctx, const water3d_desc &desc)
Tạo mặt nước.
Định danh của một mặt nước (hồ, biển), tạo bởi water3d_create().
Definition _types.h:236

Bầu trời và thời tiết

draw_sky3d() vẽ bầu trời phía sau mọi thứ của lần vẽ 3D (chỉ phủ chỗ chưa có gì): nền theo giờ, mặt trời, mây trôi theo gió, sao và trăng ban đêm. njin::sky3d đặt giờ (hour), vĩ độ và mùa: mặt trời mọc ở phía đông (+x), lên cao nhất lúc 12 giờ ở phía nam (+z), lặn ở phía tây. Với drive_light (mặc định), draw_sky3d() đặt luôn ánh sáng của lần vẽ: hướng và màu nắng (vàng cam lúc sớm và chiều, trăng xanh nhạt ban đêm), ánh sáng nền theo màu trời, màu sương bằng màu chân trời; cài đặt bóng đổ của ánh sáng hiện tại được giữ. Muốn tự lo thì tắt nó và gọi sky3d_light().

Thời tiết (njin::weather3d) đặt trong sky3d::weather: mây che (clouds) và độ tối của mây, sương, mưa, tuyết, gió, độ ướt của mặt đất. Có sẵn năm kiểu (weather3d_preset()): trời quang, u ám, mưa, tuyết, sương mù. weather3d_lerp() chuyển dần từ kiểu này sang kiểu kia. Mưa và tuyết rơi trong một khối quanh camera, tính hoàn toàn trên GPU; mỗi hạt giữ chỗ của nó trong thế giới khi camera di chuyển, gió làm mưa rơi xiên.

njin::sky3d sky{.hour = 17.5f};
njin::draw_sky3d(ctx, sky);
weather3d weather3d_lerp(const weather3d &a, const weather3d &b, f32 t)
Nội suy giữa hai thời tiết, để trời chuyển mưa dần dần.
void draw_sky3d(context &ctx, const sky3d &sky)
Vẽ bầu trời (nền, mặt trời, mây, sao) phía sau mọi thứ của lần vẽ 3D đang mở, và mưa hay tuyết quanh ...
weather3d weather3d_preset(weather3d_kind kind)
Một kiểu thời tiết có sẵn.
@ weather3d_rain
Mưa, đất ướt.
Definition njin_world3d.h:488
@ weather3d_clear
Trời quang, ít mây.
Definition njin_world3d.h:486
Bầu trời theo giờ trong ngày, dùng với draw_sky3d().
Definition njin_world3d.h:510
weather3d weather
Thời tiết.
Definition njin_world3d.h:516
Mưa: trời u ám, mặt đất ướt tối và bóng hơn, hạt mưa xiên theo gió

Mái che. Mưa và tuyết không rơi dưới mái, và đất dưới mái không ướt. Mặc định (weather3d::cover_auto) engine tự biết chỗ nào có mái: khi có mưa hay tuyết, nó vẽ độ sâu của mọi hình đục đổ bóng nhìn thẳng từ trên xuống, trong một ô vuông 36 m quanh camera (ô 14 cm), rồi giấu giọt nào nằm dưới một bề mặt. Bản đồ này được vẽ lại mỗi 8 frame hay khi camera đi quá 2 m, nên gần như không tốn gì (đo trên RTX 3050 Laptop: không thấy khác biệt so với tắt, trong sai số 0,1 ms). Hình không đổ bóng thì không che. Muốn tự đánh dấu (mái hiên không đổ bóng, hay tắt cover_auto để bỏ lượt vẽ đó), đặt các hộp bằng weather3d_cover_set(): mưa không rơi trong phần hộp nằm dưới mặt trên của nó.

const njin::weather3d_cover porch{.center = {4.0f, 1.5f, -2.0f}, .size = {3.0f, 3.0f, 2.0f}};
njin::weather3d_cover_set(ctx, &porch, 1);
void weather3d_cover_set(context &ctx, const weather3d_cover *covers, i32 count)
Đặt các chỗ có mái (thay danh sách cũ), thêm vào phần weather3d::cover_auto che: nhà không đổ bóng,...
Một chỗ có mái, cho weather3d_cover_set(): hình hộp thẳng trục.
Definition njin_world3d.h:466

Ví dụ đầy đủ

Đồi có cỏ, đá và tuyết; một hồ; thùng gỗ nổi; ngày trôi qua trong hai phút; phím R chuyển mưa; chuột trái đắp đất.

#include <njin.h>
namespace {
using namespace njin;
sky3d sky{.hour = 9.0f};
void load(context &ctx) {
// Đồi 512 m sinh bằng nhiễu: cỏ khắp nơi, đá ở chỗ dốc, tuyết trên đỉnh.
const texture_handle grass_tex = texture_load(ctx, "assets/grass.png");
const texture_handle rock_tex = texture_load(ctx, "assets/rock.png");
ground = terrain3d_create(ctx, {.origin = {-256.0f, 0.0f, -256.0f},
.size = 512.0f,
.resolution = 513,
.height_scale = 45.0f,
.layers = {{.albedo = grass_tex, .tile = 3.0f},
{.albedo = rock_tex, .tile = 5.0f, .min_slope = 30.0f, .blend = 8.0f},
{.color = {0.95f, 0.96f, 1.0f, 1.0f}, .min_height = 36.0f}},
.layer_count = 3});
// Hồ lấp chỗ trũng; bờ và độ sâu theo địa hình.
lake = water3d_create(ctx, {.level = 8.0f, .size = {512.0f, 512.0f}, .terrain = ground});
grass = grass3d_create(ctx, {.terrain = ground, .density = 8.0f, .layer = 0});
rocks = scatter3d_create(ctx, {.terrain = ground,
.model = model_load(ctx, "assets/rock.glb"),
.density = 0.01f,
.spacing = 4.0f,
.min_height = 9.0f,
.align = 0.7f,
.sink = 0.15f});
// Một thùng gỗ thả xuống hồ: nổi và dập dềnh theo sóng.
crate = body3d_create(ctx, {.position = {0.0f, 12.0f, 0.0f}, .motion = body3d_dynamic, .mass = 120.0f});
water3d_float(ctx, lake, crate);
}
void update(context &ctx) {
// Một ngày trôi qua trong hai phút; phím R chuyển sang mưa.
sky.hour += delta(ctx) * 24.0f / 120.0f;
if (sky.hour >= 24.0f)
sky.hour -= 24.0f;
if (key_pressed(ctx, key_r))
// Chuột trái đắp đất lên chỗ chuột trỏ tới.
const camera3d cam{.position = {-60.0f, 40.0f, 60.0f}, .target = {0.0f, 10.0f, 0.0f}};
const ray3d_hit hit = physics3d_raycast(ctx, camera3d_ray(ctx, cam, mouse_pos(ctx)), 1000.0f);
if (hit.hit)
terrain3d_edit(ctx, ground, {.kind = terrain3d_raise, .center = hit.point, .radius = 5.0f, .strength = 1.5f});
}
}
void render(context &ctx) {
light3d_set(ctx, {.shadows = true, .shadow_range = 60.0f});
begin_3d(ctx, {.position = {-60.0f, 40.0f, 60.0f}, .target = {0.0f, 10.0f, 0.0f}, .far_plane = 2000.0f});
draw_sky3d(ctx, sky); // trời, mặt trời, mây; đặt luôn ánh sáng theo giờ
draw_terrain3d(ctx, ground);
draw_grass3d(ctx, grass);
draw_scatter3d(ctx, rocks);
const transform3d t = body3d_transform(ctx, crate);
draw_shape3d(ctx, {.kind = shape3d_box, .position = t.position, .rotation = t.rotation},
rgba{0.6f, 0.4f, 0.2f, 1.0f});
draw_water3d(ctx, lake); // sau cùng không bắt buộc: nước luôn vẽ sau hình đục
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_update, update, "update");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc outdoor_module() { return {.name = "outdoor", .setup = setup}; }
ray3d camera3d_ray(const context &ctx, const camera3d &camera, vec2 screen)
Tia từ camera đi qua một điểm trên màn hình, để chọn vật dưới chuột: camera3d_ray(ctx,...
void light3d_set(context &ctx, const light3d &light)
Đặt ánh sáng chung.
void begin_3d(context &ctx, const camera3d &camera)
Bắt đầu vẽ 3D qua camera.
void draw_shape3d(const context &ctx, const shape3d &shape, rgba color)
Vẽ một hình SDF mịn.
model_handle model_load(context &ctx, const char *path)
Nạp model 3D từ file glTF (.glb, .gltf) hoặc OBJ.
void end_3d(context &ctx)
Kết thúc vẽ 3D và quay về vẽ 2D trong không gian thế giới.
@ shape3d_box
Hộp: size, bo cạnh theo rounding.
Definition njin_3d.h:250
bool mouse_pressed(const context &ctx, mouse_button button)
Đúng ở frame mà nút chuột vừa được nhấn xuống (chỉ một frame).
bool key_pressed(const context &ctx, key_code key)
Đúng ở frame mà phím vừa được nhấn xuống (chỉ một frame).
vec2 mouse_pos(const context &ctx)
Vị trí con trỏ chuột, tính bằng pixel màn hình.
@ mouse_left
Chuột trái.
Definition _types.h:475
@ key_r
Phím R.
Definition _types.h:361
void ecs_register(context &ctx, sys_phase phase, sys_fnc fnc, const char *name=nullptr)
Thêm một system vào lịch chạy của một phase.
@ phase_update
Mỗi frame, logic chính của game.
Definition _mod.h:35
@ phase_startup
Một lần, trước frame đầu tiên.
Definition _mod.h:32
@ phase_render
Mỗi frame, vẽ trong không gian thế giới.
Definition _mod.h:38
ray3d_hit physics3d_raycast(const context &ctx, const ray3d &ray, f32 max_distance, body3d_handle *body=nullptr)
Bắn một tia vào các body (không trúng nhân vật, đi xuyên sensor): đạn, tầm nhìn, chọn vật bằng chuột.
body3d_handle body3d_create(context &ctx, const body3d_desc &desc)
Tạo một body vật lý 3D.
transform3d body3d_transform(const context &ctx, body3d_handle handle)
Vị trí và góc xoay hiện tại của body, để vẽ nó (scale luôn là 1).
@ body3d_dynamic
Do vật lý điều khiển: rơi, va, lăn, bị đẩy. Thùng, bóng, mảnh vỡ.
Definition njin_physics3d.h:14
texture_handle texture_load(context &ctx, const char *path)
Nạp một texture từ file ảnh.
f32 delta(const context &ctx)
Thời gian của frame trước, tính bằng giây, đã nhân với tốc độ thời gian.
void draw_water3d(const context &ctx, water3d_handle handle)
Vẽ mặt nước trong lần vẽ 3D đang mở.
void draw_terrain3d(const context &ctx, terrain3d_handle handle)
Vẽ địa hình trong lần vẽ 3D đang mở (giữa begin_3d() và end_3d()).
void draw_grass3d(const context &ctx, grass3d_handle handle)
Vẽ cỏ quanh camera của lần vẽ 3D đang mở.
void draw_scatter3d(const context &ctx, scatter3d_handle handle)
Vẽ các vật rải trong tầm nhìn của lần vẽ 3D đang mở (bằng draw_instanced3d()).
grass3d_handle grass3d_create(context &ctx, const grass3d_desc &desc)
Tạo thảm cỏ.
@ terrain3d_raise
Nâng lên strength mét ở tâm.
Definition njin_world3d.h:154
Định danh của một body vật lý 3D, tạo bởi body3d_create().
Definition _types.h:160
Camera phối cảnh cho begin_3d().
Definition njin_3d.h:15
Định danh của một thảm cỏ trên địa hình, tạo bởi grass3d_create().
Definition _types.h:243
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Kết quả khi tia chạm một vật.
Definition njin_3d.h:847
vec3 point
Điểm chạm.
Definition njin_3d.h:850
bool hit
Tia có chạm không.
Definition njin_3d.h:848
Màu gồm đỏ, xanh lá, xanh dương và alpha.
Definition _types.h:64
f32 hour
Giờ trong ngày, 0..24 (lẻ được: 6.5 là 6 giờ 30).
Definition njin_world3d.h:511
Định danh của một texture, tạo bởi texture_load().
Definition _types.h:146
Vị trí, hướng và tỉ lệ của một vật 3D.
Definition njin_3d.h:504
vec3 position
Vị trí.
Definition njin_3d.h:505
vec3 rotation
Góc xoay quanh x, y, z, tính bằng độ.
Definition njin_3d.h:506
f32 rain
Mưa, 0..1.
Definition njin_world3d.h:451

Hiệu năng và giới hạn

Đo trên RTX 3050 Laptop, bản Release, 960 x 540: địa hình 1 km² (1025 x 1025 mẫu, 1 m một mẫu) cùng cỏ, gần 9000 viên đá, hồ, trời và bóng đổ của mặt trời vẽ trong khoảng 1,7 ms mỗi frame. Tạo địa hình đó mất khoảng 0,4 giây (sinh nhiễu, lớp tự phủ, body va chạm), nên tạo lúc nạp màn, không giữa trận.

  • Mái che (cover_auto) chỉ biết hình nằm trong ô 36 m quanh camera và đổ bóng; mưa xiên vẫn lọt vào dưới mép mái. Độ bóng của mặt đất ướt áp cả cho chỗ khô dưới mái (chỉ màu tối đi là theo mái).
  • Nước không phản chiếu vật trên bờ, chỉ phản chiếu bầu trời. Bờ và độ sâu chỉ tính theo địa hình gắn vào, không theo các model khác dưới nước.
  • Vật rải được đặt một lần khi tạo; sửa địa hình chỉ đổi độ cao của chúng.
  • Mỗi địa hình là một hình vuông; thế giới lớn hơn thì đặt nhiều địa hình cạnh nhau.