Chuyển đến nội dung chính

Tech lab

Nơi chúng tôi thử xem trình duyệt thật sự làm được gì. Mọi thứ ở đây chạy trực tiếp trên thiết bị của bạn, viết tay hoàn toàn: WebGL thuần, công cụ vật lý tự xây dựng và hai trò chơi nhỏ. Không plugin, không game engine, không thư viện nào.

  • WebGL + GLSL
  • GPU instancing
  • Vật lý tự viết
  • Canvas 2D
  • Chuột + cảm ứng + bàn phím
  • 0 thư viện phụ thuộc
Chọn một bản demo
01 · WebGL

Hàng nghìn viên gạch, một lệnh vẽ

Shader GLSL viết tay và GPU instancing tạo chuyển động cho từng viên gạch ngay trên card đồ họa. Kéo để xoay, di chuyển con trỏ để đẩy làn sóng.

BẢNG ĐIỀU KHIỂN TRỰC TIẾP
Cảnh
Xây lạiCho mọi viên gạch rơi lại từ trên trời.
—FPS
—Viên gạch
—Tam giác
1Lệnh vẽ
Cách hoạt động Toán học đằng sau cánh đồng gạch

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]

Đặ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]

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]

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]

Á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]

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]
Nguồn
  1. Khronos Group. Đặc tả phần mở rộng WebGL “ANGLE_instanced_arrays”. registry.khronos.org/webgl/extensions/ANGLE_instanced_arrays/
  2. WebGL Fundamentals. “WebGL Optimization – Instanced Drawing.” webglfundamentals.org/webgl/lessons/webgl-instanced-drawing.html
  3. MDN Web Docs. “WebGL model view projection.” developer.mozilla.org/en-US/docs/Web/API/WebGL_API/WebGL_model_view_projection
  4. Thiết kế riêng của chúng tôi cho bản demo này (sóng, chỗ nhô Gauss, đường cong rơi và nảy); không có nguồn bên ngoài.
  5. Định luật cosin Lambert (phản xạ khuếch tán). Wikipedia. en.wikipedia.org/wiki/Lambert%27s_cosine_law
  6. Blinn, J. F. (1977). “Models of light reflection for computer synthesized pictures.” SIGGRAPH ’77, tr. 192–198. www.microsoft.com/en-us/research/wp-content/uploads/1977/01/p192-blinn.pdf
  7. MDN Web Docs. “Intersection Observer API.” developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
02 · Công cụ vật lý

Những viên gạch có trọng lượng thật

Công cụ nhỏ do chúng tôi tự viết: trọng lực, khối lượng, ma sát, độ nảy và xếp chồng, được tính 1.200 lần mỗi giây. Chạm vào chỗ trống để thả gạch, nắm bất cứ thứ gì và ném đi.

Trọng lực
0Vật thể
0Tiếp xúc / bước
0 msThời gian tính
Cách hoạt động Toán học đằng sau sân chơi vật lý

Đồng hồ cố định cho vật lý

Vật lý chạy theo từng bước cố định 1/120 giây, bất kể tần số khung hình của màn hình. Mỗi khung hình cộng thời gian thực vào một bộ tích lũy rồi tiêu nó theo các bước trọn vẹn (tối đa 6), nên gạch hoạt động giống nhau ở 30 hay 144 fps.

accumulator += frame_dt
while accumulator ≥ step_dt and steps < 6:
    step(step_dt)            step_dt = 1/120 s
    accumulator −= step_dt
Nguồn: [1]

Di chuyển từng vật thể

Mỗi bước cộng trọng lực vào vận tốc trước, rồi di chuyển vật theo vận tốc mới (Euler bán ẩn, ổn định khi xếp chồng). Viên gạch đang được nắm sẽ bám theo con trỏ như một lò xo cứng và hoạt động như thể có khối lượng vô hạn.

vel += gravity · step_dt     gravity = 1900 (Earth) · 320 (Moon) · 0 (Zero-G) px/s²
pos += vel · step_dt
zero_g:  vel *= 0.999 each step
|vel| ≤ 2600 px/s
grabbed: vel = 16 · (pointer_pos − pos),  inv_mass = 0
Nguồn: [2]

Tìm điểm tiếp xúc

Gạch là các hộp thẳng hàng với trục, nên hai viên chạm nhau khi chồng lấn trên cả hai trục; chúng được đẩy ra theo trục có phần chồng lấn nhỏ hơn. Quả bóng phá dỡ được kiểm tra với điểm gần nhất của mỗi hộp.

overlap_x = half_w_a + half_w_b − |dx|
overlap_y = half_h_a + half_h_b − |dy|
contact if overlap_x > 0 and overlap_y > 0
normal = axis of the smaller overlap,  penetration = that overlap

closest = clamp(ball_pos, box_min, box_max)
normal  = (ball_pos − closest) / |ball_pos − closest|
penetration = radius − |ball_pos − closest|
Nguồn: [2] [3]

Đẩy ra, rồi nảy lại

Các vật chồng lấn được đẩy ra theo tỉ lệ khối lượng nghịch đảo, chừa một khoảng 0,3 px để chồng gạch đứng yên không bị rung. Sau đó một xung lực triệt tiêu tốc độ lao vào nhau. Chỉ va chạm mạnh mới nảy, nên chồng gạch lắng xuống êm.

inv_sum = inv_mass_a + inv_mass_b
correction = 0.8 · max(penetration − 0.3, 0) / inv_sum
pos_a −= normal · correction · inv_mass_a
pos_b += normal · correction · inv_mass_b

rel_vel_n = (vel_b − vel_a) · normal          (skip if > 0)
restitution = 0.22 if rel_vel_n < −260 else 0
impulse = −(1 + restitution) · rel_vel_n / inv_sum
vel_a −= impulse · normal · inv_mass_a
vel_b += impulse · normal · inv_mass_b
Nguồn: [2]

Ma sát

Sự trượt bị cản dọc theo bề mặt tiếp xúc, tối đa 55% xung lực pháp tuyến (ma sát Coulomb). Nhờ vậy một bức tường gạch giữ nguyên hình dạng cho đến khi quả bóng đâm vào.

tangent   = (−normal_y, normal_x)
rel_vel_t = (vel_b − vel_a) · tangent
friction  = clamp(−rel_vel_t / inv_sum, −0.55·impulse, 0.55·impulse)
vel_a −= friction · tangent · inv_mass_a
vel_b += friction · tangent · inv_mass_b
Nguồn: [2] [4]

Trọng lượng và chồng gạch vững

Gạch to nặng hơn và quả bóng nặng nhất. Mỗi bước sắp xếp các vật từ dưới lên và quét mọi tiếp xúc 10 lần, một kiểu lặp Gauss–Seidel giúp lực đỡ truyền lên qua cả chồng gạch.

inv_mass = 1 / studs        (2-stud brick = ½,  4-stud = ¼)
ball inv_mass = 1/14
solver_iterations = 10,  bodies sorted bottom-first
floor: bounce 0.18 (bricks) / 0.35 (ball) when impact > 320 px/s
floor friction: vel_x *= 0.97
Nguồn: [4]
Nguồn
  1. Fiedler, G. (2004). “Fix Your Timestep!” Gaffer On Games. gafferongames.com/post/fix_your_timestep/
  2. Envato Tuts+. “How to Create a Custom 2D Physics Engine: The Basics and Impulse Resolution.” code.tutsplus.com/how-to-create-a-custom-2d-physics-engine-the-basics-and-impulse-resolution--gamedev-6331t
  3. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (chồng lấn AABB và điểm gần nhất).
  4. Catto, E. (2005). “Iterative Dynamics with Temporal Coherence.” box2d.org/files/ErinCatto_IterativeDynamics_GDC2005.pdf
030405 · Trò chơi nhỏ

Nghỉ giải lao một chút

Ba trò chơi nhỏ, mỗi trò chỉ vài kilobyte JavaScript thuần. Chuyển qua lại bằng menu phía trên. Chơi được bằng chuột, cảm ứng và bàn phím, và ghi nhớ điểm cao nhất của bạn.

Xếp chồng

Kỷ lục 0

Chạm hoặc nhấn Space để thả. Phần nhô ra ngoài mép sẽ gãy rơi. Thả thật chuẩn để giữ nguyên độ rộng.

Cách hoạt động Toán học đằng sau Stack it

Viên gạch trượt

Viên gạch mới chạy qua lại trên màn hình và nhanh dần sau mỗi viên bạn đặt được, đến một giới hạn.

speed = min(520, 170 + 10 · height) px/s
x += direction · speed · dt
turn around past −0.3·width (left) or canvas_w + 0.3·width (right)
Nguồn: [1]

Cắt phần thừa

Khi thả, chỉ phần chồng lên viên gạch bên dưới được giữ lại. Phần còn lại gãy rơi; không chồng lên chút nào là thua.

left    = max(x_new, x_top)
right   = min(x_new + w_new, x_top + w_top)
overlap = right − left
overlap ≤ 0  → game over
kept brick = (left, overlap)     scrap = w_new − overlap
Nguồn: [1]

Thả hoàn hảo

Đặt lệch dưới 5 px so với viên bên dưới thì không bị cắt gì. Ba lần thả hoàn hảo liên tiếp sẽ bắt đầu nới rộng viên gạch trở lại.

perfect = |x_new − x_top| < 5 px
perfect → combo += 1, width unchanged
combo ≥ 3 → width += min(12, 0.6·canvas_w − width)
Nguồn: [1]

Mảnh vụn rơi và camera

Mảnh vụn rơi theo trọng lực và xoay văng đi. Camera nâng lên với độ mượt không phụ thuộc tốc độ khung hình để đỉnh tháp luôn trong tầm nhìn.

scrap_vel_y += 1600 · dt        scrap_spin = ±2.4 rad/s
target = max(0, 30 · height − 0.5 · canvas_h)
camera += (target − camera) · min(1, 6 · dt)
Nguồn: [2]
Nguồn
  1. Do chúng tôi tự viết. Lối chơi chạm để xếp chồng đến từ thể loại xếp chồng một chạm, phổ biến nhờ “Stack” của Ketchapp (2016).
  2. Làm mượt hàm mũ (làm mượt theo tốc độ khung hình). Wikipedia. en.wikipedia.org/wiki/Exponential_smoothing

Phá gạch

Kỷ lục 0

Di chuyển thanh đỡ bằng con trỏ, ngón tay hoặc phím mũi tên. Chạm hoặc nhấn Space để phóng bóng. Ba mạng.

Cách hoạt động Toán học đằng sau Brick breaker

Phóng bóng và tốc độ

Quả bóng rời thanh đỡ ở một góc ngẫu nhiên nhỏ, nhanh hơn ở mỗi cấp. Mỗi khung hình được chia thành 3 bước con để bóng nhanh không xuyên qua gạch.

launch_angle ∈ [−0.45, 0.45] rad
speed = min(640, 400 + 40 · level) px/s
vel = speed · (sin launch_angle, −cos launch_angle)
sub_steps = 3 per frame
Nguồn: [1]

Tường và thanh đỡ

Tường phản xạ quả bóng. Thanh đỡ đặt góc mới theo chỗ bóng chạm vào: chính giữa đưa bóng thẳng lên, hai mép đưa bóng nghiêng tới khoảng 60°. Nhờ đó bạn có thể ngắm.

walls:   vel_x = ±|vel_x|,  ceiling: vel_y = |vel_y|
hit_offset   = clamp((ball_x − paddle_x) / (paddle_w / 2), −1, 1)
bounce_angle = 1.05 · hit_offset rad
vel = |vel| · (sin bounce_angle, −cos bounce_angle)
Nguồn: [1]

Đập gạch

Một viên gạch bị đập khi bóng cách điểm gần nhất của viên gạch không quá bán kính bóng. Bóng nảy ra theo cạnh có phần chồng lấn nhỏ hơn và nhanh thêm 1,2% sau mỗi cú đập. Từ cấp 2, hàng trên cùng cần hai cú đập.

closest = clamp(ball_pos, brick_min, brick_max)
hit if |ball_pos − closest|² ≤ radius²
flip vel_x if overlap_x < overlap_y, else flip vel_y
speed *= 1.012 while speed < 760
top_row_hits = 2 from level 2
Nguồn: [1] [2]

Mảnh vỡ và cấp độ

Gạch vỡ tung ra 6 mảnh nhỏ rơi xuống rồi mờ dần. Dọn sạch bàn sẽ sang cấp tiếp theo với nhiều hàng hơn.

pieces = 6,  vel_x ∈ [−180, 180],  vel_y ∈ [−260, 0] px/s
piece_vel_y += 900 · dt,   life = 0.7 s
rows = min(7, 4 + level),  lives = 3
Nguồn: [3]
Nguồn
  1. MDN Web Docs. “2D breakout game using pure JavaScript.” developer.mozilla.org/en-US/docs/Games/Tutorials/2D_Breakout_game_pure_JavaScript
  2. Ericson, C. (2004). Real-Time Collision Detection. Morgan Kaufmann (hình tròn với hộp).
  3. Do chúng tôi tự tinh chỉnh. Thể loại này bắt nguồn từ “Breakout” của Atari (1976).

Brick drop

Cấp cao nhất —

Không tính điểm, chỉ có cấp. Xây chồng gạch lên tới vùng nguy hiểm màu đỏ, rồi xóa hàng cho đến khi một viên gạch chạm lại đáy tấm nền: đó là một lần lội ngược, và 5 lần lội ngược thì qua cấp. Mỗi cấp, vạch nguy hiểm dâng lên gần nóc thêm một hàng, nên bạn phải liều hơn. Gạch khóa ở hàng trên cùng là thua. Hàng đầy sẽ bật ra và gạch lơ lửng rơi xuống. Các hình: 2×1, 2×2, U, móc vuông, chữ T đôi và m. Phím: ← → di chuyển, ↑ xoay, ↓ rơi nhanh, Space thả, P tạm dừng. Cảm ứng: chạm để xoay, kéo để di chuyển, vuốt xuống để thả.

Cách hoạt động Toán học đằng sau Brick drop

Bàn chơi và túi gạch

Tấm nền là lưới 15 × 22. Gạch được lấy từ một túi 8 viên, xáo trộn (Fisher–Yates) và lấy hết rồi mới làm đầy lại, nên gạch nhỏ xuất hiện gấp đôi gạch lớn và không viên nào vắng mặt quá lâu.

bag = [2×1, 2×1, 2×2, 2×2, u, square_hook, double_t, m]
for i from len(bag) − 1 down to 1:
    j = rand_int(0, i)
    swap(bag[i], bag[j])
Nguồn: [1]

Xoay một viên gạch

Một viên gạch xoay 90° bằng cách sắp lại các ô trong hình vuông bao quanh nó, rồi được dịch nhẹ để giữ ở giữa. Nếu vướng, nó thử đẩy nhẹ sang hai bên, rồi lên một hàng. Viên 2×2 không bao giờ xoay.

clockwise: (col, row) → (n − 1 − row, col)     n = max(width, height)
normalize to top-left, then
x += floor((width − new_width) / 2)
y += floor((height − new_height) / 2)
kicks = [0, −1, +1, −2, +2], then up 1
Nguồn: [3]

Rơi và khóa

Gạch rơi một hàng mỗi nhịp và nhanh hơn ở mỗi cấp. Gạch vừa chỗ nếu mọi ô đều nằm trong bàn và còn trống; khi không thể rơi tiếp, gạch khóa lại và các núm kêu “cách”.

fall_interval = max(0.08, 0.85 − 0.07 · (level − 1)) s per row
fits = every cell inside the board and empty
can’t fall → lock; click on each cell resting on floor or another brick
Nguồn: [4]

Hàng bật ra và rơi dây chuyền

Các hàng đầy cùng bật ra trong 0,5 giây. Sau đó viên gạch nào bị treo lơ lửng sẽ rơi nguyên khối: các ô của cùng một viên còn chạm nhau được gom nhóm bằng thuật toán loang (flood fill), và một nhóm rơi xuống một hàng khi mọi ô bên dưới đều trống. Lặp lại mỗi 0,06 giây cho đến khi không còn gì di chuyển, rồi kiểm tra các hàng lần nữa để tính chuỗi.

snap_offset = (t / 0.5)²
groups = flood_fill(same brick, 4 neighbours)
group falls if, for every cell, the cell below is empty or its own
repeat every 0.06 s until stable → check rows again (chain)
Nguồn: [2]

Vạch nguy hiểm và cấp độ

Không tính điểm. Xây lên tới vùng nguy hiểm sẽ kích hoạt một lần lội ngược; một viên gạch chạm đáy sẽ hoàn thành nó. Năm lần lội ngược thì qua cấp, và mỗi cấp nâng vạch lên một hàng về phía nóc.

danger_line = max(1, 6 − (level − 1))          (rows from the top)
armed when stack_top ≤ danger_line, after being below it
comeback when armed and a locked brick touches the floor
comebacks = 5 → level += 1
locks in row 0 → topped out
Nguồn: [4]
Nguồn
  1. Thuật toán xáo trộn Fisher–Yates (Durstenfeld 1964; Knuth, thuật toán P). Wikipedia. en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
  2. Thuật toán loang (flood fill). Wikipedia. en.wikipedia.org/wiki/Flood_fill
  3. Ma trận quay (xoay 90° trên lưới). Wikipedia. en.wikipedia.org/wiki/Rotation_matrix
  4. Thiết kế trò chơi của riêng chúng tôi: bàn chơi, bộ gạch, khóa bằng núm, hàng bật ra, rơi dây chuyền, lội ngược và vạch nguy hiểm dâng lên.

Bạn muốn điều này trong sản phẩm của mình?

Trình xem sản phẩm 3D, công cụ cấu hình, onboarding thú vị, trò chơi mang thương hiệu. Chúng tôi xây dựng trải nghiệm tương tác nhanh, dễ tiếp cận, và những phần nặng chỉ tải khi có người mở, nên phần còn lại của website vẫn nhanh.

Bắt đầu dự án