1
Một viên gạch, vẽ hàng nghìn lần
Mọi viên gạch dùng chung một lưới (một khối hộp cộng bốn núm 12 cạnh = 462 đỉnh), chỉ tải lên GPU một lần. Mỗi viên chỉ thêm 8 con số: vị trí, tầng, góc xoay và màu. GPU instancing đọc một bộ số cho mỗi viên, nên cả cánh đồng chỉ là một lệnh vẽ duy nhất.
brick_count = grid_size × grid_size (wave) e.g. 24 × 24 = 576
per_brick_data = (brick_x, brick_z, layer, brick_yaw) + (red, green, blue, rand)
vertex_attrib_divisor(attr, 1) → advance once per brick
draw_arrays_instanced(triangles, 0, 462, brick_count) → draw_calls = 1
Nguồn: [1]
[2]
2
Đặt từng viên gạch trên GPU
Vertex shader xoay từng viên gạch quanh trục đứng, nâng nó lên tầng của nó, cộng thêm độ cao của hoạt ảnh, rồi chiếu qua camera. Phép chiếu dùng ma trận phối cảnh tiêu chuẩn.
x_rot = x·cos(brick_yaw) − z·sin(brick_yaw)
z_rot = x·sin(brick_yaw) + z·cos(brick_yaw)
world_pos = (x_rot, y, z_rot) + (brick_x, 1.15·layer + anim_height, brick_z)
clip_pos = proj_matrix · view_matrix · world_pos
focal = 1 / tan(field_of_view / 2)
field_of_view = 0.75 rad near = 0.5 far = 600
proj_matrix = | focal/aspect 0 0 0 |
| 0 focal 0 0 |
| 0 0 (near+far)/(near−far) 2·near·far/(near−far) |
| 0 0 −1 0 |
Nguồn: [3]
3
Con sóng và chỗ nhô theo con trỏ
Độ cao của sóng là hàm sin theo khoảng cách từ mỗi viên gạch tới tâm, nên các vòng gợn lan ra ngoài. Con trỏ của bạn thêm một chỗ nhô dạng Gauss, đo trong không gian màn hình để bám theo con trỏ ở mọi góc camera. Khi bật giảm chuyển động, cả hai đều nhỏ lại.
dist_center = √(x² + z²)
wave_height = 2.8 · (½ + ½·sin(0.3·dist_center − 1.8·time)) · amplitude
pointer_dist = |brick_screen_pos − pointer_pos| (aspect_corrected)
bump_height = 4 · e^(−14·pointer_dist²) · amplitude
anim_height = wave_height + bump_height
amplitude = 1 (0.35 with prefers-reduced-motion)
Nguồn: [4]
4
Rơi xuống và nảy lên
Ở Kim tự tháp và Xoắn ốc, mỗi viên gạch bắt đầu sau một độ trễ ngẫu nhiên nhỏ, cộng thêm một chút theo tầng, nên công trình đổ xuống từ dưới lên. Gạch rơi theo đường parabol, rồi nảy theo một hàm sin tắt dần.
drop_time = 1.4·(time − drop_start) − 1.4·rand − 0.16·layer rand ∈ [0, 1) per brick
fall = clamp(1 − drop_time, 0, 1)
bounce_time = max(drop_time − 1, 0)
anim_height = 36·fall² + 0.9·|sin(9·bounce_time)|·e^(−6·bounce_time)
drop_time < 0 → brick not drawn yet
Nguồn: [4]
5
Ánh sáng, độ bóng và sương mờ
Ánh sáng được tính theo từng điểm ảnh: khuếch tán Lambert cho nhựa mờ, một thành phần bán cầu để mặt trên sáng hơn mặt bên, điểm sáng Blinn–Phong cho độ bóng của đồ chơi, và sương mờ theo khoảng cách hòa về màu trời.
color = albedo · (0.34 + 0.24·(½ + ½·normal_y) + 0.6·max(normal·light_dir, 0))
+ 0.22 · max(normal·half_vec, 0)^36
light_dir = normalize(0.45, 0.85, 0.32)
half_vec = normalize(light_dir + (0, 0.5, 0.85)) (fixed view direction)
fog_amount = clamp((clip_w − fog_start) / (1.4·fog_start), 0, 1)
fog_start = 0.95 · camera_dist
final_color = mix(color, sky_color, 0.8 · fog_amount)
Nguồn: [5]
[6]
6
Camera và vòng lặp khung hình
Camera quay quanh cảnh trên một mặt cầu, tự xoay chậm cho đến khi bạn kéo. Mỗi khung hình được lên lịch bằng requestAnimationFrame, và một IntersectionObserver tạm dừng vòng lặp khi bản demo không còn trên màn hình.
eye_pos = target + camera_dist · (sin(cam_yaw)·cos(cam_pitch), sin(cam_pitch), cos(cam_yaw)·cos(cam_pitch))
cam_yaw += 0.07 rad/s (auto_orbit)
cam_pitch ∈ [0.15, 1.35] rad
fps = frame_count / elapsed_sec (updated every 0.5 s)
Nguồn: [3]
[7]