2 điểm bởi GN⁺ 2025-02-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong các game có thể đào và đắp địa hình, hồ·sông·vũng nước phải chảy theo ranh giới mới, vì vậy cần một mô phỏng nước dựa trên lưới nhanh và ổn định
  • Mục tiêu là một mô hình trường độ cao 2D dùng cùng lưới với địa hình ở quy mô khoảng 1m, đồng thời đáp ứng bảo toàn nước, ổn định có thể kiểm soát và chi phí cập nhật tuyến tính
  • Smoothed Particle Hydrodynamics và Stable Fluids lần lượt phù hợp với chất lỏng hạt độ phân giải cao và thể tích chất lỏng kín, nên không khớp với yêu cầu xử lý nhanh mặt nước tự do trên địa hình
  • Cách tiếp cận virtual pipes được chọn lưu chiều cao nước và dòng chảy giữa các ô trên staggered grid, rồi xử lý gia tốc dòng chảy, co giãn dòng thoát và cập nhật cột nước bằng vài vòng lặp mảng 2D
  • Với dtg phù hợp, mô hình cho kết quả trông giống nước, nhưng vẫn còn hạn chế là không có quán tính và khuếch tán vận tốc, nên dòng nước nhanh đi vào hồ sẽ không tiếp tục lan truyền vào bên trong

Vì sao nước khó trong game có chỉnh sửa địa hình

  • Trong game chiến thuật hoặc mô phỏng thành phố·làng mạc, nước có thể đóng vai trò là ranh giới tự nhiên, cũng như phục vụ hàng hải·câu cá·thương mại·hải chiến, nước uống·vận chuyển·thẩm mỹ
  • Nếu có thể trực tiếp thay đổi địa hình, độ khó xử lý nước tăng lên rất nhiều
    • Khi đào các tài nguyên như đất·cát·đất sét khỏi mặt đất, việc địa hình cũng bị loại bỏ theo là tự nhiên hơn
    • Đá và quặng kim loại cũng phù hợp hơn với kiểu khai thác bằng cách đào vào địa hình thay vì chỉ là object trên bề mặt
    • Nếu không thể xây công trình trên sườn dốc thì phải làm phẳng địa hình trước khi xây
    • Có thể cung cấp chính chỉnh sửa địa hình như một công cụ biểu đạt sáng tạo
  • Tình huống cốt lõi là khi đào thủng ranh giới của hồ hoặc vũng nước để mở đường nước, thì nước sẽ chảy đi đâu và bao nhiêu

Điểm mà lời giải đơn giản chưa đủ

  • Một số cách lách vấn đề có thể là cố định nước ở vị trí ban đầu, coi mọi nơi dưới một cao độ nhất định đều là nước, hạn chế không cho đào quá sâu, hoặc dùng mô hình dòng chảy đơn giản kiểu Minecraft·Dwarf Fortress
  • Những cách này có thể dùng như phương án thay thế, nhưng quá đơn giản hoặc quá khối hộp để làm mô hình chính
  • Mô hình của Dwarf Fortress gần với nhu cầu hơn các ví dụ khác, nhưng được thiết kế với giả định 3D, trong khi bài toán cần chủ yếu là nước trên địa hình 2D
  • Timberborn dùng một mô hình cùng loại với mô hình được nói tới ở đây

Các điều kiện mong muốn của mô phỏng

  • Mô hình nước được nhắm tới cần đáp ứng các điều kiện sau
    • Tốt nhất là hoạt động trên cùng lưới với địa hình
    • Quy mô trung bình khoảng 1m, không cần mô phỏng cả những tia nước bắn nhỏ
    • Nước được xem như một trường độ cao trên địa hình, không xét dòng chảy theo phương thẳng đứng hay khoảng trống trong mặt cắt đứng
    • Nước phải chảy được và không được biến mất do sai số mô phỏng
    • Phải có khả năng kiểm soát độ ổn định
    • Chi phí mỗi bước phải tuyến tính theo kích thước mô phỏng, và lý tưởng nhất là kết thúc sau vài vòng lặp

Sự lệch pha với các mô phỏng chất lỏng hiện có

  • Smoothed Particle Hydrodynamics tạo ra kết quả chất lỏng độ phân giải cao rất ấn tượng, nhưng khác với bài toán cần ở đây
    • Hạt nước cỡ 1m có thể trông như bóng nước
    • Nếu làm hạt nhỏ hơn thì chi phí hiệu năng tăng cao
    • Mục tiêu không phải độ chân thực độ phân giải cao mà là một mô hình nhanh và đủ thuyết phục
  • Stable Fluids của Jos Stam gần hơn với mô hình xử lý một thể tích đầy chất lỏng như bồn kín
    • Nó khác với bài toán trực tiếp xử lý mặt nước tự do trên địa hình
    • Ở một số bước phải lặp để giải hệ tuyến tính thưa nên chi phí lớn
    • Đây là cách tiếp cận giải toàn bộ Navier-Stokes, trong khi thứ cần ở đây là shallow water equations

Shallow water equations và lựa chọn lưới

  • Shallow water equations là cách tiếp cận lấy trung bình lớp nước theo phương thẳng đứng trên địa hình và xử lý nó như các phương trình 2D
  • “Shallow” nghĩa là giả định kích thước thẳng đứng của cột nước nhỏ hơn rất nhiều so với quy mô ngang đang quan tâm
    • Ví dụ có thể đúng khi sông sâu vài mét đến vài chục mét nhưng khoảng cách quan tâm lại ở cấp kilomet
  • Collocated grid thông thường lưu độ cao nước và vận tốc trong cùng một ô, nhưng điều này có thể gây vấn đề trong động lực học chất lỏng
    • Nếu rời rạc hóa đạo hàm bậc một một cách ngây thơ thì có thể sinh thiên lệch hướng hoặc mất ổn định
    • Nếu dòng chảy đi vào từ trái phải và đi ra theo trên dưới trong cùng một ô, sẽ xuất hiện nghịch lý là vận tốc tổng thể trông như bằng 0
  • Staggered grid lưu các giá trị như độ cao nước·mật độ trong ô, còn vận tốc hoặc dòng chảy thì lưu ở cạnh giữa các ô
    • Mảng độ cao nước N x N
    • Mảng dòng chảy theo hướng X (N+1) x N
    • Mảng dòng chảy theo hướng Y N x (N+1)

Phương pháp Virtual pipes

  • Virtual pipes là cách tính dòng chảy bằng cách coi các ô nước được nối với nhau bởi những ống ảo
  • Một trong các bài báo được tham chiếu còn xử lý cột nước đa mức và kết nối theo phương đứng, bài khác chủ yếu nói về xói mòn thủy lực, nhưng ở đây bỏ qua các mục tiêu đó
  • Có 3 giá trị được lưu
    • water: chiều cao cột nước ở mỗi ô
    • flowX: tổng dòng nước giữa các ô kề nhau theo phương ngang
    • flowY: tổng dòng nước giữa các ô kề nhau theo phương dọc
  • Thay vì vận tốc, mô hình lưu dòng chảy (flow, flux)
    • Có thể xem dòng chảy là thể tích nước đi qua trong một đơn vị thời gian
    • Dòng chảy giữa các ô rỗng được định nghĩa tự nhiên là 0
    • Vận tốc là dòng chảy chia cho tiết diện, nên khi nước gần như không có sẽ dễ gặp bài toán 0/0 hoặc ngưỡng cắt

3 giai đoạn tạo thành một bước

  • Một bước mô phỏng được chia thành ba giai đoạn
    • Gia tốc dòng chảy: tăng dòng chảy giữa các ô tùy theo chênh lệch độ cao mặt nước của các ô lân cận
    • Co giãn dòng thoát: nếu lượng nước rời một ô lớn hơn lượng nước thực có thì giảm các dòng thoát ra
    • Cập nhật cột nước: cộng hoặc trừ chiều cao nước của mỗi ô theo các dòng chảy lân cận
  • Gia tốc dòng chảy

    • Nếu độ cao nước của hai ô kề nhau khác nhau thì dòng chảy sẽ tăng tốc từ phía cao sang phía thấp
    • Dòng chảy được cập nhật trên các cạnh bên trong theo hướng X·Y, với g, dt, dx, dy được sử dụng
    • Tiết diện A của ống ảo chỉ xuất hiện dưới dạng nhân với g, nên trong trường hợp đơn giản có thể xem như gộp vào g
    • Ma sát được thêm bằng cách làm giảm dòng chảy mỗi bước
    • Bài báo khuyến nghị hệ số pow(friction, dt)
    • Để dùng giá trị trực quan hơn, có thể dùng pow(1-friction, dt)
    • Có thể hiểu friction=0 là ma sát lớn nhất làm triệt tiêu hoàn toàn dòng chảy trước đó, còn friction=1 là không có ma sát
    • dt càng lớn thì mô phỏng càng nhanh nhưng có thể kém ổn định hơn
    • Trong mô phỏng chất lỏng, điều kiện Courant-Friedrichs-Lewy rất quan trọng
    • Trên thực tế phải giảm dt cho tới khi ổn định, và giá trị được dùng nằm khoảng 0.001~0.01
  • Cập nhật cột nước

    • Mỗi ô nhìn vào bốn dòng chảy lân cận để cộng hoặc trừ nước
    • flowX(x,y)flowY(x,y) đi vào từ trái·dưới sẽ được cộng
    • flowX(x+1,y)flowY(x,y+1) đi ra sang phải·lên trên sẽ bị trừ
    • Đây là bước thực sự di chuyển nước giữa các ô theo dòng chảy đã tính
  • Co giãn dòng thoát

    • Nếu dòng chảy quá lớn, sau khi cập nhật chiều cao nước của một số ô có thể trở thành âm
    • Chỉ các dòng chảy đi ra khỏi mỗi ô được cộng lại để kiểm tra xem lượng nước bị lấy đi trong một bước có vượt quá lượng nước thực có hay không
    • Nếu lượng bị lấy đi quá lớn thì các dòng chảy đi ra được giảm theo cùng một tỷ lệ để đảm bảo chiều cao nước luôn không âm
    • Đây là cơ chế ổn định hóa then chốt để ngăn lượng nước âm

Địa hình, điều kiện biên và xử lý độ nhớt

  • Địa hình được phản ánh trong bước gia tốc dòng chảy bằng cách dùng độ cao mặt nước thay vì chiều cao cột nước
    • Độ cao mặt nước = terrain(x,y) + water(x,y)
    • Ô có địa hình cao hơn có thể có mặt nước cao hơn dù chiều cao cột nước bằng nhau, nên nước có thể di chuyển
  • Điều kiện biên được xác định ngầm qua các giá trị dòng chảy ở biên
    • flowX(0,y), flowX(N,y), flowY(x,0), flowY(x,N) tương ứng với biên
    • Đặt bằng 0 thì chúng hoạt động như tường
    • Giá trị dòng vào sẽ thêm nước, còn giá trị dòng ra sẽ lấy nước đi
    • Với nước trên địa hình, biên thoát nơi nước biến mất ở rìa bản đồ có thể là lựa chọn tự nhiên
    • Ở nơi sông băng qua biên, có thể đặt thành biên vào để nước sông tiếp tục chảy
  • Dòng chảy ở biên phải được thiết lập lại ở đầu mỗi bước mô phỏng
    • Vì bước co giãn dòng thoát có thể làm thay đổi dòng ở biên và khiến biên thoát biến thành tường
  • Bài báo còn có một hạng tử độ nhớt làm giảm dòng chảy tùy theo chiều cao nước
    • Ý tưởng là lớp nước mỏng khó chuyển động hơn do lực bên trong, còn lớp nước lớn thì chuyển động tự do hơn
    • Điều này có thể hữu ích cho những thứ như dòng magma
    • Nó không được dùng cho nước, và ở quy mô địa hình lớn thì ảnh hưởng của độ nhớt gần như không đáng kể

Luồng triển khai và dạng hiệu năng

  • Toàn bộ mã được tổ chức theo thứ tự sau
    • Khởi tạo dòng chảy ở biên
    • Tính trước hệ số ma sát pow(1-friction, dt)
    • Gia tốc dòng chảy theo X
    • Gia tốc dòng chảy theo Y
    • Co giãn dòng thoát để ngăn lượng nước âm
    • Cập nhật cột nước
  • Phần lớn mô phỏng kết thúc bằng 4 vòng lặp duyệt qua vài mảng 2D và các công thức đơn giản
  • Toàn bộ mã cập nhật C++ có thể xem trong water_2d.cpp
  • Ví dụ video đến từ WebGPU water simulator được công bố vài ngày trước, và các hạt trong video chỉ để trực quan hóa chứ không tham gia vào mô phỏng
  • Nếu tìm được dtg phù hợp thì mô hình trông ổn định, đáp ứng các yêu cầu và cho ra kết quả nhìn như nước

Những hạn chế còn lại

  • Mô hình này không có quán tính và khuếch tán vận tốc
    • Dòng nước nhanh đi vào hồ sẽ không tiếp tục lan truyền sâu vào trong hồ mà tỏa ra theo mọi hướng
    • Nếu độ cao nước bằng nhau, hai dòng nước song song theo hướng ngược nhau có thể cùng tồn tại mà không tương tác
  • Khi nước lần đầu tràn vào một khu vực, có thể xuất hiện gợn sóng và trông hơi lạ

Mở rộng sang lưới lục giác·tam giác

  • Game mục tiêu không dùng lưới vuông mà dùng lưới tam giác đều
  • Có thể xem lưới tam giác là đối ngẫu của lưới lục giác
    • Nếu nối tâm các ô lục giác kề nhau bằng đường thẳng, ta sẽ được lưới tam giác đều
    • Nó tương tự cách bài viết về hexagonal grids của Red Blob Games dùng axial coordinate system cho lưới đối ngẫu của hex pointy-top
  • Lưới tam giác cũng có thể được lưu trong một mảng 2D thông thường hơi nghiêng
  • Chiều cao cột nước được lưu ở các đỉnh lưới để việc render mặt nước dễ hơn
  • Dòng chảy được chia thành ba hướng
    • Dòng theo hướng X
    • Dòng theo hướng Y
    • Dòng theo hướng Z
  • Với lưới đỉnh N x N, dùng các mảng sau
    • Mảng dòng X (N+1) x N
    • Mảng dòng Y N x (N+1)
    • Mảng dòng Z (N+1) x (N+1), trong đó không dùng các giá trị bottom-left và top-right
  • So với lưới vuông, chỉ cần thêm dòng Z vào điều kiện biên, gia tốc, co giãn dòng thoát và cập nhật nước
  • Phần khó nhất là không bị sai indexing
  • Mã C++ cho lưới tam giác·lục giác có thể xem trong water_2d_hex.cpp
  • Cách này có thể đẳng hướng hơn một chút so với lưới vuông

1 bình luận

 
GN⁺ 2025-02-08
Ý kiến trên Hacker News
  • Có các video Coding Adventure như một cách tiếp cận khác về mô phỏng chất lưu
    Rendering Fluids: https://www.youtube.com/watch?v=kOkfC5fLfgE
    I Tried Putting my Fluid Simulation on a Planet: https://www.youtube.com/watch?v=8nIB7e_eds4&t=817s
    GitHub: https://github.com/SebLague/Fluid-Sim?tab=readme-ov-file

  • Một trong những lý do mô phỏng thủy văn khó trong game sinh thủ tục là khi nước tích tụ, nó ảnh hưởng đến các ô xung quanh, rồi ảnh hưởng đó lại tiếp tục lan sang các ô xung quanh khác
    Sinh thủ tục thường rất phù hợp để song song hóa, nhưng trong các vùng vô hạn — nơi tưởng như cần song song hóa nhất — lại khó song song hóa đúng kiểu tính toán này
    Tôi chưa thấy chủ đề này được khai phá nhiều, và trong số những người làm việc liên quan, tôi đặc biệt thích https://nickmcd.me. Đó là một trong những địa hình sinh thủ tục tốt nhất tôi từng thấy
    Tuy vậy, công trình đó cũng bị giới hạn vùng do thiết kế mô phỏng. Trong các hướng giải khả dĩ, cách có vẻ tốt nhất là sinh thủ tục các ranh giới lưu vực không thể bị phá vỡ, rồi mô phỏng song song toàn bộ từng lưu vực cùng một lúc
    Đây là một vấn đề rất thú vị, nhưng nằm ngoài phạm vi hiểu biết của tôi nên tôi chủ yếu chỉ đứng quan sát

    • Nếu tò mò, nên tìm các khái niệm “vùng ảnh hưởng” và “vùng phụ thuộc” trong lĩnh vực giải phương trình đạo hàm riêng
      Chúng chỉ vùng có thể ảnh hưởng đến giá trị tại một điểm nhất định, và vùng mà giá trị tại điểm đó có thể ảnh hưởng tới; điều này liên quan trực tiếp đến những gì vừa nói ở trên. Trong một số trường hợp có thể biết trước các vùng đó
    • Thật lòng thì https://nickmcd.me rất đáng để xem. Thực sự xuất sắc
    • Câu hỏi thú vị. Có lẽ sẽ khả thi nếu đặt biên quanh mỗi vùng và giả định tốc độ tối đa mà hiệu ứng có thể lan truyền, tức là tính nhân quả
      Ví dụ, để mô phỏng bước 10 giờ thì đặt biên rộng 10 ô lưới. Tính 10 bước trong mỗi vùng, rồi đồng bộ trạng thái biên với các mô phỏng biên khác được tính song song, và lặp lại
    • Tôi ngạc nhiên khi biết Nick mới chỉ 25 tuổi
  • Hơi lệch chủ đề một chút, nhưng tôi nhớ đến đoạn trong bài nói rằng cần thao tác địa hình để thu thập tài nguyên
    Tôi luôn nghĩ Animal Crossing xử lý việc này khá khéo léo và hiệu quả mà không cần thao tác địa hình. Chặt cây sẽ có gỗ rơi ra, nhưng chỉ một lượng nhất định và về cơ bản tạo ra thời gian hồi
    Có thể tạo cảm giác phản hồi và tài nguyên hữu hạn mà không cần thao tác địa hình tốn kém. Tất nhiên không hợp với mọi game và phù hợp hơn với bản đồ nhỏ, nhưng đáng cân nhắc. Nếu game không thật sự cần, thường tốt hơn là đừng thao tác địa hình

    • Bài viết cũng đã đề cập chiến lược như vậy dưới dạng đá quặng vàng đặt xung quanh
      Đây là cách cấp tài nguyên tiêu chuẩn, nhưng thời gian hồi không loại bỏ vấn đề tài nguyên vô hạn mà chỉ làm chậm lại. Và nó hơi nhàm chán, tác động cũng yếu
  • Bài viết đào sâu chủ đề này rất gọn gàng, và tôi vui vì đã nhắc đến Timberborn
    Dạo này tôi hoàn toàn mê game đó, nên nếu bạn chưa chơi thì rất khuyến nghị. Dòng chảy nước dựa trên vật lý giống như một nhân vật khác trong game, và việc tìm ra cách chặn nước để dùng cho động cơ và cấp cho ruộng là vòng lặp gameplay cốt lõi

  • Thú vị và thực hiện thật sự tốt. Rủi ro lớn nhất khi phát triển những thứ như thế này là ngồi chỉnh tham số hàng giờ chỉ vì kết quả trông đẹp
    Nó làm tôi nhớ lại hồi năm 2011 khi tự triển khai thủy động lực học trên GPU cho một bài báo. Tôi xử lý máu, tức chất lưu chảy trên bề mặt/mô; mô phỏng ở 2D rồi chiếu lên mesh, có xét đến trọng lực và độ dốc bề mặt
    Tôi cũng đã đăng một video ngắn lên YouTube: https://youtu.be/4vGrNc-GGW8

  • Thật tuyệt
    Gần đây tôi đã thử nghiệm một ý tưởng tương tự với sự trợ giúp của o3-mini-high. Tôi giải thích ý tưởng thuật toán, và nó triển khai rồi render 3D mà không cần can thiệp thủ công. Tuy nhiên tôi đã phải nhập nhiều prompt
    https://3d-water-sim.netlify.app/
    Nó vẫn chưa hoàn hảo, vì tôi đã dừng lại giữa chừng; nhưng mỗi vòng lặp đều cải thiện khá rõ. Điểm thú vị là để tạo địa hình, nó không lấy từ CDN hay thứ gì tương tự, mà tự triển khai đúng từ đầu một phiên bản hoạt động của nhiễu Perlin

    • Khi làm những thử nghiệm kiểu này, tôi tò mò mục đích là để vui hay để học. Nếu mục đích là học, tôi cũng tò mò liệu bạn có vẫn thấy việc đọc bài này đáng giá dù không tự tay triển khai hay không
      Đây là câu hỏi về sự khác nhau giữa hành trình và đích đến
  • Phần trong bài viết nói rằng “mô hình này không có quán tính và khuếch tán vận tốc. Ngay cả khi một dòng nước chảy nhanh đổ vào hồ, nó cũng không lan truyền sâu hơn vào bên trong hồ mà lan ra mọi hướng trong khi bỏ qua quán tính đã tích lũy. Nếu mực nước bằng nhau, hai dòng nước song song chảy theo hướng ngược nhau cũng có thể không tương tác với nhau” có vẻ có thể giải quyết bằng cách lấy trung bình với 6 mũi tên dòng chảy xung quanh theo cùng hướng
    Tức là gán trọng số lớn cho các mũi tên phía trước/sau, và trọng số nhỏ cho các mũi tên bên cạnh. Ví dụ, khi có các mũi tên như sau
    -a-> -b->
    -c-> -d-> -e->
    -f-> -g->
    New_d = d * (1 - 2*.1 - 4*.01) + (c+e).1 + (a+b+f+b).01
    Ở đây .1 và .01 là các trọng số chọn tùy ý nên cần điều chỉnh, và cũng có thể đưa lũy thừa vào giống như cách dùng để giảm dao động. Nếu tính cả hệ số đó thì có thể thành như sau
    New_d = d * (1 - 2*.1 - 4*.01 - .001) + (c+e).1 + (a+b+f+b).01

    • Cách giải đúng có vẻ gần với việc thêm một lưới cho đạo hàm bậc hai khớp với lưới gốc
      grid 0: chiều cao nước của từng ô
      grid 1: dòng chảy nước qua từng cạnh, tức đạo hàm bậc nhất
      grid 2: gia tốc của nước trong từng ô, tức đạo hàm bậc hai
      Cấu trúc là mỗi lưới là lưới đối ngẫu của lưới trước đó và lưu giá trị đạo hàm của nó. Thực ra có lẽ không cần xử lý đặc biệt dữ liệu cạnh; chỉ cần dữ liệu đỉnh và xử lý thuần theo lưới đối ngẫu cũng được. Dòng chảy qua cạnh có thể suy ra bằng tổng dòng chảy tại hai đỉnh ở hai đầu cạnh đó
      Vì vậy, cập nhật chiều cao chất lỏng bằng dòng chảy, rồi cập nhật gia tốc dựa trên khối lượng chất lỏng đã đi vào ô với tốc độ nào và bao nhiêu, sau đó cập nhật dòng chảy bằng gia tốc và chiều cao chất lỏng hiện tại. Tôi không rành thủy động lực học, nhưng nhìn từ góc độ mô phỏng số thì có vẻ đúng, và cũng cho phép dòng chảy theo đường chéo
    • Cách đó phá vỡ bảo toàn động lượng. Để có dòng chảy thực tế, cần dùng phương trình liên tục để bảo toàn năng lượng của dòng chảy, và để lực cắt tiêu tán/khuếch tán thành xoáy
      Như bài viết đã nói, lượng tính toán sẽ tăng lên nhiều, nên trước hết cần cân nhắc mức độ thực tế đó có cần thiết cho trường hợp sử dụng thực tế hay không
  • Đây là một sản phẩm thô sơ tôi làm vài năm trước vì tò mò: https://aperocky.com/hydrosim/
    Trước khi dự án cá nhân này bị đưa vào kệ lưu trữ lạnh, tôi đã không tìm ra cách xử lý xói mòn. Tôi thích việc tác giả đề cập đến phần này và còn kèm cả phương trình

  • Gần đây tôi đã công bố một thứ tương tự. Có cả tạo heightfield ngẫu nhiên, vận chuyển trầm tích và xói mòn: https://github.com/Ono-Sendai/terraingen

  • Bạn có thể tự tay thử mô phỏng lũ lụt mang tính giáo dục do một lập trình viên xuất sắc ở công ty chúng tôi tạo ra như một phần của dự án nghiên cứu
    https://flood.concord.org/
    Để thấy hiệu ứng lớn, cần thay đổi các giá trị mô hình ở thanh công cụ bên dưới
    Đây là mô phỏng dựa trên ô, tính giá trị ô trong WebGL dựa trên các ô lân cận. Shader thực hiện phép tính đó nằm ở đây
    https://github.com/concord-consortium/flooding-model/blob/ma...