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
Đồ họa 3D

njin vẽ được thế giới 3D: camera phối cảnh, hình khối, hình SDF mịn, model glTF có animation xương, ánh sáng có bóng đổ, vật liệu, hiệu ứng, hạt, instancing, vật lý và va chạm, entity 3D, chọn vật bằng chuột và gizmo để debug. Mọi thứ đều qua njin.h (khai báo trong njin_3d.h, njin_physics3d.h và njin_gizmo.h), không cần raylib hay thư viện vật lý.

Cần biết trước: Vẽ hình và chữ, Vòng lặp game và Vẽ ảnh và shader. Chạy njin_fps (bắn súng góc nhìn thứ nhất), njin_sokoban (đẩy thùng 2.5D) và njin_platformer3d (nhảy bục góc nhìn thứ ba) để xem mọi thứ dưới đây trong một game thật.

Hình lưới (hộp, cầu), hình SDF (hộp bo góc, viên nang, xuyến) và 8 hộp instanced dưới nắng có bóng đổ, cùng gizmo 2D và 3D
#include <njin.h>
namespace {
using namespace njin;
void load(context &ctx) {
crate = model_load(ctx, "assets/crate.glb");
// Mặt trời chiếu chéo xuống và đổ bóng, ánh sáng nền hơi xanh.
light3d_set(ctx, {.direction = {-0.5f, -1.0f, -0.3f},
.ambient = {0.35f, 0.38f, 0.45f, 1.0f},
.shadows = true,
.shadow_range = 12.0f});
// Một hàng 20 "cây": mỗi instance là vị trí và tỉ lệ, rồi màu (8 số).
trees = instance_buffer_create(ctx, 8);
f32 data[20 * 8];
for (i32 i = 0; i < 20; i++) {
const f32 v[8] = {-9.5f + i, 0.5f, -4.0f, 0.8f, 0.2f, 0.5f + 0.02f * i, 0.25f, 1.0f};
for (i32 k = 0; k < 8; k++)
data[i * 8 + k] = v[k];
}
instance_buffer_upload(ctx, trees, data, 20);
}
void update(context &ctx) {
// Gizmo gọi được ở bất kỳ phase nào: một mũi tên chỉ lên model.
gizmo_arrow3d(ctx, {2.0f, 2.5f, 0.0f}, {2.0f, 1.2f, 0.0f}, colors::yellow);
}
void render(context &ctx) {
// Camera đứng ở (0, 6, 8), nhìn vào gốc tọa độ.
begin_3d(ctx, {.position = {0.0f, 6.0f, 8.0f}, .target = {0.0f, 0.0f, 0.0f}, .fovy = 50.0f});
// Một đèn điểm màu cam cạnh hộp.
light3d_add(ctx, {.position = {-2.0f, 1.5f, 1.0f}, .color = {1.0f, 0.6f, 0.3f, 1.0f}, .radius = 4.0f});
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {20.0f, 20.0f}, {0.7f, 0.7f, 0.65f, 1.0f});
draw_cube3d(ctx, {-2.0f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, colors::red);
draw_instanced3d(ctx, mesh3d_cube, trees, 0, 20);
// Model xoay quanh trục y theo thời gian.
draw_model(ctx, crate, {.position = {2.0f, 0.0f, 0.0f}, .rotation = {0.0f, elapsed(ctx) * 45.0f, 0.0f}});
// Nhân vật: viên nang SDF mịn, bóng như nhựa, có viền sáng.
material3d_set(ctx, {.specular = 0.6f, .shininess = 60.0f, .rim = {0.55f, 0.75f, 1.0f, 0.45f}});
draw_shape3d(ctx, {.kind = shape3d_capsule, .position = {0.0f, 0.6f, 2.0f}, .radius = 0.4f, .height = 1.2f},
material3d_set(ctx, {});
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 draw_3d_module() { return {.name = "draw_3d", .setup = setup}; }
void draw_model(const context &ctx, model_handle handle, const transform3d &transform, rgba tint=colors::white)
Vẽ model tại transform, với vật liệu của nó (model_material_set()).
void material3d_set(context &ctx, const material3d &material)
Đặt bề mặt cho các hình 3D vẽ sau lệnh này, đến lần gọi tiếp theo hoặc end_3d().
void light3d_set(context &ctx, const light3d &light)
Đặt ánh sáng chung.
void light3d_add(context &ctx, const light3d_source &light)
Thêm một đèn cho lần vẽ 3D đang mở.
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.
void draw_plane3d(const context &ctx, vec3 center, vec2 size, rgba color)
Vẽ mặt phẳng nằm ngang (song song mặt xz), mặt trên hướng +y.
model_handle model_load(context &ctx, const char *path)
Nạp model 3D từ file glTF (.glb, .gltf) hoặc OBJ.
void draw_cube3d(const context &ctx, vec3 center, vec3 size, rgba color)
Vẽ hộp đặc, các cạnh song song với trục.
void end_3d(context &ctx)
Kết thúc vẽ 3D và quay về vẽ 2D trong không gian thế giới.
void draw_instanced3d(const context &ctx, mesh3d_kind mesh, instance_buffer_handle buffer, u32 first, u32 count, shader_handle shader={})
Vẽ count bản của một hình có sẵn bằng một lệnh vẽ, mỗi bản đặt theo dữ liệu của nó trong một bộ đệm i...
@ mesh3d_cube
Hộp 1 x 1 x 1, tâm ở gốc.
Definition njin_3d.h:924
@ shape3d_capsule
Viên nang dọc trục y: radius, height (cả hai đầu tròn).
Definition njin_3d.h:251
void gizmo_arrow3d(context &ctx, vec3 from, vec3 to, rgba color=colors::green, f32 duration=0.0f)
Mũi tên 3D từ from tới to.
void instance_buffer_upload(context &ctx, instance_buffer_handle handle, const f32 *data, u32 count)
Ghi count instance vào bộ đệm, thay toàn bộ nội dung cũ.
instance_buffer_handle instance_buffer_create(context &ctx, u32 floats_per_instance)
Tạo một bộ đệm instance trên GPU: mỗi instance là floats_per_instance số thực, do game tự quyết định ...
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
f32 elapsed(const context &ctx)
Thời gian đã trôi qua kể từ lúc mở cửa sổ, tính bằng giây.
std::int32_t i32
Số nguyên có dấu 32 bit.
Definition _types.h:14
float f32
Số thực 32 bit.
Definition _types.h:28
constexpr rgba yellow
Vàng.
Definition _math.h:239
constexpr rgba red
Đỏ.
Definition _math.h:236
constexpr rgba green
Xanh lá.
Definition _math.h:237
Định danh của một bộ đệm instance, tạo bởi instance_buffer_create().
Definition _types.h:109
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
Định danh của một model 3D, tạo bởi model_load().
Definition _types.h:153

Một lần vẽ 3D

Mọi lệnh vẽ 3D nằm giữa begin_3d() và end_3d(), trong phase_render. njin::camera3d là camera phối cảnh: position, target, up và góc nhìn dọc fovy. Trục y hướng lên, hệ tay phải như glTF: nhìn theo -z thì +x ở bên phải. Tỉ lệ khung hình lấy theo screen_size(), nên màn hình ảo vẫn đúng.

Lệnh vẽ được ghi lại và vẽ thật ở end_3d(), theo đúng thứ tự gọi: trước hết engine tính bóng đổ từ tất cả, rồi mới vẽ từng hình. Vì vậy thứ tự material3d_set(), fx3d_set() và lệnh vẽ vẫn có ý nghĩa như bình thường, còn light3d_add() thì chiếu lên mọi hình của lần vẽ dù gọi trước hay sau.

Vẽ 2D trước begin_3d() thì 3D đè lên; vẽ 2D sau end_3d() (cùng phase_render) thì đè lên 3D. UI trong phase_post_render luôn ở trên cùng. Một frame có thể có nhiều lần vẽ 3D (ví dụ một cảnh nhỏ trong UI).

Hình khối

Hàm Là gì Khi nào dùng
draw_cube3d(), draw_sphere3d(), draw_plane3d(), draw_cylinder3d(), draw_capsule3d() Lưới tam giác Nhiều, rẻ: tường, sàn, đạn
draw_shape3d() với njin::shape3d Hình SDF: cầu, hộp bo góc, viên nang, trụ bo cạnh, xuyến Vật cần mịn khi nhìn gần: nhân vật, vật phẩm
draw_sdf_blend() với njin::sdf_part Nhiều hình nón bo tròn hòa làm một khối liền (smooth min) Nhân vật đất sét ghép từ đầu, thân, tay chân, cử động vẫn liền mạch
draw_instanced3d() Hàng nghìn hình lưới bằng một lệnh vẽ Rừng, đám đông, gạch lát
draw_model(), draw_model_anim() Model glTF/OBJ nạp bằng model_load() Đồ vật, nhân vật làm trong Blender
model_create() với njin::mesh3d_data Model từ lưới tam giác game tự dựng (vị trí, màu từng đỉnh, chỉ số) Địa hình sinh theo seed, hình ghép lúc chạy

Vẽ hàng nghìn vật nhỏ bằng draw_instanced3d() thì dùng mesh3d_sphere_low và mesh3d_cylinder_low: cùng hình với mesh3d_sphere và mesh3d_cylinder nhưng ít mặt hơn nhiều, vì mỗi bản chỉ vài điểm ảnh trên màn hình và mỗi tam giác còn được vẽ thêm một lần cho bóng đổ.

Hình SDF được tính trên từng điểm ảnh (sphere tracing trong hộp bao của nó), nên viền luôn tròn ở mọi cỡ và bo góc được, nhưng tốn hơn hình lưới. Nó vẫn nhận ánh sáng, đổ và nhận bóng, dùng njin::material3d và njin::fx3d như hình khác.

Model và vật liệu

model_load() nạp .glb, .gltf hoặc .obj, giữ màu và texture của từng vật liệu trong file. model_material_get() và model_material_set() đọc và đổi vật liệu của từng phần:

njin::model_material m = njin::model_material_get(ctx, crate, 0);
m.albedo = njin::texture_load(ctx, "assets/crate_wood.png"); // ảnh màu
m.normal = njin::texture_load(ctx, "assets/crate_wood_n.png"); // normal map
m.emission = njin::texture_load(ctx, "assets/crate_glow.png"); // ảnh phát sáng
m.shader = my_shader; // shader riêng cho phần này
m.surface.specular = 0.1f; // bề mặt mờ
njin::model_material_set(ctx, crate, 0, m); // -1 cho mọi phần

Normal map không cần tangent trong file: shader tự dựng hệ trục từ đạo hàm màn hình.

Vật liệu đánh dấu doubleSided trong glTF (lá cây, giấy, vải một lớp) được vẽ cả hai mặt: model_material::double_sided, đổi được bằng model_material_set().

Cắt bỏ ngoài tầm nhìn và mức chi tiết

draw_model(), draw_model_anim() và njin::model3d bỏ qua model có hộp bao nằm ngoài tầm nhìn camera (frustum culling); model đó vẫn đổ bóng vào cảnh. njin::render_info_get() đếm models3d (đã vẽ) và models3d_culled (bị bỏ). Một lưới lớn như địa hình hay đường sá nên chia thành nhiều model theo vùng (mỗi vùng một model_create()), để vùng khuất được bỏ.

model_lod_build() tạo các mức chi tiết cho model: bản giản lược ít tam giác hơn, được vẽ thay khi model nhỏ trên màn hình. Mức 1 dùng khi model cao chưa tới screen (mặc định một phần tư) chiều cao màn hình, mỗi mức sau ở một nửa mức trước. Model có xương giữ xương và animation ở mọi mức.

const njin::model_handle person = njin::model_load(ctx, "assets/person.glb");
njin::model_lod_build(ctx, person); // 3 mức, mỗi mức khoảng nửa số tam giác mức trước

Việc giản lược dùng thư viện meshoptimizer (MIT). Nó giữ đường ranh giữa các màu và các mảnh UV, nên lưới phẳng có nhiều màu xen kẽ (như bàn cờ) gần như không giản lược được; model_lod_build() khi đó trả về 0. draw_instanced3d() và ray3d_model() luôn dùng model gốc.

Animation của model

Một glTF có skin (xương) mang theo các animation của nó: model_load() nạp chúng cùng model. model_anim_find() tìm một animation theo tên action trong Blender, model_anim_count(), model_anim_name() và model_anim_duration() liệt kê chúng. draw_model_anim() vẽ model ở một njin::model_pose: animation anim ở giây time, và nếu cần trộn với blend_anim theo tỉ lệ blend để chuyển mượt giữa hai động tác.

// Đứng yên trộn sang chạy theo tốc độ, chuyển mượt trong 0.2 giây.
blend = njin::move_toward(blend, moving ? 1.0f : 0.0f, dt / 0.2f);
njin::draw_model_anim(ctx, robot, {.position = pos, .rotation = {0, yaw, 0}},
{.anim = idle, .time = t, .blend_anim = run, .blend_time = t, .blend = blend});
Điều Chi tiết
Tính xương Trên GPU, tối đa 128 xương, 4 xương mỗi đỉnh
Bóng đổ Theo đúng tư thế
Nhiều bản Mỗi lần vẽ một tư thế riêng: cùng một model, mỗi con một động tác
Đồng phục model_material_find() tìm vật liệu theo tên; draw_model_anim() với njin::model_recolor đổi màu nó trong một lần vẽ
Động tác giữ khung cuối Khung cuối của mỗi clip lấy từ khung liền trước (raylib trả về tư thế đầu clip ở đúng thời điểm cuối)
Dùng tư thế gốc Phần vẽ bằng shader của game, draw_instanced3d(), ray3d_model()
Không có armature Xương gốc không có node cha trong glTF: engine cảnh báo, model không có animation

Với entity, njin::model3d giữ tư thế và engine tự tăng thời gian mỗi frame (Entity 3D).

Nhân vật ghép mảnh

Khi mỗi nhân vật là một tổ hợp mảnh (đầu, thân, tay, tóc, mũ) theo một bộ gen, xuất sẵn một file cho mỗi tổ hợp là không được. model_create_skinned() tạo model có xương từ một lưới game tự ghép (njin::skinned_mesh3d_data: vị trí, bốn xương và bốn trọng số mỗi đỉnh, chỉ số), chia thành các phần, mỗi phần một vật liệu. Model mới chép bộ xương của một model có xương đã nạp, và dùng chung các animation của nó: một thư viện clip nạp một lần cho mọi nhân vật.

// Thư viện clip nạp một lần; mỗi tổ hợp mảnh ghép một model.
const njin::model_handle clips = njin::model_load(ctx, "assets/shared_animations.glb");
const njin::model_handle body = njin::model_create_skinned(
ctx, {.positions = pos.data(), .vertex_count = n, .joints = joints.data(), .weights = weights.data(),
.indices = idx.data(), .index_count = (njin::u32)idx.size(), .skeleton = clips});
// Mỗi người một tư thế, một bảng màu trên cùng lưới.
const njin::model_recolor palette[] = {{.material = 0, .color = skin}, {.material = 1, .color = shirt}};
njin::draw_model_anim(ctx, body, at, {.anim = njin::model_anim_find(ctx, body, "Walk_Loop"), .time = t},
njin::colors::white, palette, 2);
Việc Hàm
Chỉ số xương theo tên, để ghép đúng xương vào mảnh model_bone_find(), model_bone_name(), model_bone_count()
Một xương ở một tư thế (gắn đồ vào tay, vùng trúng đòn theo xương) model_bone_pose(): vị trí và ba trục trong không gian model, như draw_model_anim() đặt nó
Nhiều vật liệu đổi màu trong một lần vẽ draw_model_anim() với một mảng njin::model_recolor
Giải phóng thư viện clip trước Model ghép chỉ còn tư thế gốc

Morph target

Morph target (blend shape, shape key trong Blender) là các hình khác của cùng một lưới: mặt cười, mắt nhắm, cơ phồng. model_load() nạp chúng cùng model, theo tên trong extras.targetNames của glTF (Blender ghi tên shape key vào đó). Target cùng tên ở nhiều mesh, như cái nháy mắt ở mặt và ở mi, là một morph.

Trọng số của mỗi lần vẽ được tính theo ba lớp:

  1. Trọng số mặc định trong file (mesh.weights).
  2. Animation của njin::model_pose (anim, trộn với blend_anim) đặt lại những morph mà clip có đường cong (kênh weights của glTF). Model không có xương mà có những clip như vậy thì đó là các animation của nó (model_anim_count()).
  3. Cộng thêm model_pose::morph_weights, mảng theo chỉ số model_morph_find(): game nháy mắt hay nói trong khi clip cử động phần còn lại.
const njin::i32 blink = njin::model_morph_find(ctx, hero, "Blink");
njin::f32 morphs[16] = {};
morphs[blink] = blinking ? 1.0f : 0.0f;
njin::draw_model_anim(ctx, hero, at, {.anim = talk, .time = t, .morph_weights = morphs, .morph_count = 16});
Điều Chi tiết
Trộn ở đâu Trên CPU, vào bộ đệm đỉnh của mesh, ngay trước mỗi lần vẽ có trọng số khác lần trước, rồi xương uốn nó trên GPU. Nên chạy với mọi shader, kể cả shader của game, và bóng đổ theo đúng hình
Vì sao không trên GPU Chỉ còn ba chỗ cho thuộc tính đỉnh (một mặt có hàng chục morph), và sẽ phải sửa mọi shader dựng sẵn lẫn shader của game
Chi phí Mỗi mesh có morph tốn một lần chép đỉnh lên GPU cho mỗi lượt vẽ (cả lượt bóng) khi trọng số đổi; vẽ lại cùng trọng số thì không tốn gì
Đọc trọng số model_morph_weights() trả về đúng trọng số lần vẽ sẽ dùng
Vẽ nhiều bản draw_instanced3d() có bản nhận njin::model_pose: mọi instance của một lệnh có cùng một hình. Bản không có nó vẽ hình mặc định của file
Nhiều hình trong một đám đông Chia instance thành vài nhóm, mỗi nhóm một lệnh với pose riêng (trọng số theo instance cần thêm một bộ đệm instance và một texture độ lệch, chưa có)
Bắn tia ray3d_model() có bản nhận njin::model_pose, thử đúng hình đã morph (bản cũ thử hình trong file)
Mức chi tiết model_lod_build() giữ morph ở mọi mức: độ lệch của mỗi target đi qua cùng phép giản lược, và việc giản lược tính cả độ lệch nên mặt phẳng mà morph làm cong vẫn giữ đủ cạnh
Gộp mesh model_load_desc::merge giữ morph của các mesh được gộp
Tangent Target có TANGENT thì tangent cũng được trộn, nên normal map theo đúng hình
// Một đám đông cùng cười: một lệnh vẽ cho mọi instance.
njin::f32 smile[16] = {};
smile[njin::model_morph_find(ctx, face, "Smile")] = 1.0f;
const njin::model_pose happy{.morph_weights = smile, .morph_count = 16};
njin::draw_instanced3d(ctx, face, crowd, 0, 500, happy);
// Bắn trúng má đang phồng, không phải má trong file.
const njin::ray3d_hit hit = njin::ray3d_model(ctx, shot, face, at, happy);
ray3d_hit ray3d_model(const context &ctx, const ray3d &ray, model_handle model, const transform3d &transform)
Tia với từng tam giác của một model đặt tại transform, như draw_model() vẽ nó.
i32 model_morph_find(const context &ctx, model_handle handle, const char *name)
Tìm morph theo tên (so đúng cả chuỗi).
Tư thế của một model có xương khi vẽ: animation nào, ở giây thứ mấy, và (tùy chọn) trộn với animation...
Definition njin_3d.h:620
Kết quả khi tia chạm một vật.
Definition njin_3d.h:847

Xương lò xo

Tóc, đuôi, râu, vạt áo choàng: những xương không có trong animation nhưng phải trễ theo quán tính khi nhân vật chạy, đung đưa, rồi trở về. spring3d_create() nhận các chuỗi (njin::spring3d_chain: một xương và mọi xương dưới nó) và các collider (njin::spring3d_collider: hình cầu hay viên nang gắn vào một xương, như đầu và thân) mà xương lò xo không xuyên qua. Tham số giống spring bone của VRM.

Mỗi frame, spring3d_update() lấy tư thế của animation (hay xương game đặt), chạy lò xo trong thế giới theo transform của lần vẽ, và ghi tư thế cuối vào một mảng để vẽ bằng model_pose::bones:

njin::bone_pose3d bones[128];
const njin::model_pose run{.anim = run_clip, .time = t};
njin::spring3d_update(ctx, hair, run, at, njin::delta(ctx), bones, 128);
njin::draw_model_anim(ctx, hero, at, {.anim = run_clip, .time = t, .bones = bones});
Trường của njin::spring3d_chain Ý nghĩa
stiffness Lực kéo về tư thế của animation; lớn là cứng
drag 0..1, cản: 0 đung đưa mãi, 1 chỉ trễ rồi về
gravity, gravity_dir Trọng lực kéo đầu xương, trong thế giới
radius Bán kính va chạm của đầu mỗi xương

Mỗi nhân vật cần một bộ lò xo riêng, vì nó nhớ đầu các xương đang ở đâu. Lò xo chạy theo bước 1/60 giây; dt 0 (game tạm dừng) giữ nguyên. Khi dịch chuyển tức thời nhân vật, gọi spring3d_reset() để tóc không bay theo cả quãng đường.

Chép animation sang bộ xương khác

Một bộ clip (chẳng hạn từ Mixamo) cho nhiều nhân vật có bộ xương khác tên, khác tỉ lệ: retarget3d_create() ghép xương của model đích với xương của model nguồn, và retarget3d_pose() cho tư thế của model đích để vẽ bằng model_pose::bones.

  • Ghép xương: các cặp tên game đưa (retarget3d_desc::pairs) trước, rồi theo tên chuẩn của bone_humanoid_name(), hiểu cách đặt tên của Mixamo (mixamorig:LeftForeArm), Unreal (lowerarm_l, spine_02), Unity và VRM (LeftLowerArm) và Blender (forearm.L), rồi theo đúng tên. retarget3d_source_bone() cho biết một xương đã được ghép với xương nào.
  • Góc xoay: xương đích xoay đúng góc mà xương nguồn xoay khỏi tư thế gốc của nó. Hai model cần cùng trục và tư thế gốc giống nhau (cùng chữ T hay cùng chữ A). Xương không được ghép (đuôi ngựa, vũ khí) đi theo xương cha như ở tư thế gốc.
  • Hông: dời theo hông của nguồn, nhân với tỉ lệ độ cao hông của hai model, nên người chân ngắn bước ngắn hơn và chân không trượt.
const njin::retarget3d_handle to_dwarf = njin::retarget3d_create(ctx, {.source = hero, .target = dwarf});
njin::bone_pose3d bones[128];
njin::retarget3d_pose(ctx, to_dwarf, {.anim = run_clip, .time = t}, bones, 128);
njin::draw_model_anim(ctx, dwarf, at, {.bones = bones});

Ví dụ đầy đủ: tóc lò xo, nháy mắt bằng morph, và người lùn chạy bằng clip của người hùng.

#include <njin.h>
#include <cmath>
namespace {
using namespace njin;
model_handle hero, dwarf;
i32 run_clip = -1, blink = -1;
retarget3d_handle hero_to_dwarf;
f32 t = 0.0f;
f32 x = 0.0f;
void load(context &ctx) {
hero = model_load(ctx, "assets/hero.glb"); // có xương, tóc, mặt có morph "Blink"
dwarf = model_load(ctx, "assets/dwarf.glb"); // bộ xương khác tên, chân ngắn hơn
run_clip = model_anim_find(ctx, hero, "Run");
blink = model_morph_find(ctx, hero, "Blink");
// Tóc: xương "hair_1" và mọi xương dưới nó đung đưa, không xuyên qua đầu.
const spring3d_chain chain{.bone = model_bone_find(ctx, hero, "hair_1"), .stiffness = 1.5f, .drag = 0.4f,
.gravity = 0.5f};
const spring3d_collider head{.bone = model_bone_find(ctx, hero, "head"), .offset = {0.0f, 0.1f, 0.0f},
.tail = {0.0f, 0.1f, 0.0f}, .radius = 0.11f};
hair = spring3d_create(ctx, {.model = hero, .chains = &chain, .chain_count = 1, .colliders = &head,
.collider_count = 1});
// Người lùn chạy bằng clip của người hùng: ghép xương theo tên chuẩn.
hero_to_dwarf = retarget3d_create(ctx, {.source = hero, .target = dwarf});
}
void render(context &ctx) {
const f32 dt = delta(ctx);
t += dt;
x += 3.0f * dt;
begin_3d(ctx, {.position = {x - 4.0f, 2.0f, 6.0f}, .target = {x, 1.0f, 0.0f}});
// Nháy mắt 0.15 giây mỗi 3 giây, cộng thêm vào biểu cảm của clip.
f32 morphs[16] = {};
if (blink >= 0 && blink < 16)
morphs[blink] = std::fmod(t, 3.0f) < 0.15f ? 1.0f : 0.0f;
const transform3d at{.position = {x, 0.0f, 0.0f}, .rotation = {0.0f, 90.0f, 0.0f}};
const model_pose pose{.anim = run_clip, .time = t, .morph_weights = morphs, .morph_count = 16};
// Tóc trễ theo quán tính khi chạy.
bone_pose3d bones[128];
if (spring3d_update(ctx, hair, pose, at, dt, bones, 128) > 0) {
model_pose with_hair = pose;
with_hair.bones = bones;
draw_model_anim(ctx, hero, at, with_hair);
}
// Người lùn đi bên cạnh, cùng một clip.
bone_pose3d dwarf_bones[128];
if (retarget3d_pose(ctx, hero_to_dwarf, pose, dwarf_bones, 128) > 0)
draw_model_anim(ctx, dwarf, {.position = {x, 0.0f, 2.0f}, .rotation = {0.0f, 90.0f, 0.0f}},
{.bones = dwarf_bones});
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc anim3d_module() { return {.name = "anim3d", .setup = setup}; }
i32 model_bone_find(const context &ctx, model_handle handle, const char *name)
Tìm xương theo tên (so đúng cả chuỗi).
void draw_model_anim(const context &ctx, model_handle handle, const transform3d &transform, const model_pose &pose, rgba tint=colors::white)
Vẽ model có xương ở tư thế pose.
i32 model_anim_find(const context &ctx, model_handle handle, const char *name)
Tìm animation theo tên đặt trong Blender (tên action khi xuất glTF).
spring3d_handle spring3d_create(context &ctx, const spring3d_desc &desc)
Tạo một bộ xương lò xo cho một nhân vật.
i32 spring3d_update(context &ctx, spring3d_handle handle, const model_pose &pose, const transform3d &transform, f32 dt, bone_pose3d *out, i32 count)
Chạy lò xo dt giây và ghi tư thế cuối của mọi xương vào out: tư thế pose (animation,...
retarget3d_handle retarget3d_create(context &ctx, const retarget3d_desc &desc)
Chuẩn bị chép animation từ desc.source sang desc.target.
i32 retarget3d_pose(const context &ctx, retarget3d_handle handle, const model_pose &pose, bone_pose3d *out, i32 count)
Tư thế của model đích khi model nguồn ở tư thế pose.
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.
Một xương ở một tư thế: vị trí và ba trục của nó trong không gian của model (trước transform của lần ...
Definition njin_3d.h:771
const bone_pose3d * bones
Tư thế do game đặt: model_bone_count() xương, trong không gian của model như model_bone_pose() trả về...
Definition njin_3d.h:633
Định danh của một cách chép animation từ bộ xương này sang bộ xương khác, tạo bởi retarget3d_create()...
Definition _types.h:209
Một chuỗi xương lò xo: xương bone và mọi xương nằm dưới nó (tóc, đuôi, râu, vạt áo).
Definition njin_anim3d.h:16
Một collider mà xương lò xo không xuyên qua (đầu, thân, vai), gắn vào một xương nên đi theo animation...
Definition njin_anim3d.h:30
Định danh của một bộ xương lò xo (tóc, đuôi, áo choàng), tạo bởi spring3d_create().
Definition _types.h:202
Vị trí, hướng và tỉ lệ của một vật 3D.
Definition njin_3d.h:504

Đặt chân lên mặt đất

Animation được làm trên sàn phẳng: đứng trên bậc thang hay dốc thì một bàn chân lơ lửng, bàn kia lún vào đất. foot3d_create() và foot3d_update() đặt từng bàn chân lên mặt đất ngay dưới nó:

  1. Dò thẳng xuống dưới mỗi cổ chân (physics3d_raycast(), hay một hàm njin::foot3d_ground của game cho địa hình hay lưới ô) để biết mặt đất cao bao nhiêu so với gốc của lần vẽ.
  2. Hạ cả người xuống cho bàn chân thấp hơn với tới (đứng ở mép bậc, một chân thõng xuống).
  3. IK hai xương (đùi, cẳng chân) cho mỗi chân: cổ chân tới đúng chỗ, đầu gối gập về phía animation gập nó, hay về knee_forward khi chân thẳng.
  4. Lòng bàn chân nghiêng theo mặt đất (không quá max_tilt, nên mép bậc không làm chân lật).

Bàn chân đang nhấc trong animation (khi bước) vẫn cao hơn mặt đất dưới nó bấy nhiêu, nên đi lên bậc thang vẫn là bước. Độ cao được làm mượt theo smoothing (đi lên nhanh gấp đôi đi xuống, để chân không lún vào bậc vừa bước lên); foot3d_reset() sau khi dịch chuyển tức thời. weight giảm về 0 khi nhân vật rời mặt đất (nhảy, rơi).

Không đưa legs thì foot3d_create() tự tìm hai chân người theo bone_humanoid_name(). Gốc của lần vẽ là chỗ nhân vật đứng: chân của character3d (character3d_position()). Tia dò không trúng character3d, chỉ trúng body và địa hình.

Thứ tự khi dùng chung: retarget3d_pose(), rồi foot3d_update() với pose.bones là kết quả đó, rồi spring3d_update() với pose.bones là kết quả của foot3d (tóc đung đưa theo người đã hạ xuống), rồi vẽ.

#include <njin.h>
namespace {
using namespace njin;
model_handle hero{};
i32 idle = -1;
f32 t = 0.0f;
bone_pose3d placed[128], swayed[128];
void load(context &ctx) {
hero = model_load(ctx, "models/hero.glb");
idle = model_anim_find(ctx, hero, "Idle_Loop");
// Không đưa `legs`: tìm hai chân người theo tên (thigh_l, LeftUpLeg, upper_leg.L...).
feet = foot3d_create(ctx, {.model = hero, .max_step = 0.4f});
const spring3d_chain tail{.bone = model_bone_find(ctx, hero, "hair_1")};
hair = spring3d_create(ctx, {.model = hero, .chains = &tail, .chain_count = 1});
body = character3d_create(ctx, {.position = {0.0f, 0.0f, 0.0f}});
}
void render(context &ctx) {
t += delta(ctx);
// Gốc của lần vẽ là chân của character3d (đáy viên nang).
const transform3d at{.position = character3d_position(ctx, body)};
// Animation, rồi đặt chân lên bậc thang, dốc, đá; nhảy lên thì thôi đặt chân.
const f32 weight = character3d_grounded(ctx, body) ? 1.0f : 0.0f;
foot3d_update(ctx, feet, {.anim = idle, .time = t}, at, delta(ctx), placed, 128, weight);
// Rồi tóc lò xo trên tư thế đó.
spring3d_update(ctx, hair, {.bones = placed}, at, delta(ctx), swayed, 128);
draw_model_anim(ctx, hero, at, {.bones = swayed});
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc feet_module() { return {.name = "feet", .setup = setup}; }
foot3d_handle foot3d_create(context &ctx, const foot3d_desc &desc)
Tạo một bộ đặt chân cho một nhân vật: mỗi frame, mỗi bàn chân của animation được đặt lên mặt đất ngay...
i32 foot3d_update(context &ctx, foot3d_handle handle, const model_pose &pose, const transform3d &transform, f32 dt, bone_pose3d *out, i32 count, f32 weight=1.0f)
Đặt chân lên mặt đất và ghi tư thế cuối vào out, dò đất bằng physics3d_raycast() thẳng xuống dưới mỗi...
vec3 character3d_position(const context &ctx, character3d_handle handle)
Vị trí chân của nhân vật.
bool character3d_grounded(const context &ctx, character3d_handle handle)
Nhân vật có đang đứng trên thứ gì đủ bằng phẳng không (sau bước mô phỏng vừa rồi).
character3d_handle character3d_create(context &ctx, const character3d_desc &desc)
Tạo một nhân vật.
Định danh của một nhân vật vật lý 3D, tạo bởi character3d_create().
Definition _types.h:167
Định danh của một bộ đặt chân (IK chân chạm đất), tạo bởi foot3d_create().
Definition _types.h:215

Đo bằng harness với một người 65 xương, animation đứng yên: trên bậc 0,2 m, mép bậc và dốc 20 độ (dọc, ngang, chéo 45 độ), cổ chân cách mặt đất đúng như trên sàn phẳng, sai tối đa 6 mm, mũi chân không lún; trên sàn phẳng tư thế giống hệt animation.

Ánh sáng

Phần Đặt bằng Ghi chú
Mặt trời, ánh sáng nền light3d_set() với njin::light3d Có hiệu lực từ begin_3d() tiếp theo
Bóng đổ của mặt trời light3d::shadows, shadow_range, shadow_size, shadow_softness Hộp bóng quanh chỗ camera nhìn; mép bóng mềm 3 x 3
Sương mù light3d::fog_color, fog_density Theo khoảng cách tới camera
Đèn điểm, đèn nón light3d_add() với njin::light3d_source mỗi lần vẽ, hoặc entity (Entity 3D) Tối đa njin::light3d_max (16)
Bóng đổ của đèn điểm, đèn nón light3d_source::shadows, light3d::source_shadow_size Tối đa njin::light3d_shadow_max (4) mỗi lần vẽ; đèn sau đó chiếu sáng mà không đổ bóng
Bề mặt material3d_set() với njin::material3d Độ bóng, phát sáng, viền sáng (rim), unlit, texture, cast_shadows

Ánh sáng là Lambert cộng điểm sáng bóng Blinn-Phong, cộng ánh sáng nền. emission cộng màu sau khi chiếu sáng: bật bloom (post_fx_set()) để vật phát sáng lan sáng ra xung quanh.

Bóng của đèn điểm và đèn nón tốn: mọi hình đổ bóng được vẽ thêm 6 lần cho một đèn điểm (6 mặt của một khối lập phương quanh đèn) và 1 lần cho một đèn nón, mỗi mặt một ảnh bóng source_shadow_size pixel. Chỉ bật cho vài đèn quan trọng (đèn pin, đèn treo giữa phòng); radius của đèn cũng là tầm của bóng.

// Đèn pin của người chơi: đèn nón đổ bóng.
njin::light3d_add(ctx, {.kind = njin::light3d_spot, .position = eye, .direction = forward,
.intensity = 2.0f, .radius = 25.0f, .cone = 40.0f, .shadows = true});

Hiệu ứng

Các hiệu ứng chung của Particle và hiệu ứng đều dùng được cho 3D:

Hiệu ứng Với 3D
camera_shake() Rung camera của begin_3d(): độ lệch thành góc nhìn, góc nghiêng thành nghiêng camera
hitstop(), screen_flash(), post_fx_set(), chuyển scene Như 2D, không cần gì thêm
Nháy màu, tan biến fx3d_set() với njin::fx3d: tương đương njin::flash_fx và njin::dissolve_fx của sprite
Hạt particles3d_spawn() dùng lại emitter 2D (njin::fx::explosion(), sparks(), dust()...)

Hình vẽ bằng lệnh (draw_sphere3d(), draw_shape3d(), draw_model()...) không phải entity: game tự giữ thời gian của hiệu ứng và đặt mức mỗi frame. Một entity có njin::shape3d_render hay njin::model3d thì đặt trường fx của component đó thay cho fx3d_set() (Entity 3D). Với lệnh vẽ:

// Mục tiêu trúng đạn: nháy trắng rồi tan trong 0.4 giây.
const float t = age / 0.4f;
njin::fx3d_set(ctx, {.flash = {1, 1, 1, 1 - t * 3}, .dissolve = t});
njin::draw_sphere3d(ctx, pos, 0.6f, njin::colors::red);
njin::fx3d_set(ctx, {});

Hạt 3D quay mặt về camera, dừng trong hitstop, và particles3d_desc::scale đổi đơn vị pixel của các mẫu njin::fx sang đơn vị thế giới 3D.

Hiệu ứng tính trên ảnh 3D theo độ sâu (góc tường tối đi, sàn bóng phản chiếu, vết đạn dán lên bề mặt, mờ chuyển động, tia nắng, lóa ống kính) ở trang riêng: Hiệu ứng màn hình 3D.

Instancing

draw_instanced3d() vẽ count bản của một hình có sẵn hay một model bằng một lệnh vẽ, dữ liệu mỗi bản nằm trong một bộ đệm instance như draw_instanced() của 2D. Không truyền shader thì shader có sẵn đọc:

Thuộc tính Nội dung
instance0 Vị trí xyz, tỉ lệ đều w (0 là 1)
instance1 (từ 8 số) Màu rgba
instance2 (từ 12 số) Góc xoay xyz, độ
instance3 (16 số) Tỉ lệ theo x, y, z (0 là 1)

Hàng chục nghìn instance mà camera chỉ thấy một phần (rừng, thành phố) thì xếp chúng theo ô và chỉ vẽ những ô camera thấy: Chỉ vẽ phần camera thấy: instance theo ô.

Shader của game

shader_begin() trước một lệnh vẽ 3D thì hình đó vẽ bằng shader của game. Engine đặt sẵn các uniform vec3 lightDir, lightColor, ambient, viewPos nếu shader khai báo chúng, cùng mvp, matModel, matNormal, colDiffuse theo tên chuẩn của raylib. Hình SDF luôn dùng shader của engine.

Vật lý và va chạm

njin_physics3d.h dựng trên Jolt Physics (giấy phép MIT, engine kéo về khi build, game không thấy nó). Hình của body dùng cùng quy ước với njin::shape3d, nên một vật và hình vẽ nó dùng chung số.

Phần Tạo bằng Dùng cho
Body tĩnh body3d_create() với body3d_static Sàn, tường, bục cố định
Body kinematic body3d_kinematic, rồi body3d_move_kinematic() mỗi bước Bục di chuyển, thang máy, cửa: chở và đẩy vật khác
Body động body3d_dynamic Thùng, bóng, mảnh vỡ: rơi, va, lăn, bị đẩy; body3d_add_impulse() cho cú nổ, body3d_add_force() và body3d_add_torque() cho lực đẩy liên tục
Nhân vật character3d_create() Người chơi, quái: viên nang đi trên sàn, leo bậc, trượt dọc tường, đẩy body động
Body từ model body3d_desc::model và scale Sàn, dốc, hang làm trong Blender (tĩnh, kinematic); vật lồi (động)
Sensor body3d_desc::sensor Vùng nhặt đồ, checkpoint, bẫy, đích: không va, chỉ báo chạm
Sự kiện chạm physics3d_contact_count(), physics3d_contact() Biết cái gì bắt đầu hay thôi chạm cái gì
Khớp nối joint3d_create() Cửa bản lề, bập bênh, dây xích, piston, lò xo, bánh răng, ròng rọc
Ragdoll ragdoll3d_create() Nhân vật ngã, trúng đòn: bộ xương của model đi theo vật lý
Vật mềm softbody3d_create() Bóng cao su, nệm, khối thạch: biến dạng khi va chạm, giữ phồng bằng áp suất; va chạm với nhau, nhân vật đứng lên được
Vải cloth3d_create() Lá cờ, rèm, áo choàng: ghim vào một chỗ, bay theo gió
Xe có bánh vehicle3d_create() Ô tô: động cơ, hộp số tự động, lái, phanh, giảm xóc
Xe máy motorcycle3d_create() Xe hai bánh: tự giữ thăng bằng, nghiêng vào cua
Xe bánh xích tracked3d_create() Xe tăng, máy xúc: rẽ bằng hai dải xích, quay tại chỗ
Tia physics3d_raycast() Đạn, tầm nhìn, camera không xuyên tường; trả về body bị trúng, đi xuyên sensor; bản có njin::soft3d_hit trúng cả vật mềm

Engine mô phỏng ở phase_fixed_update, ngay sau các system của game trong phase đó: game đặt vận tốc hay vị trí đích, rồi vật lý chạy luôn trong cùng bước. Nhân vật được điều khiển bằng vận tốc, và game tự cộng trọng lực và cú nhảy (để tự chỉnh cảm giác nhảy: coyote time, nhảy đôi...):

// Mỗi bước cố định:
const bool on_ground = njin::character3d_grounded(ctx, player);
vertical = on_ground ? 0.0f : vertical - gravity * dt;
if (jump_pressed && on_ground)
vertical = jump_speed;
// Đứng trên bục đang chạy thì cộng vận tốc của bục.
const njin::vec3 ground = njin::character3d_ground_velocity(ctx, player);
njin::character3d_set_velocity(ctx, player, walk + ground + njin::vec3{0, vertical, 0});

Các nhân vật chặn nhau: không đi xuyên qua nhau mà trượt vòng qua, như với tường. Engine chỉ so mỗi nhân vật với những người ở gần (một lưới ô), nên cả trăm nhân vật vẫn rẻ; phần tốn là mỗi nhân vật đang bật cần một lần tính va chạm mỗi bước (vài chục micro giây). Một đám đông lớn nên chỉ bật những người camera thấy bằng character3d_set_active(), còn người ở xa game tự dời theo đường đi:

bool seen = false;
njin::camera3d_to_screen(ctx, cam, feet, &seen); // hay cách game tự biết
if (seen != njin::character3d_active(ctx, walker)) {
if (seen)
njin::character3d_set_position(ctx, walker, feet); // bật lại ở chỗ game đã dời tới
njin::character3d_set_active(ctx, walker, seen);
}

Vẽ một body động theo đúng chỗ và góc vật lý tính ra bằng body3d_transform():

const njin::transform3d t = njin::body3d_transform(ctx, crate);
njin::draw_shape3d(ctx, {.kind = njin::shape3d_box, .position = t.position, .rotation = t.rotation,
.size = {1, 1, 1}}, {0.7f, 0.5f, 0.3f, 1.0f});

Một entity có component njin::body3d thì không cần bước này: engine ghi transform cho nó (Entity 3D).

body3d_desc::user gắn một số của game vào body (chỉ số trong mảng, id entity): đọc lại bằng body3d_user() từ body mà physics3d_raycast(), character3d_ground_body() hay physics3d_contact() trả về.

Body từ model

Có body3d_desc::model thì hình của body lấy từ lưới tam giác của model, đặt như draw_model() vẽ nó với cùng position, rotation và scale. Nên một model vừa là hình vẽ vừa là mặt đất để đi:

const njin::model_handle hill = njin::model_load(ctx, "assets/hill.glb");
njin::body3d_create(ctx, {.position = hill_pos, .model = hill}); // tĩnh: đúng từng tam giác
Loại body Hình từ model
Tĩnh, kinematic Đúng từng tam giác: dốc, bậc, hang đều đi được
Động Bao lồi của các đỉnh (hình lồi nhỏ nhất bọc model): chỗ lõm bị lấp

Sensor và sự kiện chạm

Body có sensor = true không va chạm: vật và nhân vật đi xuyên qua, physics3d_raycast() cũng xuyên qua, còn engine báo sự kiện chạm. Mỗi bước mô phỏng, physics3d_contact_count() và physics3d_contact() cho các njin::contact3d: hai body (a, b), hoặc một body a và một nhân vật character (khi đó b không hợp lệ); began là bắt đầu hay thôi chạm, sensor nếu một bên là sensor, cùng điểm và pháp tuyến chạm.

Một cặp chỉ báo một lần khi bắt đầu chạm và một lần khi thôi chạm, dù chạm ở nhiều điểm. Body bị hủy không có sự kiện thôi chạm. Đọc sự kiện trong phase_fixed_update: mỗi bước game thấy đúng sự kiện của bước trước, không sót, không lặp.

// Trong một system của phase_fixed_update: người chơi chạm đồng xu (sensor) thì nhặt.
entt::registry &reg = njin::world(ctx);
for (njin::i32 i = 0; i < njin::physics3d_contact_count(ctx); i++) {
const njin::contact3d c = njin::physics3d_contact(ctx, i);
if (!c.began || !c.sensor || c.character.id != player.id)
continue;
const auto e = (entt::entity)njin::body3d_user(ctx, c.a); // user = id entity của đồng xu
if (reg.valid(e) && reg.all_of<coin>(e))
reg.destroy(e); // component body3d hủy luôn body của nó
}

Khớp nối

joint3d_create() nối hai body, hoặc một body với một điểm cố định của thế giới (b không hợp lệ). Điểm nối anchor và trục axis tính trong tọa độ thế giới, lúc tạo khớp. Hủy một body thì hủy luôn các khớp của nó.

Loại Làm gì Ví dụ
joint3d_fixed Hàn cứng: giữ nguyên vị trí và góc tương đối Gắn hai mảnh thành một vật
joint3d_point Khớp cầu: xoay tự do quanh anchor Dây xích, con lắc
joint3d_hinge Bản lề: xoay quanh axis qua anchor Cửa, bập bênh, bánh xe
joint3d_slider Trượt dọc axis, không xoay Piston, ngăn kéo, cửa kéo
joint3d_distance Giữ khoảng cách giữa anchor và anchor_b trong [min, max] Dây, thanh nối

min và max là giới hạn: góc, độ (bản lề); quãng trượt tính từ vị trí lúc tạo (khớp trượt); khoảng cách (njin::joint3d_distance). Với bản lề và khớp trượt, min >= max là không giới hạn. motor_force lớn hơn 0 cho bản lề và khớp trượt một mô-tơ: joint3d_set_motor() đặt tốc độ, joint3d_position() đọc góc hay quãng trượt hiện tại.

// Bập bênh: tấm ván động trên một bản lề, nghiêng tối đa 18 độ mỗi bên.
const njin::body3d_handle plank = njin::body3d_create(
ctx, {.position = pivot, .size = {4, 0.2f, 1}, .motion = njin::body3d_dynamic, .mass = 25});
njin::joint3d_create(ctx, {.kind = njin::joint3d_hinge, .a = plank, .anchor = pivot, .axis = {0, 0, 1},
.min = -18, .max = 18});

Lò xo, truyền động và lực

Các khớp và hàm dưới đây đủ để lắp một cỗ máy từ linh kiện: động cơ, pít-tông, lò xo, dây chun, bánh răng, xích, thanh răng, ròng rọc, cánh quạt.

Linh kiện Làm bằng
Lò xo, giảm xóc spring (tần số, Hz) và damping trên bản lề, khớp trượt, njin::joint3d_distance: giới hạn [min, max] thành lò xo; không có giới hạn thì lò xo giữ khớp ở vị trí lúc tạo
Dây chun njin::joint3d_distance với min = 0, max là chiều dài lúc chùng và spring: chỉ kéo khi bị căng
Động cơ dây chun, chân ga joint3d_set_motor_force() đổi lực mô-tơ lúc chạy (0 là tắt, khớp quay tự do); joint3d_speed() đếm số vòng đã vặn
Bánh răng, xích, dây curoa njin::joint3d_gear nối hai bản lề joint_a, joint_b với tỉ số ratio: dương là ngược chiều (bánh răng ăn khớp), âm là cùng chiều (xích, curoa)
Thanh răng njin::joint3d_rack nối bản lề joint_a (bánh) và khớp trượt joint_b (thanh): thanh trượt ratio đơn vị mỗi vòng bánh
Ròng rọc, palăng njin::joint3d_pulley: dây từ a vắt qua pulley_a, pulley_b xuống b; ratio 2 là palăng
Cánh quạt, ống phụt body3d_add_force() tại một điểm, mỗi bước cố định; joint3d_speed() cho tốc độ quay của cánh
Máy hỏng joint3d_force(), joint3d_torque() cho lực khớp đang chịu: quá ngưỡng thì joint3d_destroy()

Bánh răng tính theo góc quay của mỗi bánh so với khung của nó (body b của bản lề), nên vẫn đúng khi cả cỗ máy chạy và xoay. Mô-men truyền theo tỉ số: bánh nhỏ (ratio 3) quay bánh lớn chậm hơn ba lần với mô-men gấp ba. Hủy một bản lề thì bánh răng hay thanh răng trên nó bị hủy theo. Răng và mắt xích chỉ là hình vẽ: khớp giữ tỉ số, không cần va chạm giữa các răng.

// Bánh nhỏ 10 răng có mô-tơ quay bánh lớn 30 răng, cả hai trên khung `frame`, trục z.
const njin::joint3d_handle small = njin::joint3d_create(ctx, {.kind = njin::joint3d_hinge, .a = pinion,
.b = frame, .anchor = {0, 1, 0}, .axis = {0, 0, 1}, .motor_force = 50});
const njin::joint3d_handle big = njin::joint3d_create(ctx, {.kind = njin::joint3d_hinge, .a = wheel,
.b = frame, .anchor = {2, 1, 0}, .axis = {0, 0, 1}});
njin::joint3d_create(ctx, {.kind = njin::joint3d_gear, .joint_a = small, .joint_b = big, .ratio = 3});
njin::joint3d_set_motor(ctx, small, 360); // bánh lớn quay 120 độ mỗi giây, ngược chiều
// Pít-tông có đệm lò xo cuối hành trình: trượt tự do dọc y trong 0..0.5, vượt ra thì lò xo 4 Hz kéo lại.
njin::joint3d_create(ctx, {.kind = njin::joint3d_slider, .a = rod, .b = cylinder, .axis = {0, 1, 0},
.min = 0, .max = 0.5f, .spring = 4, .damping = 0.3f});

Ragdoll

ragdoll3d_create() biến một model có xương thành ragdoll: mỗi xương được chọn là một viên nang động, nối với phần cha bằng khớp có giới hạn góc (Ragdoll của Jolt), rơi và va chạm như mọi body. Các phần của cùng một ragdoll không va vào nhau. Các xương không được chọn (ngón tay, gốc) đi theo phần gần nhất phía trên chúng. Giới hạn góc tính từ tư thế gốc trong file (tư thế T hay A), nên ragdoll bắt đầu được từ bất kỳ khung animation nào: thường là tư thế vừa vẽ, lúc nhân vật trúng đòn hay ngã.

Mỗi khung, ragdoll3d_bones() đọc tư thế của ragdoll vào một mảng, và model_pose::bones vẽ model theo mảng đó thay cho animation. model_bone_pose() với tư thế ấy cũng trả về xương của ragdoll (để camera đi theo đầu, chẳng hạn). ragdoll3d_body() trả về body của một phần, để đẩy bằng body3d_add_impulse() hay nhận ra phần bị trúng trong physics3d_raycast().

Trường của njin::ragdoll3d_bone Ý nghĩa
name, radius, length Xương và viên nang dọc theo trục y của nó; radius, length 0 là đo từ da của model
swing, twist Khớp cầu: góc lệch và góc vặn tối đa, độ
bend_min, bend_max Bản lề quanh trục x của xương thay cho khớp cầu (gối, khuỷu tay)
// Người chơi ngã: từ tư thế đang vẽ, giữ vận tốc lúc chạy.
rag = njin::ragdoll3d_create(ctx, {.model = man, .transform = at, .pose = pose, .bones = parts,
.bone_count = std::size(parts), .velocity = velocity});
// Mỗi khung, trong phase_render:
static njin::bone_pose3d bones[128];
njin::ragdoll3d_bones(ctx, rag, at, bones, 128);
njin::draw_model_anim(ctx, man, at, {.bones = bones});

Ví dụ danh sách parts cho mannequin của Quaternius nằm ở ragdoll3d_create().

Vật mềm và vải

Vật mềm là một đám điểm (đỉnh) nối bằng lò xo, mỗi bước vật lý biến dạng theo trọng lực, va chạm và áp suất bên trong (soft body của Jolt). softbody3d_create() dựng nó từ một hình có sẵn hoặc từ lưới của game; cloth3d_create() dựng một tấm vải chữ nhật. Cả hai trả về njin::softbody3d_handle và dùng chung các hàm softbody3d_*.

Hình (njin::softbody3d_kind) Dựng thế nào Ví dụ
softbody3d_box Lưới điểm đặc bên trong hộp size, giữ thể tích Nệm, khối thạch, khối cao su
softbody3d_sphere Mặt cầu rỗng bán kính radius; đặt pressure để phồng Quả bóng, bong bóng
softbody3d_mesh Mặt của mesh hoặc mọi lưới của model (đỉnh trùng vị trí được gộp) Gối, đồ chơi bơm hơi

stiffness (0..1) là độ cứng của các cạnh: 1 là không dãn. bend là độ cứng khi gập. pressure là áp suất bên trong khi vật đúng hình lúc tạo, Pa: bóp nhỏ thì áp suất tăng như bóng bay. Quả bóng 1 kg bán kính 0.5 cần khoảng 50 (mềm, lún khi chạm đất) đến 300 (căng); lớn hơn nữa thì vật phồng to hơn lúc tạo.

Vật mềm va chạm với mọi body, và với các vật mềm khác cũng bật collide_soft (mặc định bật): hai tấm vải chồng lên nhau, tấm vải rơi lên võng, quả bóng rơi lên nệm. Mỗi đỉnh giữ cách mặt của vật kia một khoảng bằng độ dày của hai vật (thickness của vải). Jolt không tính va chạm giữa hai vật mềm, nên engine tự tính trước mỗi bước vật lý: đỉnh nào sắp lấn vào mặt của vật kia thì bị hãm lại bằng vận tốc, phần đà của nó truyền sang mặt bị chạm (võng trũng xuống dưới tấm vải). Hai vật ở xa nhau chỉ tốn một phép so khung bao; tắt collide_soft cho vật mềm không bao giờ gặp vật mềm khác.

Nhân vật đứng được trên mặt trên của vật mềm bật walkable (mặc định bật): nệm lún xuống theo sức nặng của nhân vật (njin::character3d_desc::mass), bạt nhún võng xuống. Mặt dựng đứng (tấm rèm) thì nhân vật vẫn đi xuyên, đẩy các đỉnh sang bên, nên rèm không chặn đường. character3d_ground_soft() cho biết nhân vật đang đứng trên vật mềm nào (character3d_ground_body() khi đó không hợp lệ), còn character3d_ground_velocity() là vận tốc của mặt vật mềm dưới chân: cộng vào cú nhảy để bật cao hơn khi bạt đang nảy lên.

physics3d_raycast() và các hàm physics3d_*_push, _cast đi xuyên qua vật mềm. Các bản physics3d_raycast(), physics3d_box_cast() và physics3d_hull_cast() có thêm tham số njin::soft3d_hit thì trúng cả vật mềm: bắn vào tấm rèm thì trúng rèm, và biết vật mềm, tam giác và đỉnh gần chỗ trúng nhất.

// Bắn vào rèm: chỗ bị trúng bị đẩy theo hướng đạn.
njin::soft3d_hit soft;
const njin::ray3d_hit hit = njin::physics3d_raycast(ctx, shot, 100.0f, nullptr, &soft);
if (hit.hit && soft.soft.id != 0)
njin::softbody3d_add_impulse(ctx, soft.soft, shot.direction * 0.5f);

Vẽ vật mềm bằng softbody3d_model(): một model luôn có hình hiện tại của vật, trong thế giới, engine cập nhật sau mỗi bước vật lý. Vẽ nó bằng draw_model() với transform mặc định, đổi màu và ảnh bằng model_material_set(); model thuộc về vật mềm, softbody3d_destroy() hủy nó. Vật nhiều đỉnh (quá 65535, vải tính gấp đôi vì có mặt sau) được chia thành nhiều lưới trong cùng một model. Muốn tự vẽ thì đọc softbody3d_vertices(), softbody3d_normals() và softbody3d_indices(); softbody3d_draw_debug() vẽ các cạnh và đỉnh bị ghim bằng gizmo.

Đỉnh của tấm vải ở hàng r, cột c có chỉ số r * (columns + 1) + c; hàng 0 là cạnh trên. Vải nằm trong mặt phẳng x–y của nó (đứng thẳng như lá cờ), rotation.x = 90 cho vải nằm ngang. Ghim các cạnh bằng pin_edges, các đỉnh khác bằng pinned hay softbody3d_pin(); softbody3d_nearest() tìm đỉnh gần một điểm.

Hàm Làm gì
softbody3d_pin() Ghim hay bỏ ghim một đỉnh: đỉnh bị ghim đứng yên
softbody3d_move_pinned() Dời một đỉnh bị ghim tới chỗ mới sau bước tới, kéo phần còn lại theo (áo choàng ghim vào vai)
softbody3d_set_wind() Gió thổi qua: mặt nào chắn gió thì bị đẩy theo drag và diện tích
softbody3d_add_impulse() Đẩy cả vật một cú, chia đều cho các đỉnh: đá quả bóng
softbody3d_position() Tâm của vật, để camera đi theo
// Áo choàng: tấm vải ghim cạnh trên, mỗi bước cố định kéo hai góc trên theo hai vai.
cape = njin::cloth3d_create(ctx, {.position = back, .size = {0.6f, 1.0f}, .columns = 8, .rows = 12,
.pin_edges = njin::cloth3d_top});
// Mỗi bước cố định:
njin::softbody3d_move_pinned(ctx, cape, 0, shoulder_left);
njin::softbody3d_move_pinned(ctx, cape, 8, shoulder_right);

Xe có bánh

vehicle3d_create() tạo một ô tô trên bộ điều khiển xe của Jolt: thân xe là một body động hình hộp size (hay bao lồi của model), mỗi bánh là một giảm xóc dò mặt đất, có động cơ, hộp số tự động, vi sai, phanh và phanh tay. Đầu xe hướng +z: xe có rotation 0 chạy theo trục z. Mặc định có bốn bánh ở bốn góc dưới của thân: hai bánh trước lái, cả bốn bánh kéo, hai bánh sau có phanh tay; wheels cho bánh của game (xe ba bánh, xe tải sáu bánh).

Lái bằng vehicle3d_set_input() mỗi bước cố định: ga (-1 lùi .. 1 tiến), lái (-1 trái .. 1 phải), phanh và phanh tay. Đang chạy tới mà ga âm thì xe phanh trước, dừng hẳn rồi mới lùi, như người lái thật. vehicle3d_body() là thân xe (body3d_transform() để vẽ, body3d_velocity() cho đồng hồ tốc độ), vehicle3d_wheel_transform() là chỗ và góc của từng bánh lúc này: trục y của nó là trục bánh, nên vẽ bánh bằng một hình trụ. vehicle3d_rpm() và vehicle3d_gear() cho tiếng máy và đồng hồ.

engine_torque mạnh quá sức bám của lốp thì bánh quay trượt, và hộp số không lên số khi bánh đang trượt.

Cảnh dưới đây có một lá cờ bay trong gió, một quả bóng và một chiếc xe lái bằng phím mũi tên:

#include <njin.h>
namespace {
using namespace njin;
void load(context &ctx) {
light3d_set(ctx, {.direction = {-0.5f, -1.0f, -0.3f}, .shadows = true});
// Mặt đất: một hộp tĩnh lớn, mặt trên ở y = 0.
body3d_create(ctx, {.position = {0.0f, -0.5f, 0.0f}, .size = {200.0f, 1.0f, 200.0f}});
// Cột cờ và lá cờ ghim cạnh trái vào cột, gió thổi chéo.
body3d_create(ctx, {.shape = shape3d_cylinder, .position = {-4.0f, 2.0f, 0.0f}, .radius = 0.05f, .height = 4.0f});
flag = cloth3d_create(ctx, {.position = {-3.0f, 3.4f, 0.0f}, .size = {2.0f, 1.2f}, .columns = 20, .rows = 12,
.pin_edges = cloth3d_left});
softbody3d_set_wind(ctx, flag, {6.0f, 0.0f, 2.0f});
model_material_set(ctx, softbody3d_model(ctx, flag), 0, {.color = {0.85f, 0.15f, 0.15f, 1.0f}});
// Quả bóng: mặt cầu có áp suất bên trong, nảy và lún khi rơi.
ball = softbody3d_create(ctx, {.kind = softbody3d_sphere, .position = {0.0f, 3.0f, 2.0f}, .radius = 0.5f,
.pressure = 200.0f});
// Xe bốn bánh mặc định: hai bánh trước lái, cả bốn bánh kéo.
car = vehicle3d_create(ctx, {.position = {4.0f, 1.0f, 0.0f}});
}
void drive(context &ctx) {
// Mũi tên lên/xuống là ga và lùi, trái/phải là lái, phím cách là phanh tay.
const f32 throttle = (key_held(ctx, key_up) ? 1.0f : 0.0f) - (key_held(ctx, key_down) ? 1.0f : 0.0f);
const f32 steer = (key_held(ctx, key_right) ? 1.0f : 0.0f) - (key_held(ctx, key_left) ? 1.0f : 0.0f);
vehicle3d_set_input(ctx, car, throttle, steer, 0.0f, key_held(ctx, key_space) ? 1.0f : 0.0f);
// Đá quả bóng lên khi nhấn B.
if (key_pressed(ctx, key_b))
softbody3d_add_impulse(ctx, ball, {0.0f, 6.0f, 0.0f});
}
void render(context &ctx) {
const transform3d body = body3d_transform(ctx, vehicle3d_body(ctx, car));
begin_3d(ctx, {.position = body.position + vec3{-6.0f, 5.0f, -8.0f}, .target = body.position});
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {200.0f, 200.0f}, {0.55f, 0.6f, 0.5f, 1.0f});
draw_cylinder3d(ctx, {-4.0f, 0.0f, 0.0f}, {-4.0f, 4.0f, 0.0f}, 0.05f, colors::gray);
// Vải và bóng: model của vật mềm luôn có hình hiện tại, vẽ ở transform mặc định.
draw_model(ctx, softbody3d_model(ctx, flag), {});
// Thân xe theo body của nó, mỗi bánh là một hình trụ theo transform của bánh.
draw_shape3d(ctx, {.kind = shape3d_box, .position = body.position, .rotation = body.rotation,
.size = {1.8f, 0.6f, 4.0f}, .rounding = 0.1f},
for (i32 i = 0; i < vehicle3d_wheel_count(ctx, car); i++) {
const transform3d w = vehicle3d_wheel_transform(ctx, car, i);
draw_shape3d(ctx, {.kind = shape3d_cylinder, .position = w.position, .rotation = w.rotation, .radius = 0.35f,
.height = 0.25f},
{0.15f, 0.15f, 0.15f, 1.0f});
}
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_fixed_update, drive, "drive");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc physics3d_soft_module() { return {.name = "physics3d_soft", .setup = setup}; }
void model_material_set(context &ctx, model_handle handle, i32 index, const model_material &material)
Đặt vật liệu thứ index của model, cho mọi lần vẽ sau đó.
void draw_cylinder3d(const context &ctx, vec3 from, vec3 to, f32 radius, rgba color)
Vẽ hình trụ đặc nối hai điểm (lưới tam giác).
@ shape3d_cylinder
Hình trụ dọc trục y: radius, height, bo cạnh theo rounding.
Definition njin_3d.h:252
@ shape3d_box
Hộp: size, bo cạnh theo rounding.
Definition njin_3d.h:250
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).
bool key_held(const context &ctx, key_code key)
Đúng khi phím đã được giữ từ frame trước và vẫn đang giữ.
@ key_space
Phím cách.
Definition _types.h:411
@ key_right
Mũi tên phải.
Definition _types.h:401
@ key_up
Mũi tên lên.
Definition _types.h:398
@ key_b
Phím B.
Definition _types.h:345
@ key_down
Mũi tên xuống.
Definition _types.h:399
@ key_left
Mũi tên trái.
Definition _types.h:400
@ phase_fixed_update
Theo nhịp cố định, 0 hoặc nhiều lần mỗi frame. Cho vật lý.
Definition _mod.h:34
transform3d vehicle3d_wheel_transform(const context &ctx, vehicle3d_handle handle, i32 wheel)
Chỗ và góc của một bánh lúc này (đã tính giảm xóc, góc lái và bánh quay), trong thế giới.
void softbody3d_set_wind(context &ctx, softbody3d_handle handle, vec3 wind)
Đặt gió thổi qua vật mềm, vận tốc của không khí (đơn vị mỗi giây).
model_handle softbody3d_model(context &ctx, softbody3d_handle handle)
Một model luôn có hình hiện tại của vật mềm, trong thế giới: engine cập nhật nó sau mỗi bước vật lý.
void vehicle3d_set_input(context &ctx, vehicle3d_handle handle, f32 throttle, f32 steer, f32 brake, f32 handbrake)
Đặt cách lái cho bước vật lý tới.
softbody3d_handle cloth3d_create(context &ctx, const cloth3d_desc &desc)
Tạo một tấm vải: lá cờ, rèm, áo choàng, khăn trải bàn.
i32 vehicle3d_wheel_count(const context &ctx, vehicle3d_handle handle)
Số bánh của xe.
softbody3d_handle softbody3d_create(context &ctx, const softbody3d_desc &desc)
Tạo một vật mềm.
body3d_handle vehicle3d_body(const context &ctx, vehicle3d_handle handle)
Thân xe, để đọc vị trí (body3d_transform()), vận tốc, hay đẩy nó.
body3d_handle body3d_create(context &ctx, const body3d_desc &desc)
Tạo một body vật lý 3D.
void softbody3d_add_impulse(context &ctx, softbody3d_handle handle, vec3 impulse)
Đẩy cả vật mềm một cú (xung lực, kg * đơn vị mỗi giây), chia đều cho các đỉnh không bị ghim: cú đá và...
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).
vehicle3d_handle vehicle3d_create(context &ctx, const vehicle3d_desc &desc)
Tạo một xe có bánh: thân xe là body động, mỗi bánh là một giảm xóc dò mặt đất (không phải body),...
@ cloth3d_left
Cột đầu (cột 0).
Definition njin_physics3d.h:811
@ softbody3d_sphere
Mặt cầu bán kính radius, rỗng: thêm pressure để thành quả bóng.
Definition njin_physics3d.h:725
constexpr rgba blue
Xanh dương.
Definition _math.h:238
constexpr rgba gray
Xám.
Definition _math.h:240
Định danh của một vật mềm (vải, bóng, khối cao su), tạo bởi softbody3d_create() hoặc cloth3d_create()...
Definition _types.h:196
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
Vector 3 chiều.
Definition _types.h:47
Định danh của một xe có bánh, tạo bởi vehicle3d_create().
Definition _types.h:222

Xe máy và xe bánh xích

motorcycle3d_create() tạo một xe máy trên bộ điều khiển xe máy của Jolt: một bánh trước lái trên phuộc nghiêng, một bánh sau kéo, và một lò xo giữ xe đứng thẳng, nghiêng xe vào cua theo tốc độ và góc lái. Xe đứng yên không đổ, chạy nhanh thì góc lái được engine giảm bớt để xe không văng (cua rộng hơn); max_lean là góc nghiêng lớn nhất khi vào cua. Số mặc định là một xe 240 kg kể cả người lái, chạy ổn không cần chỉnh.

tracked3d_create() tạo một xe bánh xích (xe tăng, máy xúc): mỗi bên một dải xích chạy trên wheels_per_side bánh, rẽ bằng cách cho hai dải chạy khác tốc độ. vehicle3d_set_input() lái nó như ô tô; khi xe gần đứng yên mà chỉ lái không ga, hai dải chạy ngược chiều và xe quay tại chỗ. vehicle3d_set_tracks() điều khiển thẳng từng dải (-1 lùi .. 1 tiến), vehicle3d_track_speed() cho tốc độ của dải xích để cuộn ảnh của nó.

Cả hai trả về njin::vehicle3d_handle, nên mọi hàm vehicle3d_* dùng được: vehicle3d_body() cho thân xe, vehicle3d_wheel_transform() cho từng bánh, vehicle3d_rpm(), vehicle3d_gear(). vehicle3d_draw_debug() vẽ thân, bánh, giảm xóc và điểm bánh chạm đất bằng gizmo, cho mọi loại xe.

#include <njin.h>
namespace {
using namespace njin;
bool riding_bike = true;
bool debug = false;
void load(context &ctx) {
light3d_set(ctx, {.direction = {-0.5f, -1.0f, -0.3f}, .shadows = true});
// Mặt đất: hộp tĩnh lớn, ma sát cao như mặt đường.
body3d_create(ctx, {.position = {0.0f, -0.5f, 0.0f}, .size = {400.0f, 1.0f, 400.0f}, .friction = 1.0f});
// Xe máy và xe tăng với số mặc định.
bike = motorcycle3d_create(ctx, {.position = {0.0f, 1.0f, 0.0f}});
tank = tracked3d_create(ctx, {.position = {6.0f, 2.0f, 0.0f}});
}
void drive(context &ctx) {
// Tab đổi xe, G bật tắt hình debug.
if (key_pressed(ctx, key_tab))
riding_bike = !riding_bike;
if (key_pressed(ctx, key_g))
debug = !debug;
// Mũi tên lên/xuống là ga và lùi, trái/phải là lái, phím cách là phanh.
const f32 gas = (key_held(ctx, key_up) ? 1.0f : 0.0f) - (key_held(ctx, key_down) ? 1.0f : 0.0f);
const f32 steer = (key_held(ctx, key_right) ? 1.0f : 0.0f) - (key_held(ctx, key_left) ? 1.0f : 0.0f);
const f32 brake = key_held(ctx, key_space) ? 1.0f : 0.0f;
// Xe không lái thì phanh lại cho đứng yên.
vehicle3d_set_input(ctx, bike, riding_bike ? gas : 0.0f, riding_bike ? steer : 0.0f, riding_bike ? brake : 1.0f, 0.0f);
vehicle3d_set_input(ctx, tank, riding_bike ? 0.0f : gas, riding_bike ? 0.0f : steer, riding_bike ? 1.0f : brake, 0.0f);
}
void render(context &ctx) {
const transform3d at = body3d_transform(ctx, vehicle3d_body(ctx, riding_bike ? bike : tank));
begin_3d(ctx, {.position = at.position + vec3{-6.0f, 4.0f, -8.0f}, .target = at.position});
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {400.0f, 400.0f}, {0.55f, 0.6f, 0.5f, 1.0f});
// Thân xe máy và xe tăng theo body của chúng.
const transform3d b = body3d_transform(ctx, vehicle3d_body(ctx, bike));
draw_shape3d(ctx, {.kind = shape3d_box, .position = b.position, .rotation = b.rotation, .size = {0.4f, 0.6f, 0.8f}},
const transform3d t = body3d_transform(ctx, vehicle3d_body(ctx, tank));
draw_shape3d(ctx, {.kind = shape3d_box, .position = t.position, .rotation = t.rotation, .size = {3.4f, 1.0f, 6.4f}},
{0.35f, 0.45f, 0.3f, 1.0f});
// Mỗi bánh là một hình trụ theo transform của bánh (trục y là trục bánh).
for (const vehicle3d_handle v : {bike, tank})
for (i32 i = 0; i < vehicle3d_wheel_count(ctx, v); i++) {
const transform3d w = vehicle3d_wheel_transform(ctx, v, i);
const f32 radius = v.id == bike.id ? 0.31f : 0.3f;
draw_shape3d(ctx, {.kind = shape3d_cylinder, .position = w.position, .rotation = w.rotation, .radius = radius,
.height = v.id == bike.id ? 0.05f : 0.1f},
{0.15f, 0.15f, 0.15f, 1.0f});
}
// Hình debug: thân, bánh, giảm xóc và chỗ bánh chạm đất.
if (debug) {
}
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_fixed_update, drive, "drive");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc physics3d_vehicles_module() { return {.name = "physics3d_vehicles", .setup = setup}; }
@ key_tab
Tab.
Definition _types.h:413
@ key_g
Phím G.
Definition _types.h:350
vehicle3d_handle motorcycle3d_create(context &ctx, const motorcycle3d_desc &desc)
Tạo một xe máy (bộ điều khiển xe máy của Jolt): một lò xo giữ xe đứng thẳng và nghiêng vào cua theo t...
void vehicle3d_draw_debug(context &ctx, vehicle3d_handle handle, rgba color={1.0f, 0.6f, 0.2f, 1.0f})
Vẽ xe bằng gizmo (njin_gizmo.h) cho frame này: hộp thân xe, mỗi bánh là một vòng tròn với trục,...
vehicle3d_handle tracked3d_create(context &ctx, const tracked3d_desc &desc)
Tạo một xe bánh xích (bộ điều khiển xe xích của Jolt): rẽ bằng cách cho hai dải xích chạy khác tốc độ...

Entity 3D

Thay vì gọi lệnh vẽ và đọc body mỗi frame, một entity (Entity, component và event) có thể mang component 3D. Engine vẽ và cập nhật chúng theo njin::transform3d của entity, và inspector hiện đủ các component này.

Component Engine làm gì
njin::transform3d Vị trí, góc xoay, tỉ lệ của entity; các component dưới đây đọc hoặc ghi nó
njin::model3d Vẽ model tại transform; tăng pose.time và pose.blend_time mỗi frame (theo delta(), nhân speed)
njin::shape3d_render Vẽ hình SDF shape tại transform (bỏ qua shape.position và shape.rotation)
njin::light3d_source Đèn đặt tại transform3d::position (bỏ qua position), chiếu mọi lần vẽ 3D
njin::body3d Động: sau mỗi bước ghi vị trí và góc của body vào transform. Kinematic: trước mỗi bước đưa body tới transform. Tĩnh: không làm gì
njin::character3d Sau mỗi bước ghi vị trí chân của nhân vật vào transform3d::position (góc xoay do game đặt)

Gỡ njin::body3d hay njin::character3d, hoặc hủy entity, thì body hay nhân vật bị hủy theo. njin::model3d và njin::shape3d_render có fx (nháy màu, tan biến như fx3d_set()) và visible để ẩn mà không gỡ component. Entity được vẽ ở end_3d() của mọi lần vẽ có camera3d::entities (mặc định bật), cùng các lệnh vẽ của game; tắt nó cho một cảnh phụ, như model xoay trong menu.

// Một thùng: vật lý đặt transform, shape3d_render vẽ đúng chỗ đó.
entt::registry &reg = njin::world(ctx);
const auto crate = reg.create();
reg.emplace<njin::transform3d>(crate);
reg.emplace<njin::shape3d_render>(crate, njin::shape3d_render{
.shape = {.kind = njin::shape3d_box, .size = {0.9f, 0.9f, 0.9f}, .rounding = 0.05f},
.color = {0.7f, 0.5f, 0.3f, 1.0f}});
reg.emplace<njin::body3d>(crate, njin::body3d_create(ctx, {.position = {0, 3, 0}, .size = {0.9f, 0.9f, 0.9f},
.motion = njin::body3d_dynamic, .mass = 8}));
// Người chơi: nhân vật vật lý và một robot có animation.
const auto hero = reg.create();
reg.emplace<njin::transform3d>(hero);
reg.emplace<njin::character3d>(hero, njin::character3d_create(ctx, {.position = start}));
reg.emplace<njin::model3d>(hero, njin::model3d{.model = robot, .pose = {.anim = idle, .blend_anim = run}});
// Mỗi frame game chỉ đổi tỉ lệ trộn: reg.get<njin::model3d>(hero).pose.blend = speed / max_speed;

Chọn vật bằng chuột

Hàm Làm gì
camera3d_ray() Tia từ camera qua một điểm trên màn hình, ví dụ mouse_pos()
camera3d_to_screen() Điểm 3D ra màn hình, để đặt nhãn hay thanh máu
ray3d_box(), ray3d_sphere(), ray3d_plane() Tia với hộp, cầu, mặt phẳng
ray3d_shape() Tia với hình SDF, đúng như draw_shape3d() vẽ
ray3d_model() Tia với từng tam giác của model
physics3d_raycast() Tia với mọi body vật lý, trả về body gần nhất bị trúng
const njin::ray3d ray = njin::camera3d_ray(ctx, cam, njin::mouse_pos(ctx));
const njin::ray3d_hit hit = njin::ray3d_shape(ray, player_shape);
if (hit.hit && njin::mouse_pressed(ctx, njin::mouse_left))
select_player();

Trong 2D, dùng scr2w() cho vị trí chuột rồi collision_overlap_point() hoặc collision_raycast() (Va chạm).

Gizmo và inspector

Các hàm gizmo_* (njin_gizmo.h) vẽ hình để debug: đường, mũi tên, hộp, cầu, trục tọa độ, điểm, nhãn chữ, cả 2D lẫn 3D. Gọi được ở mọi phase, vẽ đè lên trên cùng, và giữ lại duration giây nếu muốn thấy vết. gizmos_set_visible() bật tắt tất cả. Xem thêm Công cụ debug: njin_inspector.

Khi njin_inspector đang nối, ô World của nó tự chuyển sang 3D trong lúc game vẽ 3D: mỗi lệnh vẽ (cả từng instance) là một chấm màu, cùng các đèn, hướng nắng, khung nhìn của camera game và gizmo. Kéo chuột phải để xoay, chuột giữa để dời, cuộn để phóng.

Ô World của njin_inspector khi njin_sokoban chạy: sàn, tường, thùng và nhân vật là các chấm, khung xanh là camera của game, vạch vàng là hướng nắng

Game mẫu

Game Xem gì
njin_fps Camera góc nhìn thứ nhất, model glTF (khẩu súng), bóng đổ, sương mù, hai đèn màu và đèn pin (đèn nón, phím F) đều đổ bóng, đèn nòng súng, vệt đạn phát sáng với bloom, tia lửa và nổ bằng hạt 3D, mục tiêu nháy rồi tan, rung camera, hitstop, ray3d_box, gizmo (phím G)
njin_sokoban Camera 2.5D, sàn và tường bằng draw_instanced3d(), thùng có texture, nhân vật là viên nang SDF có viền sáng, đèn điểm trên ô đích, bụi và lấp lánh bằng hạt, hiện dần khi vào màn, gizmo (phím G)
njin_platformer3d Camera góc nhìn thứ ba không xuyên tường (physics3d_raycast), người chơi là entity (njin::character3d và njin::model3d: robot glTF với animation đứng, chạy, nhảy; đứng và chạy trộn theo tốc độ) với coyote time và nhảy đôi, bục tĩnh, bục di chuyển chở người (body kinematic), thùng đẩy được và tấm ván bập bênh là entity (njin::body3d, njin::shape3d_render), bập bênh là bản lề giới hạn ±18°, đồi cỏ là body lưới tam giác từ assets/hill.glb, sáu đồng xu là sensor (chạm là một sự kiện chạm, entity đồng xu bị hủy), đích là sensor, checkpoint, rơi thì tan rồi hiện lại, vòng đích phát sáng, gizmo (phím G)