| … | … | @@ -13,6 +13,10 @@ struct DCMeshData { |
| 13 | 13 | std::vector<float> positions; // x,y,z triples |
| 14 | 14 | std::vector<float> normals; // x,y,z triples |
| 15 | 15 | std::vector<float> materials; // per-vertex material_id (route ② texturing; empty on CPU path) |
| 16 | // Per-vertex horizontal Laplacian of density, in metres — the third | |
| 17 | // material attribute (altitude, slope, curvature). Computed in qef_solve | |
| 18 | // from the density grid it already reads; empty on the CPU path. | |
| 19 | std::vector<float> curv; | |
| 16 | 20 | std::vector<float> proj; // per-vertex projection index, candidate 0 (Stage-2; empty on CPU path) |
| 17 | 21 | std::vector<float> proj2; // per-vertex projection index, candidate 1 (crease other face; Stage-2) |
| 18 | 22 | std::vector<int> indices; // triangle indices (visual: includes +1 boundary extension) |
| … | … | @@ -878,15 +878,18 @@ void dc_auto_smooth(DCMeshData& mesh, float angle_threshold_degrees) { |
| 878 | 878 | std::vector<float> new_materials; // route ② texturing: material must ride the split |
| 879 | 879 | std::vector<float> new_proj; // Stage-2 per-face projection candidate 0 rides the split |
| 880 | 880 | std::vector<float> new_proj2; // ...and candidate 1 (crease other face) |
| 881 | std::vector<float> new_curv; // curvature rides the split too | |
| 881 | 882 | bool has_mat = ((int)mesh.materials.size() == vert_count); |
| 882 | 883 | bool has_proj = ((int)mesh.proj.size() == vert_count); |
| 883 | 884 | bool has_proj2 = ((int)mesh.proj2.size() == vert_count); |
| 885 | bool has_curv = ((int)mesh.curv.size() == vert_count); | |
| 884 | 886 | std::vector<int> corner_remap(mesh.indices.size(), -1); |
| 885 | 887 | new_positions.reserve(mesh.positions.size()); |
| 886 | 888 | new_normals.reserve(mesh.normals.size()); |
| 887 | 889 | if (has_mat) new_materials.reserve(mesh.materials.size()); |
| 888 | 890 | if (has_proj) new_proj.reserve(mesh.proj.size()); |
| 889 | 891 | if (has_proj2) new_proj2.reserve(mesh.proj2.size()); |
| 892 | if (has_curv) new_curv.reserve(mesh.curv.size()); | |
| 890 | 893 | |
| 891 | 894 | std::vector<bool> visited; |
| 892 | 895 | |
| … | … | @@ -940,6 +943,7 @@ void dc_auto_smooth(DCMeshData& mesh, float angle_threshold_degrees) { |
| 940 | 943 | if (has_mat) new_materials.push_back(mesh.materials[v]); // split inherits material |
| 941 | 944 | if (has_proj) new_proj.push_back(mesh.proj[v]); // ...and both projection candidates |
| 942 | 945 | if (has_proj2) new_proj2.push_back(mesh.proj2[v]); |
| 946 | if (has_curv) new_curv.push_back(mesh.curv[v]); | |
| 943 | 947 | |
| 944 | 948 | std::vector<int> stack; |
| 945 | 949 | stack.push_back(start); |
| … | … | @@ -1030,6 +1034,7 @@ void dc_auto_smooth(DCMeshData& mesh, float angle_threshold_degrees) { |
| 1030 | 1034 | if (has_mat) mesh.materials = std::move(new_materials); |
| 1031 | 1035 | if (has_proj) mesh.proj = std::move(new_proj); |
| 1032 | 1036 | if (has_proj2) mesh.proj2 = std::move(new_proj2); |
| 1037 | if (has_curv) mesh.curv = std::move(new_curv); | |
| 1033 | 1038 | |
| 1034 | 1039 | DC_PRINT("[DCAutoSmooth] %d -> %d verts (%d split, threshold=%.0f deg)\n", |
| 1035 | 1040 | old_verts, new_verts, new_verts - old_verts, angle_threshold_degrees); |
| … | … | @@ -327,9 +327,11 @@ void dc_simplify_meshopt(DCMeshData& mesh, int target_tri_count, bool lock_borde |
| 327 | 327 | bool has_mat = ((int)mesh.materials.size() == vert_count); // route ② texturing |
| 328 | 328 | bool has_proj = ((int)mesh.proj.size() == vert_count); // Stage-2 projection candidate 0 |
| 329 | 329 | bool has_proj2 = ((int)mesh.proj2.size() == vert_count); // Stage-2 projection candidate 1 |
| 330 | bool has_curv = ((int)mesh.curv.size() == vert_count); // curvature attribute | |
| 330 | 331 | std::vector<float> new_materials(has_mat ? new_vert_count : 0); |
| 331 | 332 | std::vector<float> new_proj(has_proj ? new_vert_count : 0); |
| 332 | 333 | std::vector<float> new_proj2(has_proj2 ? new_vert_count : 0); |
| 334 | std::vector<float> new_curv(has_curv ? new_vert_count : 0); | |
| 333 | 335 | for (int v = 0; v < vert_count; v++) { |
| 334 | 336 | if (remap[v] != ~0u) { |
| 335 | 337 | unsigned int nv = remap[v]; |
| … | … | @@ -342,6 +344,7 @@ void dc_simplify_meshopt(DCMeshData& mesh, int target_tri_count, bool lock_borde |
| 342 | 344 | if (has_mat) new_materials[nv] = mesh.materials[v]; |
| 343 | 345 | if (has_proj) new_proj[nv] = mesh.proj[v]; |
| 344 | 346 | if (has_proj2) new_proj2[nv] = mesh.proj2[v]; |
| 347 | if (has_curv) new_curv[nv] = mesh.curv[v]; | |
| 345 | 348 | } |
| 346 | 349 | } |
| 347 | 350 | |
| … | … | @@ -350,6 +353,7 @@ void dc_simplify_meshopt(DCMeshData& mesh, int target_tri_count, bool lock_borde |
| 350 | 353 | if (has_mat) mesh.materials = std::move(new_materials); |
| 351 | 354 | if (has_proj) mesh.proj = std::move(new_proj); |
| 352 | 355 | if (has_proj2) mesh.proj2 = std::move(new_proj2); |
| 356 | if (has_curv) mesh.curv = std::move(new_curv); | |
| 353 | 357 | mesh.indices.resize(result_count); |
| 354 | 358 | for (size_t i = 0; i < result_count; i++) { |
| 355 | 359 | mesh.indices[i] = static_cast<int>(remap[result_indices[i]]); |
| … | … | @@ -742,9 +742,45 @@ void main() { |
| 742 | 742 | } |
| 743 | 743 | |
| 744 | 744 | // ---- Step 7: Write output ---- |
| 745 | // 12 floats per vertex: pos(3) + normal(3) + corner_signs + seam_flag + | |
| 746 | // material_id + voxel_idx + pad(2) | |
| 747 | uint out_idx = voxel_scan[gid] * 12u; | |
| 745 | // 16 floats per vertex: pos(3) + normal(3) + corner_signs + seam_flag + | |
| 746 | // material_id + voxel_idx + proj0 + proj1 + | |
| 747 | // curvature + pad(3) | |
| 748 | // | |
| 749 | // WAS 12, and every slot was taken. Widened rather than reusing one: | |
| 750 | // corner_signs, seam_flag, material_id, voxel_idx and both projection | |
| 751 | // candidates are all live at readback, and a slot that is "probably dead" | |
| 752 | // is how a stride bug gets shipped silently. | |
| 753 | uint out_idx = voxel_scan[gid] * 16u; | |
| 754 | ||
| 755 | // CURVATURE, the third material attribute. | |
| 756 | // | |
| 757 | // terrain-lookdev's computeAlbedo() takes a LAPLACIAN of the heightfield: | |
| 758 | // lap = h[i-1] + h[i+1] + h[i-n] + h[i+n] - 4*h[i] | |
| 759 | // not the erosion filter's ErosionResult.ridge. For a heightfield-derived | |
| 760 | // density (d = h(x,z) - y) the HORIZONTAL Laplacian of d is exactly the | |
| 761 | // Laplacian of h, and this stage already has the density grid — so the | |
| 762 | // faithful quantity costs four extra reads and no new buffer, no change to | |
| 763 | // the density stage, and no field evaluation. | |
| 764 | // | |
| 765 | // Y IS EXCLUDED DELIBERATELY. The full 3-D Laplacian would fold the | |
| 766 | // vertical curvature of the density ramp into a signal that is supposed to | |
| 767 | // describe the ground's shape in plan. | |
| 768 | // | |
| 769 | // RAW, in metres. The lookdev normalises by the 95th percentile of |lap| | |
| 770 | // over its tile, which is not available per chunk on the GPU; the material | |
| 771 | // scales it instead, which also makes it tunable while looking at it. | |
| 772 | float curvature = 0.0; | |
| 773 | { | |
| 774 | int qx = clamp(x, 1, V - 1); | |
| 775 | int qy = clamp(y, 0, V); | |
| 776 | int qz = clamp(z, 1, V - 1); | |
| 777 | float dc = density[density_index(qx, qy, qz, S)]; | |
| 778 | float dxm = density[density_index(qx - 1, qy, qz, S)]; | |
| 779 | float dxp = density[density_index(qx + 1, qy, qz, S)]; | |
| 780 | float dzm = density[density_index(qx, qy, qz - 1, S)]; | |
| 781 | float dzp = density[density_index(qx, qy, qz + 1, S)]; | |
| 782 | curvature = (dxm + dxp + dzm + dzp) - 4.0 * dc; | |
| 783 | } | |
| 748 | 784 | |
| 749 | 785 | vertices[out_idx + 0u] = result.x; |
| 750 | 786 | vertices[out_idx + 1u] = result.y; |
| … | … | @@ -758,4 +794,8 @@ void main() { |
| 758 | 794 | vertices[out_idx + 9u] = uintBitsToFloat(gid); // voxel_idx |
| 759 | 795 | vertices[out_idx + 10u] = uintBitsToFloat(cell_proj0); // per-face projection candidate 0 (Stage-2) |
| 760 | 796 | vertices[out_idx + 11u] = uintBitsToFloat(cell_proj1); // per-face projection candidate 1 (crease other face) |
| 797 | vertices[out_idx + 12u] = curvature; // horizontal Laplacian of density, metres | |
| 798 | vertices[out_idx + 13u] = 0.0; | |
| 799 | vertices[out_idx + 14u] = 0.0; | |
| 800 | vertices[out_idx + 15u] = 0.0; | |
| 761 | 801 | } |
| … | … | @@ -148,10 +148,10 @@ void try_emit_quad(int x, int y, int z, int axis) { |
| 148 | 148 | if ((mesh_quality_flags & 1u) != 0u) { |
| 149 | 149 | // Read vertex positions to compare diagonal lengths. |
| 150 | 150 | // Vertex layout: 12 floats per vertex (pos.xyz at offset 0-2). |
| 151 | vec3 p0 = vec3(vertices[cell_vi[0]*12u], vertices[cell_vi[0]*12u+1u], vertices[cell_vi[0]*12u+2u]); | |
| 152 | vec3 p1 = vec3(vertices[cell_vi[1]*12u], vertices[cell_vi[1]*12u+1u], vertices[cell_vi[1]*12u+2u]); | |
| 153 | vec3 p2 = vec3(vertices[cell_vi[2]*12u], vertices[cell_vi[2]*12u+1u], vertices[cell_vi[2]*12u+2u]); | |
| 154 | vec3 p3 = vec3(vertices[cell_vi[3]*12u], vertices[cell_vi[3]*12u+1u], vertices[cell_vi[3]*12u+2u]); | |
| 151 | vec3 p0 = vec3(vertices[cell_vi[0]*16u], vertices[cell_vi[0]*16u+1u], vertices[cell_vi[0]*16u+2u]); | |
| 152 | vec3 p1 = vec3(vertices[cell_vi[1]*16u], vertices[cell_vi[1]*16u+1u], vertices[cell_vi[1]*16u+2u]); | |
| 153 | vec3 p2 = vec3(vertices[cell_vi[2]*16u], vertices[cell_vi[2]*16u+1u], vertices[cell_vi[2]*16u+2u]); | |
| 154 | vec3 p3 = vec3(vertices[cell_vi[3]*16u], vertices[cell_vi[3]*16u+1u], vertices[cell_vi[3]*16u+2u]); | |
| 155 | 155 | float diag03_sq = dot(p3 - p0, p3 - p0); |
| 156 | 156 | float diag12_sq = dot(p2 - p1, p2 - p1); |
| 157 | 157 | split_02 = (diag12_sq < diag03_sq); |
| … | … | @@ -94,7 +94,7 @@ void main() { |
| 94 | 94 | uint vi = voxel_scan[gid]; |
| 95 | 95 | |
| 96 | 96 | // 12 floats per vertex: pos(3) + normal(3) + corner_signs + seam_flag + material_id + voxel_idx + pad(2) |
| 97 | uint base = vi * 12u; | |
| 97 | uint base = vi * 16u; | |
| 98 | 98 | |
| 99 | 99 | // Read world-space position |
| 100 | 100 | vec3 world_pos = vec3(vertices[base + 0u], vertices[base + 1u], vertices[base + 2u]); |
| … | … | @@ -312,7 +312,7 @@ void DCGpuMeshPipeline::_allocate_buffers(int grid_dims) { |
| 312 | 312 | voxel_flags_buf_ = make_buf(V3 * 4); |
| 313 | 313 | voxel_scan_buf_ = make_buf(V3 * 4); |
| 314 | 314 | active_voxels_buf_ = make_buf(V3 * 4); |
| 315 | vertex_buf_ = make_buf(V3 * 12 * 4); // 12 floats per vertex | |
| 315 | vertex_buf_ = make_buf(V3 * 16 * 4); // 16 floats per vertex (12 + curvature + pad(3)) | |
| 316 | 316 | index_buf_ = make_buf(edge_count * 6 * 4); // 6 indices per edge (worst case) |
| 317 | 317 | counts_buf_ = make_buf(16); // 4 uints |
| 318 | 318 | // dispatch_args needs DISPATCH_INDIRECT usage flag for compute_list_dispatch_indirect |
| … | … | @@ -791,7 +791,7 @@ bool DCGpuMeshPipeline::readback_mesh(DCMeshData& out_mesh) { |
| 791 | 791 | } |
| 792 | 792 | |
| 793 | 793 | // Read vertices (12 floats per vertex) |
| 794 | int vert_bytes = static_cast<int>(active_voxel_count) * 12 * 4; | |
| 794 | int vert_bytes = static_cast<int>(active_voxel_count) * 16 * 4; | |
| 795 | 795 | PackedByteArray vert_data = rd_->buffer_get_data(vertex_buf_, 0, vert_bytes); |
| 796 | 796 | const float* verts = reinterpret_cast<const float*>(vert_data.ptr()); |
| 797 | 797 | |
| … | … | @@ -806,14 +806,16 @@ bool DCGpuMeshPipeline::readback_mesh(DCMeshData& out_mesh) { |
| 806 | 806 | out_mesh.materials.resize(active_voxel_count); // vertex slot 8 (route ② texturing) |
| 807 | 807 | out_mesh.proj.resize(active_voxel_count); // vertex slot 10 (Stage-2 per-face texturing) |
| 808 | 808 | out_mesh.proj2.resize(active_voxel_count); // vertex slot 11 (Stage-2 crease other face) |
| 809 | out_mesh.curv.resize(active_voxel_count); // vertex slot 12 (curvature, metres) | |
| 809 | 810 | uint32_t nan_count = 0; |
| 810 | 811 | for (uint32_t i = 0; i < active_voxel_count; i++) { |
| 811 | const float* v = &verts[i * 12]; | |
| 812 | const float* v = &verts[i * 16]; | |
| 812 | 813 | float px = v[0], py = v[1], pz = v[2]; |
| 813 | 814 | float nx = v[3], ny = v[4], nz = v[5]; |
| 814 | 815 | uint32_t mat = *reinterpret_cast<const uint32_t*>(&v[8]); // material_id (uint bits) |
| 815 | 816 | uint32_t prj = *reinterpret_cast<const uint32_t*>(&v[10]); // projection candidate 0 (uint bits) |
| 816 | 817 | uint32_t prj2 = *reinterpret_cast<const uint32_t*>(&v[11]);// projection candidate 1 (uint bits) |
| 818 | float curv = v[12]; // curvature (float, not bits) | |
| 817 | 819 | if (std::isnan(px) || std::isnan(py) || std::isnan(pz) || |
| 818 | 820 | std::isinf(px) || std::isinf(py) || std::isinf(pz)) { |
| 819 | 821 | px = py = pz = 0.0f; |
| … | … | @@ -821,6 +823,7 @@ bool DCGpuMeshPipeline::readback_mesh(DCMeshData& out_mesh) { |
| 821 | 823 | mat = 0u; |
| 822 | 824 | prj = 0u; |
| 823 | 825 | prj2 = 0u; |
| 826 | curv = 0.0f; | |
| 824 | 827 | nan_count++; |
| 825 | 828 | } |
| 826 | 829 | out_mesh.positions[i * 3 + 0] = px; |
| … | … | @@ -832,6 +835,7 @@ bool DCGpuMeshPipeline::readback_mesh(DCMeshData& out_mesh) { |
| 832 | 835 | out_mesh.materials[i] = static_cast<float>(mat); |
| 833 | 836 | out_mesh.proj[i] = static_cast<float>(prj); |
| 834 | 837 | out_mesh.proj2[i] = static_cast<float>(prj2); |
| 838 | out_mesh.curv[i] = std::isfinite(curv) ? curv : 0.0f; | |
| 835 | 839 | } |
| 836 | 840 | |
| 837 | 841 | // Validate indices: drop out-of-bounds and degenerate triangles. |
| … | … | @@ -2010,18 +2010,32 @@ void DCTerrain::_upload_cell(CellResult& result) { |
| 2010 | 2010 | } |
| 2011 | 2011 | } |
| 2012 | 2012 | |
| 2013 | // CURVATURE -> ARRAY_CUSTOM1 (R_FLOAT). The third material attribute, from | |
| 2014 | // qef_solve's horizontal Laplacian of the density grid. A separate channel | |
| 2015 | // rather than a third component of CUSTOM0: that one is RG_FLOAT and every | |
| 2016 | // shader reading it expects two projection candidates in .xy. | |
| 2017 | PackedFloat32Array curv_array; | |
| 2018 | bool has_curv = (static_cast<int>(mesh_data.curv.size()) == vert_count); | |
| 2019 | if (has_curv) { | |
| 2020 | curv_array.resize(vert_count); | |
| 2021 | float* cvp = curv_array.ptrw(); | |
| 2022 | for (int i = 0; i < vert_count; i++) cvp[i] = mesh_data.curv[i]; | |
| 2023 | } | |
| 2024 | ||
| 2013 | 2025 | Array arrays; |
| 2014 | 2026 | arrays.resize(Mesh::ARRAY_MAX); |
| 2015 | 2027 | arrays[Mesh::ARRAY_VERTEX] = vertices; |
| 2016 | 2028 | arrays[Mesh::ARRAY_NORMAL] = normals_array; |
| 2017 | 2029 | if (has_materials) arrays[Mesh::ARRAY_COLOR] = colors_array; |
| 2018 | 2030 | if (has_proj) arrays[Mesh::ARRAY_CUSTOM0] = proj_array; |
| 2031 | if (has_curv) arrays[Mesh::ARRAY_CUSTOM1] = curv_array; | |
| 2019 | 2032 | arrays[Mesh::ARRAY_INDEX] = indices_array; |
| 2020 | 2033 | |
| 2021 | 2034 | auto t_arrays = std::chrono::high_resolution_clock::now(); |
| 2022 | 2035 | |
| 2023 | 2036 | uint64_t surf_flags = 0; |
| 2024 | 2037 | if (has_proj) surf_flags |= (uint64_t)Mesh::ARRAY_CUSTOM_RG_FLOAT << Mesh::ARRAY_FORMAT_CUSTOM0_SHIFT; |
| 2038 | if (has_curv) surf_flags |= (uint64_t)Mesh::ARRAY_CUSTOM_R_FLOAT << Mesh::ARRAY_FORMAT_CUSTOM1_SHIFT; | |
| 2025 | 2039 | |
| 2026 | 2040 | Ref<ArrayMesh> arr_mesh; |
| 2027 | 2041 | arr_mesh.instantiate(); |