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
Hiệu ứng màn hình 3D

Trang này thêm cho cảnh 3D những hiệu ứng tính trên ảnh của cả khung hình, từ độ sâu của từng pixel: góc tường và chân vật tối đi (SSAO), sàn bóng phản chiếu (SSR), vết đạn, vết cháy, sơn dán lên mọi bề mặt (decal), nhòe khi camera quay (mờ chuyển động), tia nắng xuyên qua khe, lóa ống kính và khử răng cưa theo thời gian (TAA). Mọi thứ khai báo trong njin_post3d.h và chạy trên OpenGL 3.3 như phần 3D còn lại.

Cần biết trước: Đồ họa 3D (begin_3d(), ánh sáng, vật liệu). Tia nắng đi cùng bầu trời của Thế giới ngoài trời 3D nhưng không cần nó.

SSAO ở góc tường và chân vật, sàn bóng phản chiếu khối cầu, khối hộp và cột, vết decal đỏ sẫm trên tường

Bật hiệu ứng

njin::post3d gom mọi hiệu ứng; mỗi cái tắt khi độ mạnh của nó bằng 0 (mặc định), nên post3d{} vẽ y như khi không có trang này. Đặt bằng post3d_set(), sửa được mỗi frame (tắt mờ chuyển động khi mở menu, tăng tia nắng lúc hoàng hôn).

fx.ssao = 0.8f;
fx.ssr = 1.0f;
fx.shafts = 1.0f;
void post3d_set(context &ctx, const post3d &fx)
Đặt hiệu ứng màn hình 3D, cho mọi lần vẽ 3D từ frame này.
Hiệu ứng màn hình của cảnh 3D, tính từ độ sâu của lần vẽ 3D: che khuất môi trường (SSAO),...
Definition njin_post3d.h:20
f32 ssao
Độ đậm, 0..1. 0 là tắt.
Definition njin_post3d.h:25
f32 shafts
Độ sáng, 0..2. 0 là tắt.
Definition njin_post3d.h:59
f32 ssr
Độ mạnh, 0..1, nhân với reflect của mỗi bề mặt. 0 là tắt.
Definition njin_post3d.h:37
Hiệu ứng Trường chính Áp lên
Che khuất môi trường (SSAO) ssao, ssao_radius Mọi hình đục
Phản chiếu (SSR) ssr, cùng material3d::reflect của bề mặt Bề mặt có reflect > 0
Decal decal3d_add() Mọi hình đục trong hộp của decal
Mờ chuyển động motion_blur Cả ảnh 3D, khi camera di chuyển hay quay; mesh và model đang chạy
Tia nắng shafts Trời quanh mặt trời, qua khe giữa các vật
Lóa ống kính flare Cả ảnh, khi mặt trời trong khung hình và không bị che
Khử răng cưa theo thời gian (TAA) taa, taa_sharpen Lần vẽ 3D đầu tiên vào thế giới của mỗi frame

Hiệu ứng chỉ áp cho lần vẽ 3D vào thế giới, không cho lần vẽ vào render texture (begin_3d() có target). Chúng chạy trong end_3d() theo thứ tự: sau mọi hình đục là decal, SSAO, phản chiếu; rồi kính, nước và hạt 3D; rồi TAA, tia nắng, lóa ống kính và mờ chuyển động. post_fx_set() (Hậu kỳ (post-processing)) và shader riêng của game chạy sau cùng, trên cả ảnh.

Che khuất môi trường (SSAO)

Ánh sáng nền (light3d::ambient) chiếu đều mọi mặt, nên một khối đặt trên sàn trông như lơ lửng. SSAO xét quanh mỗi điểm trên màn hình, trong bán kính ssao_radius đơn vị 3D, xem có bao nhiêu chỗ bị bề mặt khác che, rồi làm tối điểm đó theo ssao: góc tường, khe giữa hai vật, chân vật trên sàn tối đi, mặt trống thì giữ nguyên.

Mặc định tính ở nửa độ phân giải (ssao_half) rồi phóng lên theo độ sâu, nên mép vật vẫn sắc: rẻ gấp bốn và gần như không khác bản đầy đủ. ssao_samples là số mẫu mỗi pixel; ít thì nhanh, nhiều thì mịn.

Phản chiếu (SSR)

Bề mặt phản chiếu là bề mặt có material3d::reflect lớn hơn 0: đặt bằng material3d_set() cho hình khối, hoặc model_material::surface.reflect cho model. 1 là gương ở mọi góc nhìn; nhỏ hơn thì phản chiếu rõ khi nhìn xiên, mờ khi nhìn thẳng xuống, như sàn bóng hay mặt nước. post3d::ssr nhân với nó cho cả cảnh.

marble.reflect = 0.5f;
njin::material3d_set(ctx, marble);
njin::draw_plane3d(ctx, {0, 0, 0}, {20, 20}, {0.9f, 0.9f, 0.92f, 1});
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 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.
Bề mặt của các hình 3D vẽ sau material3d_set().
Definition njin_3d.h:143
f32 reflect
Độ phản chiếu, 0..1, cho phản chiếu trên màn hình (post3d::ssr): 1 là gương ở mọi góc nhìn,...
Definition njin_3d.h:186

Mỗi pixel phản chiếu dò tia phản xạ của nó trên ảnh độ sâu, tới ssr_distance đơn vị, và lấy màu chỗ tia gặp một bề mặt. Vì chỉ có những gì đang thấy trên màn hình, thứ ngoài khung hình hay bị vật khác che không phản chiếu được: chỗ đó mờ dần về màu sương light3d::fog_color (với draw_sky3d() là màu chân trời), theo ssr_sky. material3d::reflect một mình không làm gì: không bật post3d::ssr thì cảnh vẽ như cũ.

Decal

Decal là một ảnh chiếu lên mọi hình đục nằm trong một hình hộp: tường, sàn, model, địa hình, theo đúng hình của bề mặt (một vết sơn trên quả cầu cong theo quả cầu). decal3d_add() thêm một decal; nó có ở mọi lần vẽ 3D vào thế giới cho tới khi hết lifetime (thời gian của game, dừng khi pause; fade giây cuối mờ dần), bị decal3d_remove() xóa, hay bị decal mới thay chỗ khi quá decal3d_set_max() (mặc định 256, cái cũ nhất đi).

Ảnh nằm trên mặt xz của hộp và chiếu theo trục y: không xoay là chiếu từ trên xuống sàn. size.y là độ sâu mà decal phủ tới: mỏng thì không lan sang vật bên cạnh. Để dán theo một tia chạm (vết đạn chỗ trúng), decal3d_rotation() cho góc xoay từ pháp tuyến của bề mặt. Không có ảnh thì decal là một vết tròn mềm mép.

const njin::ray3d_hit hit = njin::physics3d_raycast(ctx, shot, 100.0f);
if (hit.hit)
njin::decal3d_add(ctx, {.position = hit.point,
.rotation = njin::decal3d_rotation(hit.normal),
.size = {0.25f, 0.2f, 0.25f},
.texture = bullet_hole,
.lifetime = 30.0f});
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.
decal3d_handle decal3d_add(context &ctx, const decal3d_desc &desc)
Thêm một decal.
vec3 decal3d_rotation(vec3 normal)
Góc xoay cho decal3d_desc::rotation để trục chiếu của decal trùng với pháp tuyến của bề mặt: decal nằ...
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
vec3 normal
Pháp tuyến của bề mặt tại điểm chạm.
Definition njin_3d.h:851
bool hit
Tia có chạm không.
Definition njin_3d.h:848

Hai cách phủ (njin::decal3d_blend): decal3d_multiply (mặc định) nhân màu vào bề mặt, chỉ làm tối nhưng giữ nguyên ánh sáng và bóng đổ của bề mặt, hợp cho vết đạn, vết cháy, máu, bùn; decal3d_paint phủ màu lên như sơn, chiếu sáng theo nắng và ánh sáng nền, cho ký hiệu sáng màu trên nền tối. Bề mặt nghiêng nhiều so với trục chiếu mờ đi (angle_fade), nên vết không bị kéo dài trên mặt bên của vật.

Mờ chuyển động

Khi camera di chuyển hay quay, hay một vật tự chạy, mỗi pixel trượt trên màn hình giữa hai frame; motion_blur nhòe ảnh theo hướng đó, 1 là cả quãng trượt của một frame, 0.5 như màn trập máy quay phim. Vật đang chạy trước camera đứng yên cũng nhòe, và vệt nhòe lan ra nền sát mép nó, như ảnh chụp thật (xem Vận tốc của từng vật). Camera đứng yên và vật đứng yên thì ảnh giữ nguyên từng pixel. Vệt nhòe dài nhất là 6% chiều rộng màn hình, nên camera nhảy chỗ (đổi cảnh) không làm ảnh nhòe nát.

Tia nắng và lóa ống kính

Mặt trời là hướng ngược với light3d::direction; draw_sky3d() đặt hướng đó theo giờ trong ngày, nên tia nắng đi theo mặt trời trên trời. Trời là chỗ không có hình 3D nào (độ sâu xa nhất).

shafts làm trời quanh mặt trời tỏa thành tia: mỗi pixel gom ánh trời nằm giữa nó và mặt trời, nên vật che thành bóng tối xẻ tia, khe giữa các vật thành tia sáng. shafts_length là tia dài bao nhiêu phần đường tới mặt trời, shafts_color nhân với màu nắng. flare thêm quầng, vòng sáng và các đốm sáng dọc đường từ mặt trời qua tâm màn hình. Cả hai mờ dần khi mặt trời ra khỏi khung hình (hết hẳn khi ra ngoài một phần tư màn hình), không có khi mặt trời ở sau lưng camera hay đã lặn; lóa ống kính còn mờ theo phần đĩa mặt trời bị vật che.

Mặt trời chiều sau cột giữa: tia nắng tỏa qua hai khe, sàn phản chiếu các cột

Khử răng cưa theo thời gian (TAA)

taa = true làm mép xiên của hình 3D mịn như vẽ ở độ phân giải gấp đôi, và mép mảnh (dây, cột xa) thôi nhấp nháy. Mỗi frame, hình chiếu 3D dịch đi một phần nhỏ của pixel, mỗi frame một chỗ (dãy Halton 2, 3, tám vị trí), nên cùng một pixel lần lượt thấy mép vật ở những điểm khác nhau. Ảnh của frame được trộn với ảnh đã trộn của các frame trước (khoảng 10% ảnh mới), sau khi đưa ảnh cũ về đúng chỗ theo độ sâu và chuyển động của camera. Sau khi trộn, ảnh được làm nét lại một chút (taa_sharpen, mặc định 0.25), vì trộn nhiều điểm làm ảnh hơi mềm.

fx.taa = true;
bool taa
Bật TAA.
Definition njin_post3d.h:83

Ảnh cũ chỉ được dùng khi còn khớp, nên không để lại bóng mờ:

  • Mỗi pixel so màu ảnh cũ với màu chín pixel quanh nó trong ảnh mới; màu cũ nằm ngoài khoảng đó thì bị kéo về.
  • Độ sâu của ảnh cũ, quanh chỗ đọc, phải chứa điểm đang vẽ; không thì ảnh cũ ở đó cho thấy vật khác (một vật đã chạy đi, hay chỗ vừa lộ ra sau vật), nên bị bỏ.
  • Camera nhảy chỗ (đi xa hơn 3 đơn vị hay quay hơn khoảng 25 độ trong một frame), đổi cỡ ảnh, hay tắt TAA rồi bật lại, thì ảnh cũ bị bỏ hẳn: frame đầu sau đó là ảnh mới chưa khử.

Chỉ lần vẽ 3D đầu tiên vào thế giới của mỗi frame được khử; 2D vẽ sau end_3d() vẽ lên ảnh đã khử nên không rung. Vật đang tự chạy (mesh và model, xem mục dưới) được đưa về chỗ cũ theo chuyển động của chính nó, nên mép của nó cũng được khử; ảnh cũ ở chỗ vừa lộ ra sau vật bị bỏ, nên không để lại vệt.

Vận tốc của từng vật

Khi TAA hay mờ chuyển động bật, end_3d() vẽ lại các mesh và model đục một lần nữa (chỉ ghi chuyển động, không tô màu) để biết mỗi pixel đã ở đâu frame trước: theo transform của lần vẽ đó frame trước và bây giờ, model có xương thì theo cả tư thế frame trước. Lần vẽ này chỉ chạy một lần mỗi frame, cho lần vẽ 3D đầu tiên vào thế giới.

Engine tự nhận ra lần vẽ nào frame này là lần vẽ nào frame trước:

  • Entity có njin::model3d: theo entity.
  • Các lần vẽ khác (draw_model(), draw_model_anim(), draw_cube3d()...): theo model hay hình, và thứ tự trong pass. Cảnh vẽ theo cùng thứ tự mỗi frame thì tự khớp.
  • Thứ tự đổi giữa các frame (danh sách quái bị xóa bớt, sắp lại theo khoảng cách): gọi draw3d_motion_id() ngay trước lần vẽ, với một số riêng không đổi của vật, ví dụ số hiệu của nó.
for (const enemy &e : enemies) {
njin::draw3d_motion_id(ctx, e.id); // khác 0, không đổi theo frame
njin::draw_model_anim(ctx, enemy_model, e.at, e.pose);
}
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.
void draw3d_motion_id(const context &ctx, u64 id)
Đặt tên cho lần vẽ 3D kế tiếp, để TAA và mờ chuyển động biết nó ở đâu frame trước.

Không có tên thì hai vật cùng model đổi chỗ trong danh sách sẽ bị coi như nhảy sang chỗ của nhau: TAA bỏ ảnh cũ ở đó, mờ chuyển động nhòe chúng dù đứng yên. Một vật nhảy xa hơn một phần tư màn hình trong một frame (dịch chuyển tức thời, vật mới xuất hiện ở chỗ của vật cũ) được coi như chỉ có chuyển động của camera.

Hình SDF (draw_shape3d()), draw_instanced3d(), địa hình, cỏ, nước, kính và hạt 3D không có chuyển động riêng: chúng theo chuyển động của camera, như trước.

Ví dụ đầy đủ

Một góc phòng có sàn bóng và một hộp chạy vòng quanh quả cầu; phím 1, 2, 3, 4 bật tắt SSAO, phản chiếu, mờ chuyển động và TAA; chuột trái để lại vết đạn trên sàn hay tường, mờ dần sau 10 giây.

#include <njin.h>
#include <cmath>
namespace {
using namespace njin;
const camera3d camera{.position = {5.5f, 3.5f, 6.5f}, .target = {-0.5f, 0.8f, -0.5f}, .fovy = 50.0f};
// Tường của phòng, để tìm chỗ chuột trỏ tới: tâm và cỡ.
struct box {
vec3 center, size;
};
const box walls[] = {{{-3.0f, 1.5f, 0.0f}, {0.3f, 3.0f, 8.0f}}, {{0.0f, 1.5f, -3.0f}, {8.0f, 3.0f, 0.3f}}};
void load(context &ctx) {
// Bật từng hiệu ứng; mỗi cái tắt ở giá trị 0.
fx.ssao = 0.8f; // góc tường, chân vật tối đi
fx.ssr = 1.0f; // sàn bóng (material3d::reflect bên dưới) phản chiếu
fx.motion_blur = 0.5f; // nhòe khi camera quay hay vật chạy
post3d_set(ctx, fx);
}
void update(context &ctx) {
// Phím 1, 2, 3, 4 bật tắt SSAO, phản chiếu, mờ chuyển động, TAA.
if (key_pressed(ctx, key_1))
fx.ssao = fx.ssao > 0.0f ? 0.0f : 0.8f;
if (key_pressed(ctx, key_2))
fx.ssr = fx.ssr > 0.0f ? 0.0f : 1.0f;
if (key_pressed(ctx, key_3))
fx.motion_blur = fx.motion_blur > 0.0f ? 0.0f : 0.5f;
if (key_pressed(ctx, key_4))
fx.taa = !fx.taa;
post3d_set(ctx, fx);
// Chuột trái: một vết đạn chỗ chuột trỏ tới (sàn hay tường), mờ dần sau 10 giây.
const ray3d ray = camera3d_ray(ctx, camera, mouse_pos(ctx));
ray3d_hit hit = ray3d_plane(ray, {0.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f});
for (const box &w : walls) {
const ray3d_hit h = ray3d_box(ray, w.center, w.size);
if (h.hit && (!hit.hit || h.distance < hit.distance))
hit = h;
}
if (hit.hit)
decal3d_add(ctx, {.position = hit.point,
.rotation = decal3d_rotation(hit.normal),
.size = {0.3f, 0.2f, 0.3f},
.color = {0.05f, 0.05f, 0.05f, 0.9f},
.lifetime = 10.0f,
.fade = 2.0f});
}
}
void render(context &ctx) {
light3d_set(ctx, {.direction = {-0.45f, -1.0f, -0.3f}, .shadows = true, .shadow_range = 15.0f});
begin_3d(ctx, camera);
// Sàn bóng: phản chiếu khi post3d::ssr bật.
material3d floor{};
floor.reflect = 0.6f;
material3d_set(ctx, floor);
draw_plane3d(ctx, {0.0f, 0.0f, 0.0f}, {20.0f, 20.0f}, {0.7f, 0.7f, 0.72f, 1.0f});
material3d_set(ctx, {});
for (const box &w : walls)
draw_cube3d(ctx, w.center, w.size, {0.85f, 0.8f, 0.7f, 1.0f});
draw_sphere3d(ctx, {1.0f, 1.0f, 0.0f}, 1.0f, {0.85f, 0.2f, 0.15f, 1.0f});
// Một hộp chạy vòng quanh quả cầu: mờ chuyển động nhòe nó, TAA khử cả mép nó.
// Tên riêng (draw3d_motion_id) giữ nó khớp với chính nó frame trước, dù thứ tự vẽ có đổi.
const f32 t = elapsed(ctx);
draw_cube3d(ctx, {1.0f + 2.0f * std::cos(t), 0.3f, 2.0f * std::sin(t)}, {0.5f, 0.5f, 0.5f},
{0.2f, 0.6f, 0.9f, 1.0f});
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 screen_fx_module() { return {.name = "screen_fx", .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.
ray3d_hit ray3d_box(const ray3d &ray, vec3 center, vec3 size)
Tia với hộp có các cạnh song song với trục.
ray3d_hit ray3d_plane(const ray3d &ray, vec3 point, vec3 normal)
Tia với mặt phẳng vô hạn qua point, pháp tuyến normal.
void draw_sphere3d(const context &ctx, vec3 center, f32 radius, rgba color)
Vẽ hình cầu đặc.
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.
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_1
Phím 1 (hàng số).
Definition _types.h:373
@ key_4
Phím 4 (hàng số).
Definition _types.h:376
@ key_3
Phím 3 (hàng số).
Definition _types.h:375
@ key_2
Phím 2 (hàng số).
Definition _types.h:374
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.
float f32
Số thực 32 bit.
Definition _types.h:28
Các mẫu particle hay dùng, để truyền vào particles_spawn() hoặc gắn thẳng lên entity.
Definition njin_fx.h:147
Camera phối cảnh cho begin_3d().
Definition njin_3d.h:15
Mô tả một module: một nhóm system có tên.
Definition _mod.h:67
f32 distance
Khoảng cách từ gốc tia tới điểm chạm.
Definition njin_3d.h:849
Một tia trong thế giới 3D, dùng để chọn vật bằng chuột, bắn đạn, kiểm tra tầm nhìn.
Definition njin_3d.h:841
Vector 3 chiều.
Definition _types.h:47

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

Đo trên RTX 3050 Laptop, bản Release, 1280 x 720, cảnh của ảnh trên (trung bình ba lần đo): không hiệu ứng 0,64 ms mỗi frame; SSAO nửa độ phân giải thêm khoảng 0,24 ms (đầy đủ 0,54 ms), phản chiếu 0,41 ms, 50 decal 0,28 ms, mờ chuyển động 0,15 ms, tia nắng 0,25 ms, lóa ống kính 0,14 ms; tất cả cùng 50 decal thêm khoảng 1 ms. TAA thêm khoảng 0,3 đến 0,4 ms (kể cả việc vẽ thế giới vào ảnh riêng và vận tốc của từng vật). Với bốn vật đang chạy và một nhân vật có xương, TAA thêm khoảng 0,35 ms và mờ chuyển động khoảng 0,25 ms. Bật bất kỳ hiệu ứng nào (hay có một decal) thì thế giới được vẽ vào ảnh riêng có độ sâu rồi chép ra màn hình, như khi bật post_fx_set().

  • Chỉ có những gì trên màn hình: phản chiếu không thấy thứ ngoài khung hình hay bị che, SSAO không biết thứ sau một vật. Mép màn hình là chỗ hai hiệu ứng này yếu nhất.
  • Hình SDF (draw_shape3d()), địa hình, nước và cỏ không phản chiếu; chúng vẫn hiện trong phản chiếu của bề mặt khác, và vẫn nhận SSAO và decal.
  • Kính, nước và hạt 3D vẽ sau decal, SSAO và phản chiếu nên không có chúng; mờ chuyển động của chúng theo độ sâu của hình đục phía sau.
  • Pháp tuyến tính từ độ sâu, nên đúng đường giao giữa hai mặt (sàn gặp tường) phản chiếu có thể lóe một pixel.
  • Hình SDF, draw_instanced3d(), địa hình, cỏ và nước không có chuyển động riêng (chỉ theo camera): khi chúng tự chạy, mép của chúng còn răng cưa dưới TAA và không nhòe. Bóng của vật đang chạy nằm trên sàn đứng yên, nên mép bóng dưới TAA hơi mềm. Hình dạng thay đổi do morph cũng không có chuyển động riêng.
  • Dưới TAA, mép vật đang chạy nhanh hơi mềm hơn ảnh không khử (ảnh mới được trộn nhiều hơn để không nhòe theo).
  • Chỉ lần vẽ 3D đầu tiên vào thế giới của mỗi frame được khử; 2D vẽ vào ảnh thế giới trước begin_3d() được trộn cùng ảnh 3D, không rung nhưng có thể hơi mềm khi camera quay.
  • Mỗi lần vẽ 3D vào thế giới đều có hiệu ứng: game vẽ thế giới bằng hai begin_3d() trong một frame thì decal được vẽ ở cả hai lần.