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
Chỉ vẽ phần camera thấy: instance theo ô

draw_instanced3d() vẽ cả một khúc bộ đệm bằng một lệnh, nhưng card đồ họa vẫn xử lý từng instance trong khúc đó, kể cả những cái nằm sau lưng camera. Với một khu rừng hay một thành phố hàng chục nghìn vật mà camera chỉ thấy một góc, phần lớn công sức ấy bỏ phí. njin_spatial_batch.h giải quyết bằng cách chia mặt đất thành ô: xếp instance trong bộ đệm theo ô một lần, rồi mỗi frame chọn những ô cần vẽ và nhận lại các khúc bộ đệm tương ứng. Cũng cách đó chọn được mức chi tiết: ô gần vẽ hình mịn, ô xa vẽ hình ít mặt.

Cần biết trước: Đồ họa 3D, phần Instancing. Cách này dùng được cả với draw_instanced() của 2D, vì nó cũng nhận instance đầu và số instance.

Ví dụ: 40.000 cây

#include <njin.h>
#include <algorithm>
#include <cmath>
#include <vector>
namespace {
using namespace njin;
// 40.000 cây trên cánh đồng 2000 x 2000, lưới 50 x 50 ô (40 đơn vị một ô).
// Mặt phẳng của lưới là x, z của thế giới 3D.
constexpr i32 tree_count = 40000;
constexpr i32 floats = 16; // vị trí, màu, góc xoay, tỉ lệ x y z
const batch_grid2d grid{.origin = {-1000.0f, -1000.0f}, .cell_size = {40.0f, 40.0f}, .cols = 50, .rows = 50};
std::vector<u32> offsets; // offsets[c]: cây đầu tiên của ô c trong bộ đệm
std::vector<u8> near_cells, far_cells; // mỗi frame: ô vẽ hình mịn, ô vẽ hình ít mặt
void load(context &ctx) {
rng &r = random(ctx);
std::vector<f32> unsorted(tree_count * floats, 0.0f);
std::vector<i32> cell(tree_count);
offsets.assign(grid.count() + 1, 0);
for (i32 i = 0; i < tree_count; i++) {
f32 *t = &unsorted[i * floats];
t[0] = r.range(-1000.0f, 1000.0f); // x
t[2] = r.range(-1000.0f, 1000.0f); // z
t[5] = r.range(0.35f, 0.6f); // màu: xanh lá
t[4] = t[6] = 0.2f;
t[7] = 1.0f;
t[12] = t[14] = 0.5f; // thân bán kính 0.5
t[13] = r.range(4.0f, 8.0f); // cao 4 đến 8
cell[i] = grid.cell_at({t[0], t[2]});
offsets[cell[i] + 1]++;
}
// Xếp cây theo ô: số cây mỗi ô cộng dồn thành offsets, rồi chép từng cây vào phần của ô nó.
for (i32 c = 0; c < grid.count(); c++)
offsets[c + 1] += offsets[c];
std::vector<u32> next(offsets.begin(), offsets.end() - 1);
std::vector<f32> sorted(unsorted.size());
for (i32 i = 0; i < tree_count; i++)
std::copy_n(&unsorted[i * floats], floats, &sorted[next[cell[i]]++ * floats]);
trees = instance_buffer_create(ctx, floats);
instance_buffer_upload(ctx, trees, sorted.data(), tree_count);
near_cells.resize(grid.count());
far_cells.resize(grid.count());
}
void render(context &ctx) {
// Camera bay vòng quanh cánh đồng, nhìn chếch xuống.
const f32 a = elapsed(ctx) * 0.1f;
const vec3 target{std::cos(a) * 500.0f, 0.0f, std::sin(a) * 500.0f};
const camera3d camera{.position = {target.x - 120.0f, 80.0f, target.z - 120.0f}, .target = target};
// Phần mặt đất camera này thấy: một hình chữ nhật đủ rộng để bao trọn nó.
const batch_view2d view{.lo = {target.x - 400.0f, target.z - 400.0f},
.hi = {target.x + 400.0f, target.z + 400.0f},
.focus = {camera.position.x, camera.position.z},
.overhang = 0.5f}; // thân cây nhô khỏi ô xa nhất bằng bán kính của nó
for (i32 c = 0; c < grid.count(); c++) {
const bool seen = batch_cell_visible(grid, view, c);
const bool close = batch_cell_detailed(grid, view, c, 150.0f);
near_cells[c] = seen && close;
far_cells[c] = seen && !close;
}
begin_3d(ctx, camera);
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {2000.0f, 2000.0f}, {0.45f, 0.5f, 0.3f, 1.0f});
for (const auto &[from, to] : batch_instance_ranges(offsets, near_cells))
draw_instanced3d(ctx, mesh3d_cylinder, trees, from, to - from);
for (const auto &[from, to] : batch_instance_ranges(offsets, far_cells))
draw_instanced3d(ctx, mesh3d_cylinder_low, trees, from, to - from);
end_3d(ctx);
}
void setup(context &ctx) {
ecs_register(ctx, phase_startup, load, "load");
ecs_register(ctx, phase_render, render, "render");
}
} // namespace
mod_desc spatial_batch_module() { return {.name = "spatial_batch", .setup = setup}; }
void begin_3d(context &ctx, const camera3d &camera)
Bắt đầu vẽ 3D qua camera.
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.
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_cylinder
Trụ bán kính 1, đáy ở gốc, cao 1 theo +y.
Definition njin_3d.h:927
@ mesh3d_cylinder_low
Như mesh3d_cylinder nhưng 8 cạnh thay vì 48, cùng lý do.
Definition njin_3d.h:933
bool batch_cell_detailed(const batch_grid2d &grid, const batch_view2d &view, i32 cell, f32 radius)
Ô cell có ở gần không: khoảng cách từ view.focus đến hình chữ nhật của ô nhỏ hơn radius.
std::vector< instance_range > batch_instance_ranges(std::span< const u32 > offsets, std::span< const u8 > wanted, std::span< const instance_range > excluded={})
Các đoạn instance cần vẽ, từ những ô được chọn.
bool batch_cell_visible(const batch_grid2d &grid, const batch_view2d &view, i32 cell)
Ô cell có lọt vào vùng nhìn không: hình chữ nhật của ô, nới thêm overhang, có chạm hình chữ nhật lo .
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_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
rng & random(context &ctx)
Bộ sinh số ngẫu nhiên dùng chung của engine.
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
Lưới chia mặt đất thành ô, để nhóm instance theo ô chứa tâm của chúng.
Definition njin_spatial_batch.h:17
i32 count() const
Số ô, cols * rows.
i32 cell_at(vec2 p) const
Ô chứa điểm p: hàng * cols + cột.
Phần mặt phẳng một camera thấy, do game dựng từ camera của mình mỗi frame.
Definition njin_spatial_batch.h:37
Camera phối cảnh cho begin_3d().
Definition njin_3d.h:15
vec3 position
Vị trí mắt.
Definition njin_3d.h:16
Vị trí một ô trong lưới. Có thể âm.
Definition _tilemap.h:43
Đị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
Bộ sinh số ngẫu nhiên PCG32: nhanh, chất lượng tốt, và lặp lại được: cùng hạt giống thì cùng dãy số.
Definition _random.h:13
f32 range(f32 lo, f32 hi)
Số thực trong [lo, hi).
Definition _random.h:50
Vector 3 chiều.
Definition _types.h:47
f32 x
Thành phần thứ nhất.
Definition _types.h:48
f32 z
Thành phần thứ ba.
Definition _types.h:50

Ba bước

Bước Khi nào Làm gì
1. Lưới Một lần njin::batch_grid2d: góc origin, kích thước ô cell_size, số cột cols và hàng rows
2. Xếp bộ đệm Khi nạp, hoặc khi vật thêm bớt Mỗi instance thuộc ô chứa tâm nó (grid.cell_at()). Ghi bộ đệm theo ô: mọi instance của ô 0, rồi ô 1... offsets[c] là instance đầu của ô c, phần tử cuối là tổng số instance
3. Chọn ô Mỗi frame, mỗi camera Một mảng wanted, mỗi ô một số (khác 0 là vẽ), rồi batch_instance_ranges() trả về các khúc [first, second) cho draw_instanced3d()

Ô liền nhau được chọn thì gộp thành một khúc, nên một vùng nhìn thấy thường chỉ tốn vài lệnh vẽ. Ô rỗng không sao.

Kích thước ô là sự đánh đổi: ô nhỏ thì vẽ sát vùng nhìn hơn nhưng nhiều khúc hơn (nhiều lệnh vẽ); ô to thì ngược lại. Bắt đầu với ô cỡ vài lần vật lớn nhất, rồi đo.

Vùng nhìn

njin::batch_view2d mô tả phần mặt đất một camera thấy, trên cùng mặt phẳng với lưới: x, z của thế giới với cảnh 3D, x, y với cảnh 2D. Mọi số dùng chung đơn vị của game.

Trường Nghĩa
lo, hi Hình chữ nhật nhìn thấy. Phải bao trọn những gì camera thấy: rộng quá chỉ vẽ thừa, hẹp quá thì vật ở mép màn hình biến mất
overhang Vật nhô khỏi ô chứa tâm nó xa nhất bao nhiêu (bán kính lớn nhất của instance)
focus Điểm đo khoảng cách cho mức chi tiết, thường là vị trí camera

batch_cell_visible() cho biết ô có chạm vùng nhìn không. batch_cell_detailed() cho biết ô có cách focus dưới một bán kính không, và không xét vùng nhìn: kết hợp hai hàm như ví dụ để có hai mức chi tiết, mỗi mức một mảng wanted.

Đây là phép lọc thô theo hình chữ nhật, không phải kiểm tra frustum 3D hay che khuất: vật trong hình chữ nhật mà nằm ngoài khung hình vẫn được vẽ. Mỗi camera (màn hình chính, minimap, gương) giữ một vùng nhìn riêng và dùng chung bộ đệm.

Bỏ bớt một khúc

Tham số thứ ba của batch_instance_ranges() là các khúc không vẽ, ví dụ một ngôi nhà đang được vẽ riêng ở dạng cắt mở:

const njin::instance_range excluded[] = {{house_first, house_first + house_count}};
for (const auto &[from, to] : njin::batch_instance_ranges(offsets, wanted, excluded))
njin::draw_instanced3d(ctx, house_model, buffer, from, to - from);

Các khúc bỏ có thể chồng nhau và không cần sắp xếp.

Những gì nó không làm

  • Không giữ hay sửa bộ đệm: game tự xếp instance, tự gọi instance_buffer_upload() và giữ offsets khớp với bộ đệm.
  • Không chọn hình cho từng mức chi tiết: game quyết định vẽ gì cho mỗi mảng wanted.
  • Dữ liệu sai thì trả về rỗng thay vì đoán: offsets không có đúng wanted.size() + 1 phần tử hay có chỗ giảm.

Các hàm chỉ đọc dữ liệu vào và không giữ gì giữa các lần gọi, nên gọi được cho nhiều camera, và trên thread khác miễn là không ai sửa dữ liệu vào cùng lúc. Giữ mảng wanted qua các frame để khỏi cấp phát lại.