Tạo địa hình thủ tục từ trên xuống


8

Tôi đang cố gắng triển khai hệ thống tạo địa hình dựa trên voxel trong Unity3d (C #). Tôi đã thực hiện thành công một hệ thống lưới 3d thống nhất và đã trích xuất phần bề mặt bằng cách sử dụng thuật toán Marching Cubes và Surface Nets.

Tôi nhanh chóng gặp phải các vấn đề cố hữu trong việc thể hiện tất cả không gian bằng lưới 3d. Phần lớn không gian này ở trên hoặc dưới bề mặt, và tôi chuyển sang phân vùng không gian bằng octrees, vì nó dường như không quá khó khăn. Một trong những điều tuyệt vời về octrees là các nút octree không có các cạnh được giao nhau bởi bề mặt không cần phải phân chia lại.

Tôi đã nghiên cứu và tìm thấy một vài tài nguyên để xây dựng mã của mình. Một là Volume GFX , và một cái khác là bài viết Đường viền kép gốc của Ju et al. Việc kiểm tra cạnh của tôi được thực hiện bằng cách kiểm tra của Marching Cube từ mã của Paul Bourke. Nếu "cubeindex" là 0 hoặc 255, thì không có cạnh nào được giao nhau và nút octree không cần phải phân tách.

Mã thế hệ Octree của tôi hoạt động cho một cái gì đó giống như một phần tư hình cầu, trong đó các tính năng bề mặt cực kỳ bình thường: Trực quan hóa nút Octree và bề mặt đường viền kép tương ứng

Như đã thấy trong hình, chỉ có các nút octree chứa bề mặt được chia nhỏ. Làm việc tuyệt vời.

Tuy nhiên, bây giờ, khi chúng ta chuyển sang một thứ phức tạp hơn, như hàm PerlinNiri của Unity3d: GASP Tiếng ồn Perlin sử dụng Octree ! Cái lỗ đó đang làm gì trong lưới ở góc dưới bên phải? Khi kiểm tra kỹ hơn, chúng ta thấy octree không chia nhỏ đúng cách (các đường màu đỏ làm nổi bật kích thước của nút vi phạm): nhập mô tả hình ảnh ở đây Điều này hóa ra cũng là vấn đề tương tự Jules Bloomenthal trong bài viết về đa giác hóa , trang 10, Hình 10. Phương pháp truyền thống khi tạo octrees từ trên xuống ("Phân khu thích ứng"), rất nhạy cảm với các đặc điểm bề mặt mịn so với kích thước của nút octree.

Ý chính

X-------X
|       |
|       |  <- Node
X--/\---X     X's - tested values, all outside of the surface!
  /  \  <- surface

Bề mặt phá vỡ bề mặt, nhưng quay trở lại trước khi nó đi qua một đỉnh. Bởi vì chúng tôi tính toán các giao điểm cạnh bằng cách nhìn vào các dấu hiệu ở hai đỉnh, điều này không đánh dấu cạnh là giao nhau.

Có phương pháp nào để xác định xem những dị thường này có tồn tại không? Tốt hơn là, giải pháp sẽ hoạt động không chỉ cho chức năng nhiễu Unity3d, mà còn cho bất kỳ chức năng nhiễu 3d nào (đối với vách đá / phần nhô ra / đảo nổi, v.v.).

CẬP NHẬT

Nhờ câu trả lời tuyệt vời của Jason, tôi đã có thể trả lời các câu hỏi của riêng mình mà tôi đã hỏi (trong các bình luận dưới câu trả lời của anh ấy). Vấn đề tôi gặp phải là tôi đã không hiểu làm thế nào tôi có thể tạo ra một hàm giới hạn cho các hàm lượng giác (sin, cos, v.v.) do tính chất định kỳ của chúng.

Đối với những người này, tính định kỳ là chìa khóa cho các chức năng giới hạn. Chúng tôi biết rằng sin / cos đạt đến các giá trị cực trị của chúng trong một khoảng thời gian xác định, cụ thể là mỗi π/2. Vì vậy, nếu khoảng thời gian chúng tôi đang kiểm tra có chứa bất kỳ bội số (cos) hoặc nửa bội số (sin) hơn 1 / -1 là cực trị nhất định. Nếu nó không chứa bội số (tức là khoảng [0.1,0.2]), thì phạm vi chỉ đơn giản là các giá trị của hàm được đánh giá tại các điểm cuối của nó.

CẬP NHẬT 2:

Nếu những điều trên không có ý nghĩa, hãy kiểm tra câu trả lời của Jason cho ý kiến ​​của tôi.

Câu trả lời:


6

Tôi sẽ giả sử rằng bạn có một chức năng fsao cho bề mặt được xác định bởi f(x,y,z)==0. Ngay bây giờ, tất cả những gì bạn có là chức năng của fchính nó, và vì vậy không có thêm thông tin, không thể biết khi nào nên chia nhỏ. Bề mặt luôn có thể làm những gì bạn mô tả, tức là chức năng fcó thể có các "vây" mỏng tùy ý có thể yêu cầu bạn đi sâu tùy ý vào quãng tám.

Giải pháp duy nhất cho phép tạo từ trên xuống là có thêm thông tin f. Ngay bây giờ bạn có một truy vấn điểm : bạn có thể xác định giá trị của ftại bất kỳ điểm cụ thể nào, nhưng bạn không có gì đảm bảo về giá trị sẽ ở bất kỳ điểm nào khác, ngay cả những giá trị ngay bên cạnh nó. Thay vào đó, bạn cần một truy vấn giới hạn : đưa ra AABB, nó xác định giá trị tối đa và tối thiểu của fphạm vi đó là bao nhiêu.

Bây giờ rất đơn giản: đối với mỗi nút octree, thực hiện truy vấn giới hạn. Nếu mức tối thiểu lớn hơn 0 hoặc tối đa nhỏ hơn 0, không chia nhỏ. Mặt khác, chia nhỏ trừ khi bạn ở kích thước nhỏ nhất, trong trường hợp đó bạn có thể sử dụng truy vấn điểm trên các góc như bình thường để xây dựng bề mặt.

Thật dễ dàng để viết truy vấn điểm cho các hàm thú vị, nhưng không rõ làm thế nào để viết hàm giới hạn tương ứng. Tuy nhiên, công việc của chúng tôi trở nên dễ dàng hơn bởi thực tế là chúng tôi không cần phải chính xác: tính toán chính xác tối thiểu / tối đa sẽ không đặc biệt hữu ích, vì chúng tôi sẽ so sánh chúng với số không. Vì vậy, thực sự chúng ta chỉ cần tính toán và khoảng [a, b] hoàn toàn chứa [true_min, true_max] trên các giới hạn AABB đã cho. Nếu chúng ta ở gần phạm vi thực, chúng ta sẽ chia nhỏ hầu hết những thứ thực sự chứa bề mặt, và nếu chúng ta lỏng lẻo và đưa ra phạm vi lớn hơn nhiều, cuối cùng chúng ta sẽ chia nhiều nút hơn mức cần thiết. Tuy nhiên, miễn là chúng tôi đảm bảo chứa phạm vi thực sự, chúng tôi sẽ không bao giờ bỏ lỡ một phần của bề mặt.

Ví dụ, chúng ta có thể xây dựng hàm giới hạn cho hàm hình cầu. (Để xem tại sao kỹ thuật của bạn không hoạt động ngay cả trong trường hợp này, hãy bắt đầu với một hình cầu hoàn toàn được chứa trong khối bắt đầu. Thuật toán sẽ chấm dứt ngay lập tức!). Hàm điểm là f(x,y,z) = dist(<x,y,z>, center) - radius. Chúng ta thấy rằng hàm này chỉ phụ thuộc vào khoảng cách từ điểm center, vì vậy thực sự là cốt lõi của hàm 1D. Đầu tiên, chúng tôi xác định phạm vi bán kính nào bao gồm AABB của chúng tôi. Một cách đơn giản nhưng chậm để làm điều này là xác định khoảng cách cho 8 góc và lấy khoảng cách tối thiểu và tối đa. Trường hợp đặc biệt duy nhất là nếu trung tâm được chứa trong AABB, thì mức tối thiểu bằng không. Bây giờ chúng ta có một phạm vi bán kính và bằng cách cắm vào hàm f(r) = r - radiussẽ đưa ra phạm vi mong muốn cho các giá trị của f.

Tất nhiên, đó là một ví dụ đơn giản; bạn có thể muốn một cái gì đó phức tạp hơn hình cầu. Điều hay ho của phương pháp này là bạn có thể kết hợp các hàm giới hạn như vậy. Ví dụ: nếu bạn thêm hai hàm với nhau, giả sử f + g, hàm giới hạn cho tổng có thể được xây dựng dễ dàng theo cách bảo thủ như: min và max chỉ là tổng của phút và max tương ứng của hai hàm. Để xem điều này là bảo thủ, hãy xem xét các chức năng sin(x)-sin(x)trong 1D. Rõ ràng trên bất kỳ phạm vi nào lớn hơn 2pi, mỗi phạm vi này đều có phạm vi [-1,1] và do đó, phạm vi được tính là [-2,2], nhưng phạm vi thực là khoảng [0,0] chỉ chứa 0! Tuy nhiên, tất cả hy vọng không bị mất: nếu chúng ta phóng to một phạm vi nhỏ cho x, thì khoảng thời gian sẽ nhỏ hơn (gần với [0,0]).

Điều này có nghĩa là nếu bạn có thể ràng buộc một quãng tám tiếng ồn Perlin, thì bạn có thể ràng buộc một tiếng ồn fractal đầy đủ, đó chỉ là tổng của nhiều quãng tám riêng lẻ. Trên thực tế, nếu chúng ta có thể ràng buộc các hàm riêng lẻ, chúng ta có thể sử dụng các quy tắc số học khoảng để xây dựng các hàm thú vị hơn. Đặc biệt để tổng hợp thủ tục, tính tổng, tối thiểu / tối đa, biến dạng miền và chia tỷ lệ theo hằng số đều có thể được xử lý bằng số học khoảng, cho phép chúng ta thêm kết cấu bề mặt, đặt đối tượng và biến dạng & tỷ lệ mà không cần phải viết các hàm giới hạn phức tạp. Vì vậy, chúng ta có thể viết thư f(x) + min(g(x), h(x)) * 4 + h(2*x)cho truy vấn điểm, và tự động (nhưng không chính xác) xây dựng các giới hạn chức năng tương ứng, giả sử chúng ta có chức năng giới hạn cho f, gh.

Cuối cùng, một số chức năng không dễ dàng được thể hiện như một sự kết hợp đơn giản của những người khác. Trong trường hợp đó, bạn thường có thể ràng buộc đạo hàm và sử dụng giá trị đó để ràng buộc giá trị của hàm. Điều này hơi chuyên sâu hơn về toán học, vì vậy tôi sẽ không trình bày ở đây.

Ghi chú thực hiện

Có lẽ dễ nhất để biểu diễn một hàm fbằng một cặp hàm:

  1. một truy vấn điểm f_pointlà một chức năng lấy một điểm đến một float.
  2. một truy vấn giới hạn f_boundsđưa AABB đến một khoảng [min, max].

Sau đó, bạn có thể viết các tổ hợp như Sum, Scale, v.v ... lấy các cặp này và trả về các cặp mới là sự kết hợp đã cho. Bạn có thể biểu diễn chúng dưới dạng tuples, object, v.v. tùy thuộc vào ngôn ngữ.


Đây là một câu trả lời tuyệt vời. Có lẽ bạn có thể làm rõ cách tiếp cận xác định "hàm giới hạn", đối với thứ gì đó giống như quãng tám của nhiễu Perlin? Tôi hiểu rằng tôi luôn có thể chỉ sử dụng mức tối thiểu / tối đa tuyệt đối cho tiếng ồn nhất định của mình, nhưng tôi sẽ làm thế nào để thiết kế một hàm ước tính [min, max] khác nhau cho các khoảng thời gian khác nhau? Tôi tin rằng điều này nằm trong việc giải quyết vấn đề cực đoan cục bộ, nhưng tôi đã hy vọng bạn có thể tham gia vào một câu chuyện khôn ngoan khác. Cảm ơn bạn!
TheVulch

Tôi tin rằng bây giờ tôi đã hiểu ... Tiếng ồn Perlin chỉ là sự kết hợp của nhiều chức năng đơn giản - vì vậy, bằng cách kết hợp các giới hạn trên các chức năng đơn giản này, chúng ta có thể có được một hàm giới hạn phức tạp cho toàn bộ quãng tám. Đây có phải là sự suy nghĩ đúng đắn? Mặc dù vậy, tôi vẫn không biết cách xây dựng một cái gì đó đơn giản như chức năng giới hạn của tội lỗi.
TheVulch

Nhiễu perlin giới hạn là khó khăn vì định nghĩa piecewise (ở các cạnh của tế bào, công thức thay đổi). Đối với tội lỗi, lưu ý rằng giới hạn của bất kỳ chức năng đơn điệu nào (luôn tăng hoặc luôn giảm) được tìm thấy ở các cạnh trong 1 chiều. Do đó, chúng ta có thể chia khoảng thời gian truy vấn thành nhiều phần, mỗi phần cho mỗi đoạn có kích thước pi mà nó bao gồm, trong đó tội lỗi là đơn điệu. Nếu có nhiều hơn hai mảnh, sau đó trả về [-1,1] (nghĩ về lý do tại sao). Mặt khác, chỉ cần trả về mức tối thiểu / tối đa trong số 2/3 điểm cuối (có thể một điểm được chia sẻ). Thủ thuật chia tách truy vấn cũng hoạt động với perlin.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.