From c03edb786125552d7d92900b720bc5e1578d10ac Mon Sep 17 00:00:00 2001 From: ercmine Date: Sun, 5 Apr 2026 21:02:41 -0500 Subject: [PATCH] Add deterministic procedural mesh toolkit foundation --- CMakeLists.txt | 22 +- docs/architecture.md | 9 +- docs/procgen.md | 109 +++++ engine/procgen/procgen.cpp | 779 ++++++++++++++++++++++++++++++++ engine/procgen/procgen.hpp | 161 +++++++ engine/shell/main.cpp | 121 +++-- tests/procgen/procgen_tests.cpp | 57 +++ 7 files changed, 1206 insertions(+), 52 deletions(-) create mode 100644 docs/procgen.md create mode 100644 engine/procgen/procgen.cpp create mode 100644 engine/procgen/procgen.hpp create mode 100644 tests/procgen/procgen_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e67168..a0d5c35 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -137,6 +137,17 @@ render_apply_project_options(render_renderer) render_apply_warnings(render_renderer) + +add_library(render_procgen STATIC + engine/procgen/procgen.hpp + engine/procgen/procgen.cpp +) +add_library(render::procgen ALIAS render_procgen) +target_include_directories(render_procgen PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(render_procgen PUBLIC render::core render::renderer) +render_apply_project_options(render_procgen) +render_apply_warnings(render_procgen) + add_library(render_scene STATIC engine/scene/scene.hpp engine/scene/scene.cpp @@ -151,7 +162,7 @@ add_library(render_engine STATIC engine/engine.cpp ) add_library(render::engine ALIAS render_engine) -target_link_libraries(render_engine PUBLIC render::core render::filesystem render::platform render::renderer render::serialization render::scene) +target_link_libraries(render_engine PUBLIC render::core render::filesystem render::platform render::renderer render::serialization render::scene render::procgen) render_apply_project_options(render_engine) render_apply_warnings(render_engine) @@ -236,6 +247,11 @@ if(RENDER_BUILD_TESTS) render_apply_project_options(render_render_pass_system_tests) render_apply_warnings(render_render_pass_system_tests) + add_executable(render_procgen_tests tests/procgen/procgen_tests.cpp) + target_link_libraries(render_procgen_tests PRIVATE render::procgen) + render_apply_project_options(render_procgen_tests) + render_apply_warnings(render_procgen_tests) + add_executable(render_scene_tests tests/scene/scene_tests.cpp) target_link_libraries(render_scene_tests PRIVATE render::scene) render_apply_project_options(render_scene_tests) @@ -266,6 +282,8 @@ if(RENDER_BUILD_TESTS) set_tests_properties(unit.renderer.debug_views PROPERTIES LABELS "unit;renderer;debug") add_test(NAME unit.renderer.render_pass_system COMMAND render_render_pass_system_tests) set_tests_properties(unit.renderer.render_pass_system PROPERTIES LABELS "unit;renderer;graph") + add_test(NAME unit.procgen.mesh_toolkit COMMAND render_procgen_tests) + set_tests_properties(unit.procgen.mesh_toolkit PROPERTIES LABELS "unit;procgen") add_test(NAME unit.scene.runtime COMMAND render_scene_tests) set_tests_properties(unit.scene.runtime PROPERTIES LABELS "unit;scene") add_test(NAME unit.renderer.vfx_system COMMAND render_vfx_system_tests) @@ -275,7 +293,7 @@ if(RENDER_BUILD_TESTS) add_custom_target(render_test_unit COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure --label-regex unit - DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_lighting_pipeline_tests render_debug_renderer_tests render_scene_tests render_vfx_system_tests + DEPENDS render_platform_types_tests render_core_runtime_tests render_serialization_tests render_filesystem_tests render_renderer_lifecycle_tests render_shader_pipeline_tests render_geometry_submission_tests render_lighting_pipeline_tests render_debug_renderer_tests render_procgen_tests render_scene_tests render_vfx_system_tests WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" COMMENT "Running render unit tests" ) diff --git a/docs/architecture.md b/docs/architecture.md index bbe1422..6d5b07e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1,4 +1,4 @@ -# Architecture Snapshot (Statements 5-11) +# Architecture Snapshot (Statements 5-17) ## Runtime layering now implemented @@ -40,3 +40,10 @@ - Engine-owned scene graph with stable node handles, transform hierarchy, visibility, and debug names. - Optional camera/light/renderable attachments with traversal and renderer extraction helpers. - Explicit update step for dirty world transform propagation (`Scene::update_world_transforms`). + + +7. **Procedural mesh toolkit (`engine/procgen`)** + - Engine-owned deterministic mesh generation APIs outputting `render::rendering::CpuMeshData`. + - Primitives, spline sampling/extrusion, lathe/revolve, segment assembly, scalar fields, SDF composition, and local surface extraction. + - Mesh validation/stats and post-processing helpers for combining/transforms/re-normals/degenerate cleanup. + - Runtime shell now demonstrates representative generated meshes using the normal renderer submission path. diff --git a/docs/procgen.md b/docs/procgen.md new file mode 100644 index 0000000..7b1466b --- /dev/null +++ b/docs/procgen.md @@ -0,0 +1,109 @@ +# Procedural Mesh Toolkit (Statement 17) + +## Overview + +`engine/procgen` now provides the engine-owned procedural geometry foundation used by runtime generation and test fixtures. It is intentionally renderer-backend agnostic, deterministic when seeded, and emits `render::rendering::CpuMeshData` so generated meshes can flow directly into Statement 12 upload/submission paths. + +## Module organization + +- `engine/procgen/procgen.hpp` + - Public engine API for deterministic mesh generation and helpers. + - Primitive generation APIs (`make_box`, `make_uv_sphere`, `make_capsule`, ...). + - Spline/path sampling and extrusion APIs. + - Lathe/revolve API. + - Segment assembly API with explicit seed. + - Scalar field / SDF authoring and composition APIs. + - Surface extraction API from sampled fields. + - Mesh post-processing, validation, and stats APIs. +- `engine/procgen/procgen.cpp` + - Concrete topology generation implementations and shared mesh utility logic. + +## Determinism and seed usage + +- Segment assembly consumes `render::core::Seed` and uses `render::core::Random` splits per segment. +- Variation (yaw and scale jitter) is explicit per segment descriptor. +- No hidden global RNG is used. +- Same input recipe + same seed => byte-identical generated CPU mesh buffers in tests. +- Floating-point caveat: exact results assume identical compiler/platform floating-point behavior; topology/ordering is deterministic in-engine. + +## Generation modes and conventions + +### Primitives + +Implemented primitives: +- quad +- plane +- box +- UV sphere +- cylinder +- cone +- torus +- capsule + +Conventions: +- Counter-clockwise winding. +- Per-vertex normals and UVs generated. +- Tangent slot is populated with a stable orthonormal basis hook. +- Segment counts are clamped to safe minimums to avoid degenerate topology. + +### Spline/path and extrusion + +- `SplinePath` stores control points and open/closed state. +- Catmull-Rom interpolation is used for sampling. +- Per-sample frame data (tangent/normal/binormal) is derived for extrusion. +- `extrude_profile_along_path` sweeps a 2D profile through the sampled path. +- Supports closed profiles and optional caps. + +### Lathe/revolve + +- `lathe_profile` revolves a 2D radius-height profile around the Y axis. +- Supports full revolve and partial revolve (`angle_radians`). +- Optional start/end caps for partial lathes. + +### Segment assembly + +- `assemble_segments` merges reusable segment meshes using local attachment transforms. +- Optional deterministic yaw/scale jitter per segment. +- Useful for chained bug limbs, organic supports, tails, and decorative modular props. + +### Volumes, SDF, and extraction + +- `ScalarField` provides grid-sampled scalar storage, write, query, and world-space mapping. +- SDF primitives: sphere, box, capsule, cylinder, plane. +- SDF composition: union, subtraction, intersection, smooth union, translate. +- `rasterize_sdf_to_field` samples an arbitrary SDF into a scalar grid. +- `extract_isosurface` uses marching tetrahedra to produce a local mesh from the scalar field. + +## Surface data and post processing + +The toolkit includes practical mesh helpers: +- `merge_meshes` +- `transform_mesh` +- `recalculate_normals` +- `remove_degenerate_triangles` +- `mesh_stats` / bounds +- `validate_proc_mesh` (layout, index range, finite data, degenerate counts) + +## Renderer integration + +Toolkit output is `CpuMeshData` with procgen-owned vertex layout. The shell now uploads generated meshes via normal renderer buffer creation paths and renders: +- primitive mesh +- spline extrusion mesh +- lathed mesh +- segmented assembly mesh +- SDF/volume extracted mesh + +No bgfx/back-end detail is referenced from generation code. + +## Scope and deferred items + +Implemented now: +- deterministic local mesh generation foundation +- practical primitive/extrude/lathe/segment/SDF/volume pipeline +- mesh utility + validation support + +Deferred for future statements: +- authoring/editor UX beyond runtime previews +- higher-order remeshing/subdivision pipelines +- very large terrain/world extraction systems +- advanced UV unwrapping strategies for arbitrary extracted surfaces diff --git a/engine/procgen/procgen.cpp b/engine/procgen/procgen.cpp new file mode 100644 index 0000000..1f47ab7 --- /dev/null +++ b/engine/procgen/procgen.cpp @@ -0,0 +1,779 @@ +#include "engine/procgen/procgen.hpp" + +#include "engine/core/transform.hpp" + +#include +#include +#include +#include +#include +#include + +namespace render::procgen { +namespace { + +constexpr float kPi = 3.14159265358979323846F; + +core::Vec2 operator+(const core::Vec2& a, const core::Vec2& b) { return {a.x + b.x, a.y + b.y}; } +core::Vec2 operator-(const core::Vec2& a, const core::Vec2& b) { return {a.x - b.x, a.y - b.y}; } +core::Vec2 operator*(const core::Vec2& v, float s) { return {v.x * s, v.y * s}; } + +float length2(const core::Vec2& v) { return std::sqrt(v.x * v.x + v.y * v.y); } +core::Vec2 normalize2(const core::Vec2& v) { + const float len = length2(v); + if (len <= core::kEpsilon) return {}; + return {v.x / len, v.y / len}; +} + +core::Vec3 mat_mul_point(const core::Mat4& m, const core::Vec3& p) { return core::transform_point(m, p); } +core::Vec3 mat_mul_dir(const core::Mat4& m, const core::Vec3& v) { + return { + m.m[0] * v.x + m.m[4] * v.y + m.m[8] * v.z, + m.m[1] * v.x + m.m[5] * v.y + m.m[9] * v.z, + m.m[2] * v.x + m.m[6] * v.y + m.m[10] * v.z, + }; +} + +void append_triangle(std::vector& indices, std::uint32_t a, std::uint32_t b, std::uint32_t c) { + indices.push_back(a); + indices.push_back(b); + indices.push_back(c); +} + +ProcVertex make_vertex(const core::Vec3& p, const core::Vec3& n, const core::Vec2& uv) { + const core::Vec3 nn = core::normalize(n); + core::Vec3 tangent = core::normalize(core::cross({0.0F, 1.0F, 0.0F}, nn)); + if (core::length(tangent) <= core::kEpsilon) tangent = {1.0F, 0.0F, 0.0F}; + return {.position = p, .normal = nn, .tangent = {tangent.x, tangent.y, tangent.z, 1.0F}, .uv = uv}; +} + +void append_grid_indices(std::vector& indices, + std::uint32_t rows, + std::uint32_t cols, + bool close_rows, + bool close_cols, + std::uint32_t base_index) { + const std::uint32_t row_end = close_rows ? rows : rows - 1U; + const std::uint32_t col_end = close_cols ? cols : cols - 1U; + for (std::uint32_t r = 0; r < row_end; ++r) { + const std::uint32_t rn = (r + 1U) % rows; + for (std::uint32_t c = 0; c < col_end; ++c) { + const std::uint32_t cn = (c + 1U) % cols; + const std::uint32_t i00 = base_index + r * cols + c; + const std::uint32_t i10 = base_index + rn * cols + c; + const std::uint32_t i11 = base_index + rn * cols + cn; + const std::uint32_t i01 = base_index + r * cols + cn; + append_triangle(indices, i00, i10, i11); + append_triangle(indices, i00, i11, i01); + } + } +} + +core::Vec3 catmull_rom(const core::Vec3& p0, const core::Vec3& p1, const core::Vec3& p2, const core::Vec3& p3, float t) { + const float t2 = t * t; + const float t3 = t2 * t; + const core::Vec3 c0 = p1 * 2.0F; + const core::Vec3 c1 = (p2 - p0) * t; + const core::Vec3 c2 = (p0 * 2.0F - p1 * 5.0F + p2 * 4.0F - p3) * t2; + const core::Vec3 c3 = ((p1 * 3.0F) - (p2 * 3.0F) + p3 - p0) * t3; + return (c0 + c1 + c2 + c3) * + 0.5F; +} + +core::Vec3 sample_position(const SplinePath& path, float t) { + if (path.control_points.empty()) return {}; + if (path.control_points.size() == 1U) return path.control_points.front(); + + const std::size_t count = path.control_points.size(); + const float span = path.closed ? static_cast(count) : static_cast(count - 1U); + const float ft = core::clamp(t, 0.0F, 1.0F) * span; + const int seg = std::min(static_cast(std::floor(ft)), static_cast(span - 1.0F)); + const float local_t = ft - static_cast(seg); + + const auto fetch = [&](int idx) { + if (path.closed) { + int wrapped = idx % static_cast(count); + if (wrapped < 0) wrapped += static_cast(count); + return path.control_points[static_cast(wrapped)]; + } + return path.control_points[static_cast(core::clamp(idx, 0, static_cast(count) - 1))]; + }; + + return catmull_rom(fetch(seg - 1), fetch(seg), fetch(seg + 1), fetch(seg + 2), local_t); +} + +float sdf_box_impl(const core::Vec3& p, const core::Vec3& b) { + const core::Vec3 q{std::fabs(p.x) - b.x, std::fabs(p.y) - b.y, std::fabs(p.z) - b.z}; + const core::Vec3 maxq{std::max(q.x, 0.0F), std::max(q.y, 0.0F), std::max(q.z, 0.0F)}; + const float outside = core::length(maxq); + const float inside = std::min(std::max(q.x, std::max(q.y, q.z)), 0.0F); + return outside + inside; +} + +} // namespace + +rendering::VertexLayoutDescription proc_vertex_layout() { + rendering::VertexLayoutDescription layout{}; + layout.stride = static_cast(sizeof(ProcVertex)); + layout.elements = { + {rendering::VertexAttribute::Position, 0, 3, rendering::AttributeType::Float, false, false}, + {rendering::VertexAttribute::Normal, 12, 3, rendering::AttributeType::Float, false, false}, + {rendering::VertexAttribute::Tangent, 24, 4, rendering::AttributeType::Float, false, false}, + {rendering::VertexAttribute::TexCoord0, 40, 2, rendering::AttributeType::Float, false, false}, + }; + return layout; +} + +rendering::CpuMeshData make_cpu_mesh(std::span vertices, std::span indices) { + rendering::CpuMeshData mesh{}; + mesh.layout = proc_vertex_layout(); + mesh.vertex_count = static_cast(vertices.size()); + mesh.vertex_data.resize(vertices.size_bytes()); + if (!vertices.empty()) std::memcpy(mesh.vertex_data.data(), vertices.data(), vertices.size_bytes()); + mesh.index_type = rendering::IndexType::Uint32; + mesh.index_count = static_cast(indices.size()); + mesh.index_data.resize(indices.size_bytes()); + if (!indices.empty()) std::memcpy(mesh.index_data.data(), indices.data(), indices.size_bytes()); + return mesh; +} + +std::vector unpack_vertices(const rendering::CpuMeshData& mesh) { + if (mesh.layout.stride != sizeof(ProcVertex)) return {}; + std::vector vertices(mesh.vertex_count); + if (!vertices.empty()) std::memcpy(vertices.data(), mesh.vertex_data.data(), mesh.vertex_data.size()); + return vertices; +} + +std::vector unpack_indices_u32(const rendering::CpuMeshData& mesh) { + std::vector indices(mesh.index_count); + if (mesh.index_type == rendering::IndexType::Uint32) { + if (!indices.empty()) std::memcpy(indices.data(), mesh.index_data.data(), mesh.index_data.size()); + } else { + const auto* src = reinterpret_cast(mesh.index_data.data()); + for (std::uint32_t i = 0; i < mesh.index_count; ++i) indices[i] = src[i]; + } + return indices; +} + +MeshValidationReport validate_proc_mesh(const rendering::CpuMeshData& mesh) { + const auto base = rendering::validate_cpu_mesh_data(mesh); + if (!base.valid) return {.valid = false, .reason = base.reason}; + if (mesh.layout.stride != sizeof(ProcVertex)) return {.valid = false, .reason = "proc mesh requires ProcVertex layout"}; + const auto vertices = unpack_vertices(mesh); + const auto indices = unpack_indices_u32(mesh); + std::uint32_t degenerate = 0; + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) { + if (indices[i] >= vertices.size() || indices[i + 1] >= vertices.size() || indices[i + 2] >= vertices.size()) { + return {.valid = false, .reason = "mesh index out of range"}; + } + const core::Vec3 a = vertices[indices[i]].position; + const core::Vec3 b = vertices[indices[i + 1]].position; + const core::Vec3 c = vertices[indices[i + 2]].position; + const float area2 = core::length(core::cross(b - a, c - a)); + if (!std::isfinite(area2)) return {.valid = false, .reason = "mesh contains NaN/Inf geometry"}; + if (area2 <= 1.0e-8F) ++degenerate; + } + return {.valid = true, .reason = {}, .degenerate_triangles = degenerate}; +} + +Bounds compute_bounds(std::span vertices) { + Bounds b{}; + if (vertices.empty()) return b; + b.min = b.max = vertices.front().position; + for (const auto& v : vertices) { + b.min.x = std::min(b.min.x, v.position.x); + b.min.y = std::min(b.min.y, v.position.y); + b.min.z = std::min(b.min.z, v.position.z); + b.max.x = std::max(b.max.x, v.position.x); + b.max.y = std::max(b.max.y, v.position.y); + b.max.z = std::max(b.max.z, v.position.z); + } + return b; +} + +rendering::CpuMeshData make_quad(const float width, const float height) { + const float hx = std::max(width, core::kEpsilon) * 0.5F; + const float hy = std::max(height, core::kEpsilon) * 0.5F; + std::vector v = { + make_vertex({-hx, -hy, 0.0F}, {0.0F, 0.0F, 1.0F}, {0.0F, 1.0F}), + make_vertex({hx, -hy, 0.0F}, {0.0F, 0.0F, 1.0F}, {1.0F, 1.0F}), + make_vertex({hx, hy, 0.0F}, {0.0F, 0.0F, 1.0F}, {1.0F, 0.0F}), + make_vertex({-hx, hy, 0.0F}, {0.0F, 0.0F, 1.0F}, {0.0F, 0.0F}), + }; + const std::vector i = {0, 1, 2, 0, 2, 3}; + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_plane(const float width, const float depth, const std::uint32_t width_segments, const std::uint32_t depth_segments) { + const std::uint32_t ws = std::max(1U, width_segments); + const std::uint32_t ds = std::max(1U, depth_segments); + const float hw = std::max(width, core::kEpsilon) * 0.5F; + const float hd = std::max(depth, core::kEpsilon) * 0.5F; + std::vector v; + std::vector i; + v.reserve((ws + 1U) * (ds + 1U)); + for (std::uint32_t z = 0; z <= ds; ++z) { + const float vz = static_cast(z) / static_cast(ds); + for (std::uint32_t x = 0; x <= ws; ++x) { + const float ux = static_cast(x) / static_cast(ws); + v.push_back(make_vertex({core::lerp(-hw, hw, ux), 0.0F, core::lerp(-hd, hd, vz)}, {0.0F, 1.0F, 0.0F}, {ux, vz})); + } + } + append_grid_indices(i, ds + 1U, ws + 1U, false, false, 0); + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_box(const core::Vec3& size, const std::uint32_t segments) { + const float hx = std::max(size.x, core::kEpsilon) * 0.5F; + const float hy = std::max(size.y, core::kEpsilon) * 0.5F; + const float hz = std::max(size.z, core::kEpsilon) * 0.5F; + const auto front = make_plane(size.x, size.y, segments, segments); + const auto back = transform_mesh(front, core::Mat4::translation({0.0F, 0.0F, -2.0F * hz}) * core::Mat4::rotation(core::Quaternion::from_axis_angle({0, 1, 0}, kPi)) * core::Mat4::translation({0.0F, 0.0F, hz})); + const auto top = transform_mesh(make_plane(size.x, size.z, segments, segments), core::Mat4::translation({0.0F, hy, 0.0F})); + const auto bottom = transform_mesh(top, core::Mat4::rotation(core::Quaternion::from_axis_angle({1, 0, 0}, kPi))); + const auto right = transform_mesh(make_plane(size.z, size.y, segments, segments), core::Mat4::rotation(core::Quaternion::from_axis_angle({0, 1, 0}, kPi * 0.5F)) * core::Mat4::translation({hx, 0.0F, 0.0F})); + const auto left = transform_mesh(right, core::Mat4::rotation(core::Quaternion::from_axis_angle({0, 1, 0}, kPi))); + std::array faces{transform_mesh(front, core::Mat4::translation({0.0F, 0.0F, hz})), back, top, bottom, right, left}; + return merge_meshes(faces); +} + +rendering::CpuMeshData make_uv_sphere(const float radius, const std::uint32_t lat_segments, const std::uint32_t lon_segments) { + const std::uint32_t lat = std::max(3U, lat_segments); + const std::uint32_t lon = std::max(3U, lon_segments); + const float r = std::max(radius, core::kEpsilon); + std::vector v; + std::vector i; + v.reserve((lat + 1U) * (lon + 1U)); + for (std::uint32_t y = 0; y <= lat; ++y) { + const float fy = static_cast(y) / static_cast(lat); + const float theta = fy * kPi; + const float st = std::sin(theta); + const float ct = std::cos(theta); + for (std::uint32_t x = 0; x <= lon; ++x) { + const float fx = static_cast(x) / static_cast(lon); + const float phi = fx * core::kTwoPi; + const core::Vec3 n{std::cos(phi) * st, ct, std::sin(phi) * st}; + v.push_back(make_vertex(n * r, n, {fx, fy})); + } + } + append_grid_indices(i, lat + 1U, lon + 1U, false, false, 0); + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_cylinder(const float radius, + const float height, + const std::uint32_t radial_segments, + const std::uint32_t height_segments, + const bool capped) { + const std::uint32_t rs = std::max(3U, radial_segments); + const std::uint32_t hs = std::max(1U, height_segments); + const float r = std::max(radius, core::kEpsilon); + const float hh = std::max(height, core::kEpsilon) * 0.5F; + std::vector v; + std::vector i; + for (std::uint32_t y = 0; y <= hs; ++y) { + const float fy = static_cast(y) / static_cast(hs); + const float py = core::lerp(-hh, hh, fy); + for (std::uint32_t s = 0; s <= rs; ++s) { + const float u = static_cast(s) / static_cast(rs); + const float a = u * core::kTwoPi; + const core::Vec3 n{std::cos(a), 0.0F, std::sin(a)}; + v.push_back(make_vertex({n.x * r, py, n.z * r}, n, {u, fy})); + } + } + append_grid_indices(i, hs + 1U, rs + 1U, false, false, 0); + + if (capped) { + auto append_cap = [&](float y, float ny, bool flip) { + const std::uint32_t center = static_cast(v.size()); + v.push_back(make_vertex({0.0F, y, 0.0F}, {0.0F, ny, 0.0F}, {0.5F, 0.5F})); + for (std::uint32_t s = 0; s <= rs; ++s) { + const float u = static_cast(s) / static_cast(rs); + const float a = u * core::kTwoPi; + const float cx = std::cos(a); + const float cz = std::sin(a); + v.push_back(make_vertex({cx * r, y, cz * r}, {0.0F, ny, 0.0F}, {cx * 0.5F + 0.5F, cz * 0.5F + 0.5F})); + } + for (std::uint32_t s = 0; s < rs; ++s) { + const std::uint32_t a0 = center + 1U + s; + const std::uint32_t a1 = center + 1U + s + 1U; + if (flip) { + append_triangle(i, center, a0, a1); + } else { + append_triangle(i, center, a1, a0); + } + } + }; + append_cap(-hh, -1.0F, true); + append_cap(hh, 1.0F, false); + } + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_cone(float radius, float height, std::uint32_t radial_segments, bool capped) { + const std::uint32_t rs = std::max(3U, radial_segments); + const float r = std::max(radius, core::kEpsilon); + const float h = std::max(height, core::kEpsilon); + std::vector v; + std::vector i; + const core::Vec3 tip{0.0F, h * 0.5F, 0.0F}; + const float y0 = -h * 0.5F; + for (std::uint32_t s = 0; s <= rs; ++s) { + const float u = static_cast(s) / static_cast(rs); + const float a = u * core::kTwoPi; + const core::Vec3 p{std::cos(a) * r, y0, std::sin(a) * r}; + const core::Vec3 edge = core::normalize(tip - p); + const core::Vec3 tan{ -std::sin(a), 0.0F, std::cos(a)}; + const core::Vec3 n = core::normalize(core::cross(tan, edge)); + v.push_back(make_vertex(p, n, {u, 1.0F})); + v.push_back(make_vertex(tip, n, {u, 0.0F})); + } + for (std::uint32_t s = 0; s < rs; ++s) { + const std::uint32_t b = s * 2U; + append_triangle(i, b, b + 1U, b + 2U); + append_triangle(i, b + 1U, b + 3U, b + 2U); + } + if (capped) { + const std::uint32_t center = static_cast(v.size()); + v.push_back(make_vertex({0.0F, y0, 0.0F}, {0.0F, -1.0F, 0.0F}, {0.5F, 0.5F})); + for (std::uint32_t s = 0; s <= rs; ++s) { + const float u = static_cast(s) / static_cast(rs); + const float a = u * core::kTwoPi; + const float cx = std::cos(a); + const float cz = std::sin(a); + v.push_back(make_vertex({cx * r, y0, cz * r}, {0.0F, -1.0F, 0.0F}, {cx * 0.5F + 0.5F, cz * 0.5F + 0.5F})); + } + for (std::uint32_t s = 0; s < rs; ++s) append_triangle(i, center, center + 2U + s, center + 1U + s); + } + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_torus(float major_radius, float minor_radius, std::uint32_t major_segments, std::uint32_t minor_segments) { + const std::uint32_t ms = std::max(3U, major_segments); + const std::uint32_t ns = std::max(3U, minor_segments); + const float r0 = std::max(major_radius, core::kEpsilon * 2.0F); + const float r1 = std::max(minor_radius, core::kEpsilon); + std::vector v; + std::vector i; + for (std::uint32_t j = 0; j <= ns; ++j) { + const float vj = static_cast(j) / static_cast(ns); + const float b = vj * core::kTwoPi; + const float cb = std::cos(b); + const float sb = std::sin(b); + for (std::uint32_t k = 0; k <= ms; ++k) { + const float uk = static_cast(k) / static_cast(ms); + const float a = uk * core::kTwoPi; + const float ca = std::cos(a); + const float sa = std::sin(a); + const core::Vec3 center{ca * r0, 0.0F, sa * r0}; + const core::Vec3 n{ca * cb, sb, sa * cb}; + v.push_back(make_vertex(center + n * r1, n, {uk, vj})); + } + } + append_grid_indices(i, ns + 1U, ms + 1U, false, false, 0); + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData make_capsule(float radius, float height, std::uint32_t radial_segments, std::uint32_t hemi_segments) { + const auto body = make_cylinder(radius, std::max(height - 2.0F * radius, radius), radial_segments, 1, false); + const auto sphere = make_uv_sphere(radius, hemi_segments * 2U, radial_segments); + const auto top = transform_mesh(remove_degenerate_triangles(sphere), core::Mat4::translation({0.0F, std::max(height * 0.5F - radius, 0.0F), 0.0F})); + const auto bottom = transform_mesh(remove_degenerate_triangles(sphere), core::Mat4::rotation(core::Quaternion::from_axis_angle({1, 0, 0}, kPi)) * core::Mat4::translation({0.0F, -std::max(height * 0.5F - radius, 0.0F), 0.0F})); + std::array parts{body, top, bottom}; + return remove_degenerate_triangles(merge_meshes(parts)); +} + +PathPoint sample_path(const SplinePath& path, float t) { + PathPoint out{}; + out.position = sample_position(path, t); + const float dt = 1.0F / 512.0F; + const core::Vec3 p0 = sample_position(path, std::max(0.0F, t - dt)); + const core::Vec3 p1 = sample_position(path, std::min(1.0F, t + dt)); + out.tangent = core::normalize(p1 - p0); + core::Vec3 up{0.0F, 1.0F, 0.0F}; + if (std::fabs(core::dot(up, out.tangent)) > 0.9F) up = {1.0F, 0.0F, 0.0F}; + out.binormal = core::normalize(core::cross(out.tangent, up)); + out.normal = core::normalize(core::cross(out.binormal, out.tangent)); + out.u = t; + return out; +} + +std::vector sample_path_uniform(const SplinePath& path, const std::uint32_t samples) { + const std::uint32_t count = std::max(2U, samples); + std::vector pts; + pts.reserve(count); + for (std::uint32_t i = 0; i < count; ++i) { + const float t = static_cast(i) / static_cast(count - 1U); + pts.push_back(sample_path(path, t)); + } + return pts; +} + +rendering::CpuMeshData extrude_profile_along_path(std::span profile, + const SplinePath& path, + const ExtrudeOptions& options) { + if (profile.size() < 3 || path.control_points.size() < 2) return {}; + const auto path_points = sample_path_uniform(path, options.path_samples); + const std::uint32_t rings = static_cast(path_points.size()); + const std::uint32_t profile_count = static_cast(profile.size()); + std::vector v; + std::vector i; + v.reserve(rings * profile_count); + + for (std::uint32_t r = 0; r < rings; ++r) { + const auto& frame = path_points[r]; + for (std::uint32_t p = 0; p < profile_count; ++p) { + const core::Vec2 pp = profile[p]; + const core::Vec3 pos = frame.position + frame.normal * pp.x + frame.binormal * pp.y; + const core::Vec2 prev = profile[(p + profile_count - 1U) % profile_count]; + const core::Vec2 next = profile[(p + 1U) % profile_count]; + const core::Vec2 edge = normalize2(next - prev); + const core::Vec3 radial = core::normalize(frame.normal * edge.y - frame.binormal * edge.x); + v.push_back(make_vertex(pos, radial, {static_cast(p) / static_cast(profile_count), frame.u})); + } + } + append_grid_indices(i, rings, profile_count, false, options.closed_profile, 0); + + if (options.cap_ends) { + auto append_cap = [&](std::uint32_t ring, bool flip) { + const std::uint32_t center = static_cast(v.size()); + core::Vec3 sum{}; + for (std::uint32_t p = 0; p < profile_count; ++p) sum = sum + v[ring * profile_count + p].position; + sum = sum / static_cast(profile_count); + const core::Vec3 cap_normal = flip ? path_points[ring].tangent * -1.0F : path_points[ring].tangent; + v.push_back(make_vertex(sum, cap_normal, {0.5F, 0.5F})); + for (std::uint32_t p = 0; p < profile_count; ++p) { + const std::uint32_t a = ring * profile_count + p; + const std::uint32_t b = ring * profile_count + ((p + 1U) % profile_count); + if (flip) append_triangle(i, center, b, a); + else append_triangle(i, center, a, b); + } + }; + append_cap(0, true); + append_cap(rings - 1U, false); + } + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData lathe_profile(std::span profile, const LatheOptions& options) { + if (profile.size() < 2) return {}; + const std::uint32_t radial = std::max(3U, options.radial_segments); + const bool closed = std::fabs(options.angle_radians - core::kTwoPi) <= 1.0e-4F; + std::vector v; + std::vector i; + const std::uint32_t rings = closed ? radial : (radial + 1U); + + for (std::uint32_t r = 0; r < rings; ++r) { + const float u = static_cast(r) / static_cast(radial); + const float a = options.angle_radians * u; + const float ca = std::cos(a); + const float sa = std::sin(a); + for (std::uint32_t p = 0; p < profile.size(); ++p) { + const float rr = std::max(profile[p].x, 0.0F); + const float yy = profile[p].y; + const core::Vec3 pos{ca * rr, yy, sa * rr}; + const core::Vec2 prev = profile[p == 0 ? p : p - 1U]; + const std::size_t next_idx = std::min(profile.size() - 1U, static_cast(p + 1U)); + const core::Vec2 next = profile[next_idx]; + const core::Vec2 deriv = normalize2(next - prev); + const core::Vec3 n = core::normalize(core::Vec3{ca * deriv.y, -deriv.x, sa * deriv.y}); + v.push_back(make_vertex(pos, n, {u, static_cast(p) / static_cast(profile.size() - 1U)})); + } + } + append_grid_indices(i, rings, static_cast(profile.size()), closed, false, 0); + + auto add_partial_cap = [&](bool end_cap) { + const std::uint32_t ring = end_cap ? rings - 1U : 0U; + const std::uint32_t center = static_cast(v.size()); + core::Vec3 centroid{}; + for (std::uint32_t p = 0; p < profile.size(); ++p) centroid = centroid + v[ring * profile.size() + p].position; + centroid = centroid / static_cast(profile.size()); + const float sign = end_cap ? 1.0F : -1.0F; + v.push_back(make_vertex(centroid, {std::sin(options.angle_radians) * sign, 0.0F, -std::cos(options.angle_radians) * sign}, {0.5F, 0.5F})); + for (std::uint32_t p = 0; p + 1U < profile.size(); ++p) { + const std::uint32_t a = ring * profile.size() + p; + const std::uint32_t b = ring * profile.size() + p + 1U; + if (end_cap) append_triangle(i, center, a, b); + else append_triangle(i, center, b, a); + } + }; + + if (!closed && options.cap_start) add_partial_cap(false); + if (!closed && options.cap_end) add_partial_cap(true); + + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData assemble_segments(std::span segments, const SegmentAssemblyOptions& options) { + if (segments.empty()) return {}; + core::Random rng(options.seed); + std::vector transformed; + transformed.reserve(segments.size()); + core::Transform running{}; + for (std::size_t s = 0; s < segments.size(); ++s) { + const SegmentDescriptor& seg = segments[s]; + const core::Seed seg_seed = rng.split("segment"); + core::Random local(seg_seed); + + core::Transform jitter = seg.local_from_prev; + jitter.rotation = core::Quaternion::from_axis_angle({0.0F, 1.0F, 0.0F}, local.range_f32(-seg.yaw_jitter_radians, seg.yaw_jitter_radians)) * + jitter.rotation; + const float scale = 1.0F + local.range_f32(-seg.scale_jitter, seg.scale_jitter); + jitter.scale = jitter.scale * scale; + + running = (s == 0U) ? jitter : core::compose(running, jitter); + transformed.push_back(transform_mesh(seg.mesh, running.to_matrix())); + } + return merge_meshes(transformed); +} + +ScalarField::ScalarField(const std::uint32_t nx, const std::uint32_t ny, const std::uint32_t nz, const float cell_size, const core::Vec3& origin) + : nx_(std::max(2U, nx)), ny_(std::max(2U, ny)), nz_(std::max(2U, nz)), cell_size_(std::max(cell_size, core::kEpsilon)), origin_(origin), + data_(static_cast(nx_) * ny_ * nz_, 1.0F) {} + +std::size_t ScalarField::index(const std::uint32_t x, const std::uint32_t y, const std::uint32_t z) const { + return static_cast(z) * nx_ * ny_ + static_cast(y) * nx_ + x; +} + +float ScalarField::at(const std::uint32_t x, const std::uint32_t y, const std::uint32_t z) const { + return data_[index(std::min(x, nx_ - 1U), std::min(y, ny_ - 1U), std::min(z, nz_ - 1U))]; +} + +void ScalarField::set(const std::uint32_t x, const std::uint32_t y, const std::uint32_t z, const float value) { + data_[index(std::min(x, nx_ - 1U), std::min(y, ny_ - 1U), std::min(z, nz_ - 1U))] = value; +} + +void ScalarField::fill(const float value) { + std::fill(data_.begin(), data_.end(), value); +} + +core::Vec3 ScalarField::cell_position(const std::uint32_t x, const std::uint32_t y, const std::uint32_t z) const { + return {origin_.x + static_cast(x) * cell_size_, origin_.y + static_cast(y) * cell_size_, + origin_.z + static_cast(z) * cell_size_}; +} + +SignedDistanceFn sdf_sphere(core::Vec3 center, float radius) { + radius = std::max(radius, core::kEpsilon); + return [center, radius](const core::Vec3& p) { return core::length(p - center) - radius; }; +} + +SignedDistanceFn sdf_box(core::Vec3 center, core::Vec3 half_extents) { + half_extents.x = std::max(half_extents.x, core::kEpsilon); + half_extents.y = std::max(half_extents.y, core::kEpsilon); + half_extents.z = std::max(half_extents.z, core::kEpsilon); + return [center, half_extents](const core::Vec3& p) { return sdf_box_impl(p - center, half_extents); }; +} + +SignedDistanceFn sdf_capsule(core::Vec3 a, core::Vec3 b, float radius) { + radius = std::max(radius, core::kEpsilon); + return [a, b, radius](const core::Vec3& p) { + const core::Vec3 pa = p - a; + const core::Vec3 ba = b - a; + const float h = core::clamp(core::dot(pa, ba) / std::max(core::dot(ba, ba), core::kEpsilon), 0.0F, 1.0F); + return core::length(pa - ba * h) - radius; + }; +} + +SignedDistanceFn sdf_cylinder(core::Vec3 center, float radius, float half_height) { + radius = std::max(radius, core::kEpsilon); + half_height = std::max(half_height, core::kEpsilon); + return [center, radius, half_height](const core::Vec3& p) { + const core::Vec3 d = p - center; + const core::Vec2 q{length2({d.x, d.z}) - radius, std::fabs(d.y) - half_height}; + return std::min(std::max(q.x, q.y), 0.0F) + length2({std::max(q.x, 0.0F), std::max(q.y, 0.0F)}); + }; +} + +SignedDistanceFn sdf_plane(core::Vec3 normal, float offset) { + normal = core::normalize(normal); + return [normal, offset](const core::Vec3& p) { return core::dot(normal, p) + offset; }; +} + +SignedDistanceFn sdf_translate(SignedDistanceFn field, core::Vec3 offset) { + return [field = std::move(field), offset](const core::Vec3& p) { return field(p - offset); }; +} + +SignedDistanceFn sdf_union(SignedDistanceFn a, SignedDistanceFn b) { + return [a = std::move(a), b = std::move(b)](const core::Vec3& p) { return std::min(a(p), b(p)); }; +} + +SignedDistanceFn sdf_subtract(SignedDistanceFn a, SignedDistanceFn b) { + return [a = std::move(a), b = std::move(b)](const core::Vec3& p) { return std::max(a(p), -b(p)); }; +} + +SignedDistanceFn sdf_intersect(SignedDistanceFn a, SignedDistanceFn b) { + return [a = std::move(a), b = std::move(b)](const core::Vec3& p) { return std::max(a(p), b(p)); }; +} + +SignedDistanceFn sdf_smooth_union(SignedDistanceFn a, SignedDistanceFn b, float k) { + k = std::max(k, core::kEpsilon); + return [a = std::move(a), b = std::move(b), k](const core::Vec3& p) { + const float da = a(p); + const float db = b(p); + const float h = core::clamp(0.5F + 0.5F * (db - da) / k, 0.0F, 1.0F); + return core::lerp(db, da, h) - k * h * (1.0F - h); + }; +} + +void rasterize_sdf_to_field(const SignedDistanceFn& sdf, ScalarField& field) { + for (std::uint32_t z = 0; z < field.nz(); ++z) { + for (std::uint32_t y = 0; y < field.ny(); ++y) { + for (std::uint32_t x = 0; x < field.nx(); ++x) { + field.set(x, y, z, sdf(field.cell_position(x, y, z))); + } + } + } +} + +rendering::CpuMeshData extract_isosurface(const ScalarField& field, const float iso_value) { + static constexpr std::array, 6> kTets = {{{0, 5, 1, 6}, {0, 1, 2, 6}, {0, 2, 3, 6}, {0, 3, 7, 6}, {0, 7, 4, 6}, {0, 4, 5, 6}}}; + static constexpr std::array kOff = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}, {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}}}; + + auto interpolate = [&](const core::Vec3& p0, const core::Vec3& p1, float v0, float v1) { + const float denom = (v1 - v0); + const float t = std::fabs(denom) <= core::kEpsilon ? 0.5F : core::clamp((iso_value - v0) / denom, 0.0F, 1.0F); + return p0 + (p1 - p0) * t; + }; + + std::vector verts; + std::vector indices; + + for (std::uint32_t z = 0; z + 1U < field.nz(); ++z) { + for (std::uint32_t y = 0; y + 1U < field.ny(); ++y) { + for (std::uint32_t x = 0; x + 1U < field.nx(); ++x) { + std::array p{}; + std::array v{}; + for (std::size_t c = 0; c < 8; ++c) { + p[c] = field.cell_position(x + static_cast(kOff[c].x), y + static_cast(kOff[c].y), + z + static_cast(kOff[c].z)); + v[c] = field.at(x + static_cast(kOff[c].x), y + static_cast(kOff[c].y), + z + static_cast(kOff[c].z)); + } + + for (const auto& tet : kTets) { + std::array inside{}; + int inside_count = 0; + for (int t = 0; t < 4; ++t) { + if (v[tet[t]] <= iso_value) inside[inside_count++] = t; + } + if (inside_count == 0 || inside_count == 4) continue; + + auto point = [&](int a, int b) { return interpolate(p[tet[a]], p[tet[b]], v[tet[a]], v[tet[b]]); }; + auto add_tri = [&](const core::Vec3& a, const core::Vec3& b, const core::Vec3& c) { + const core::Vec3 n = core::normalize(core::cross(b - a, c - a)); + const std::uint32_t base = static_cast(verts.size()); + verts.push_back(make_vertex(a, n, {0.0F, 0.0F})); + verts.push_back(make_vertex(b, n, {1.0F, 0.0F})); + verts.push_back(make_vertex(c, n, {0.0F, 1.0F})); + append_triangle(indices, base, base + 1U, base + 2U); + }; + + if (inside_count == 1 || inside_count == 3) { + int i0 = -1; + std::array o{}; + int oi = 0; + if (inside_count == 1) { + i0 = inside[0]; + for (int t = 0; t < 4; ++t) if (t != i0) o[oi++] = t; + add_tri(point(i0, o[0]), point(i0, o[1]), point(i0, o[2])); + } else { + std::array is_inside{false, false, false, false}; + for (int t = 0; t < 3; ++t) is_inside[inside[t]] = true; + for (int t = 0; t < 4; ++t) if (!is_inside[t]) i0 = t; + for (int t = 0; t < 4; ++t) if (t != i0) o[oi++] = t; + add_tri(point(i0, o[0]), point(i0, o[2]), point(i0, o[1])); + } + } else { + std::array in{}; + std::array out{}; + int oo = 0; + std::array is_inside{false, false, false, false}; + for (int t = 0; t < 2; ++t) { + in[t] = inside[t]; + is_inside[inside[t]] = true; + } + for (int t = 0; t < 4; ++t) { + if (!is_inside[t]) out[oo++] = t; + } + const core::Vec3 a = point(in[0], out[0]); + const core::Vec3 b = point(in[0], out[1]); + const core::Vec3 c = point(in[1], out[0]); + const core::Vec3 d = point(in[1], out[1]); + add_tri(a, b, c); + add_tri(c, b, d); + } + } + } + } + } + return remove_degenerate_triangles(make_cpu_mesh(verts, indices)); +} + +rendering::CpuMeshData merge_meshes(std::span meshes) { + std::vector out_v; + std::vector out_i; + std::uint32_t base = 0; + for (const auto& mesh : meshes) { + if (mesh.vertex_count == 0 || mesh.index_count == 0) continue; + const auto v = unpack_vertices(mesh); + const auto i = unpack_indices_u32(mesh); + out_v.insert(out_v.end(), v.begin(), v.end()); + out_i.reserve(out_i.size() + i.size()); + for (const auto idx : i) out_i.push_back(base + idx); + base += static_cast(v.size()); + } + return make_cpu_mesh(out_v, out_i); +} + +rendering::CpuMeshData transform_mesh(const rendering::CpuMeshData& mesh, const core::Mat4& transform) { + auto v = unpack_vertices(mesh); + const auto i = unpack_indices_u32(mesh); + for (auto& vx : v) { + vx.position = mat_mul_point(transform, vx.position); + vx.normal = core::normalize(mat_mul_dir(transform, vx.normal)); + const core::Vec3 t{vx.tangent.x, vx.tangent.y, vx.tangent.z}; + const core::Vec3 nt = core::normalize(mat_mul_dir(transform, t)); + vx.tangent = {nt.x, nt.y, nt.z, vx.tangent.w}; + } + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData recalculate_normals(const rendering::CpuMeshData& mesh) { + auto v = unpack_vertices(mesh); + const auto i = unpack_indices_u32(mesh); + for (auto& vv : v) vv.normal = {0.0F, 0.0F, 0.0F}; + for (std::size_t t = 0; t + 2 < i.size(); t += 3) { + const auto a = i[t]; + const auto b = i[t + 1]; + const auto c = i[t + 2]; + const core::Vec3 n = core::cross(v[b].position - v[a].position, v[c].position - v[a].position); + v[a].normal = v[a].normal + n; + v[b].normal = v[b].normal + n; + v[c].normal = v[c].normal + n; + } + for (auto& vv : v) vv.normal = core::normalize(vv.normal); + return make_cpu_mesh(v, i); +} + +rendering::CpuMeshData remove_degenerate_triangles(const rendering::CpuMeshData& mesh, const float epsilon) { + const auto v = unpack_vertices(mesh); + const auto i = unpack_indices_u32(mesh); + std::vector filtered; + filtered.reserve(i.size()); + for (std::size_t t = 0; t + 2 < i.size(); t += 3) { + const core::Vec3 a = v[i[t]].position; + const core::Vec3 b = v[i[t + 1]].position; + const core::Vec3 c = v[i[t + 2]].position; + if (core::length(core::cross(b - a, c - a)) > epsilon) { + append_triangle(filtered, i[t], i[t + 1], i[t + 2]); + } + } + return make_cpu_mesh(v, filtered); +} + +MeshStats mesh_stats(const rendering::CpuMeshData& mesh) { + const auto vertices = unpack_vertices(mesh); + return {.vertex_count = mesh.vertex_count, .index_count = mesh.index_count, .bounds = compute_bounds(vertices)}; +} + +} // namespace render::procgen diff --git a/engine/procgen/procgen.hpp b/engine/procgen/procgen.hpp new file mode 100644 index 0000000..02c8fa0 --- /dev/null +++ b/engine/procgen/procgen.hpp @@ -0,0 +1,161 @@ +#pragma once + +#include "engine/core/math.hpp" +#include "engine/core/random.hpp" +#include "engine/core/transform.hpp" +#include "engine/render/buffer_types.hpp" + +#include +#include +#include +#include +#include +#include + +namespace render::procgen { + +struct ProcVertex { + core::Vec3 position{}; + core::Vec3 normal{}; + core::Vec4 tangent{1.0F, 0.0F, 0.0F, 1.0F}; + core::Vec2 uv{}; +}; + +struct Bounds { + core::Vec3 min{}; + core::Vec3 max{}; +}; + +[[nodiscard]] rendering::VertexLayoutDescription proc_vertex_layout(); +[[nodiscard]] rendering::CpuMeshData make_cpu_mesh(std::span vertices, std::span indices); +[[nodiscard]] std::vector unpack_vertices(const rendering::CpuMeshData& mesh); +[[nodiscard]] std::vector unpack_indices_u32(const rendering::CpuMeshData& mesh); + +struct MeshValidationReport { + bool valid{false}; + std::string reason{}; + std::uint32_t degenerate_triangles{0}; +}; + +[[nodiscard]] MeshValidationReport validate_proc_mesh(const rendering::CpuMeshData& mesh); +[[nodiscard]] Bounds compute_bounds(std::span vertices); + +struct PrimitiveOptions { + std::uint32_t radial_segments{16}; + std::uint32_t height_segments{1}; + std::uint32_t width_segments{1}; + std::uint32_t depth_segments{1}; +}; + +[[nodiscard]] rendering::CpuMeshData make_quad(float width, float height); +[[nodiscard]] rendering::CpuMeshData make_plane(float width, float depth, std::uint32_t width_segments, std::uint32_t depth_segments); +[[nodiscard]] rendering::CpuMeshData make_box(const core::Vec3& size, std::uint32_t segments = 1); +[[nodiscard]] rendering::CpuMeshData make_uv_sphere(float radius, std::uint32_t lat_segments, std::uint32_t lon_segments); +[[nodiscard]] rendering::CpuMeshData make_cylinder(float radius, float height, std::uint32_t radial_segments, std::uint32_t height_segments, bool capped); +[[nodiscard]] rendering::CpuMeshData make_cone(float radius, float height, std::uint32_t radial_segments, bool capped); +[[nodiscard]] rendering::CpuMeshData make_torus(float major_radius, float minor_radius, std::uint32_t major_segments, std::uint32_t minor_segments); +[[nodiscard]] rendering::CpuMeshData make_capsule(float radius, float height, std::uint32_t radial_segments, std::uint32_t hemi_segments); + +struct PathPoint { + core::Vec3 position{}; + core::Vec3 tangent{}; + core::Vec3 normal{}; + core::Vec3 binormal{}; + float u{0.0F}; +}; + +struct SplinePath { + std::vector control_points{}; + bool closed{false}; +}; + +[[nodiscard]] PathPoint sample_path(const SplinePath& path, float t); +[[nodiscard]] std::vector sample_path_uniform(const SplinePath& path, std::uint32_t samples); + +struct ExtrudeOptions { + std::uint32_t path_samples{16}; + bool cap_ends{true}; + bool closed_profile{true}; +}; + +[[nodiscard]] rendering::CpuMeshData extrude_profile_along_path(std::span profile, const SplinePath& path, const ExtrudeOptions& options); + +struct LatheOptions { + std::uint32_t radial_segments{24}; + float angle_radians{core::kTwoPi}; + bool cap_start{false}; + bool cap_end{false}; +}; + +[[nodiscard]] rendering::CpuMeshData lathe_profile(std::span profile_radius_height, const LatheOptions& options); + +struct SegmentDescriptor { + rendering::CpuMeshData mesh{}; + core::Transform local_from_prev{}; + float yaw_jitter_radians{0.0F}; + float scale_jitter{0.0F}; +}; + +struct SegmentAssemblyOptions { + core::Seed seed{}; +}; + +[[nodiscard]] rendering::CpuMeshData assemble_segments(std::span segments, const SegmentAssemblyOptions& options); + +class ScalarField { +public: + ScalarField(std::uint32_t nx, std::uint32_t ny, std::uint32_t nz, float cell_size, const core::Vec3& origin); + + [[nodiscard]] std::uint32_t nx() const noexcept { return nx_; } + [[nodiscard]] std::uint32_t ny() const noexcept { return ny_; } + [[nodiscard]] std::uint32_t nz() const noexcept { return nz_; } + [[nodiscard]] float cell_size() const noexcept { return cell_size_; } + [[nodiscard]] const core::Vec3& origin() const noexcept { return origin_; } + + [[nodiscard]] float at(std::uint32_t x, std::uint32_t y, std::uint32_t z) const; + void set(std::uint32_t x, std::uint32_t y, std::uint32_t z, float value); + void fill(float value); + + [[nodiscard]] core::Vec3 cell_position(std::uint32_t x, std::uint32_t y, std::uint32_t z) const; + +private: + [[nodiscard]] std::size_t index(std::uint32_t x, std::uint32_t y, std::uint32_t z) const; + + std::uint32_t nx_{0}; + std::uint32_t ny_{0}; + std::uint32_t nz_{0}; + float cell_size_{1.0F}; + core::Vec3 origin_{}; + std::vector data_{}; +}; + +using SignedDistanceFn = std::function; + +[[nodiscard]] SignedDistanceFn sdf_sphere(core::Vec3 center, float radius); +[[nodiscard]] SignedDistanceFn sdf_box(core::Vec3 center, core::Vec3 half_extents); +[[nodiscard]] SignedDistanceFn sdf_capsule(core::Vec3 a, core::Vec3 b, float radius); +[[nodiscard]] SignedDistanceFn sdf_cylinder(core::Vec3 center, float radius, float half_height); +[[nodiscard]] SignedDistanceFn sdf_plane(core::Vec3 normal, float offset); +[[nodiscard]] SignedDistanceFn sdf_translate(SignedDistanceFn field, core::Vec3 offset); +[[nodiscard]] SignedDistanceFn sdf_union(SignedDistanceFn a, SignedDistanceFn b); +[[nodiscard]] SignedDistanceFn sdf_subtract(SignedDistanceFn a, SignedDistanceFn b); +[[nodiscard]] SignedDistanceFn sdf_intersect(SignedDistanceFn a, SignedDistanceFn b); +[[nodiscard]] SignedDistanceFn sdf_smooth_union(SignedDistanceFn a, SignedDistanceFn b, float k); + +void rasterize_sdf_to_field(const SignedDistanceFn& sdf, ScalarField& field); +[[nodiscard]] rendering::CpuMeshData extract_isosurface(const ScalarField& field, float iso_value); + +[[nodiscard]] rendering::CpuMeshData merge_meshes(std::span meshes); +[[nodiscard]] rendering::CpuMeshData transform_mesh(const rendering::CpuMeshData& mesh, const core::Mat4& transform); +[[nodiscard]] rendering::CpuMeshData recalculate_normals(const rendering::CpuMeshData& mesh); +[[nodiscard]] rendering::CpuMeshData remove_degenerate_triangles(const rendering::CpuMeshData& mesh, float epsilon = 1.0e-6F); + +struct MeshStats { + std::uint32_t vertex_count{0}; + std::uint32_t index_count{0}; + Bounds bounds{}; +}; + +[[nodiscard]] MeshStats mesh_stats(const rendering::CpuMeshData& mesh); + +} // namespace render::procgen diff --git a/engine/shell/main.cpp b/engine/shell/main.cpp index 1fb651a..d38c0f9 100644 --- a/engine/shell/main.cpp +++ b/engine/shell/main.cpp @@ -1,6 +1,7 @@ #include "engine/filesystem/filesystem.hpp" #include "engine/platform/platform_log.hpp" #include "engine/platform/platform_runtime.hpp" +#include "engine/procgen/procgen.hpp" #include "engine/render/draw_submission.hpp" #include "engine/render/lighting.hpp" #include "engine/render/renderer.hpp" @@ -17,17 +18,11 @@ #include #include #include +#include #include namespace { -struct PosColorVertex { - float x; - float y; - float z; - std::uint32_t abgr; -}; - render::rendering::RendererBackend parse_renderer_backend_from_args(const int argc, char** argv) { for (int i = 1; i < argc; ++i) { const std::string_view arg = argv[i]; @@ -100,26 +95,21 @@ int main(int argc, char** argv) { renderer_config.debug = true; if (!renderer.initialize(renderer_config, runtime)) return 1; - const std::vector vertices = { - {-0.5F, -0.5F, 0.0F, 0xff4040ff}, {0.5F, -0.5F, 0.0F, 0xff40ff40}, {0.0F, 0.5F, 0.0F, 0xffff4040}}; - const std::vector indices = {0, 1, 2}; - - render::rendering::MeshBufferDescription mesh_desc{}; - mesh_desc.layout.stride = static_cast(sizeof(PosColorVertex)); - mesh_desc.layout.elements = { - {render::rendering::VertexAttribute::Position, 0, 3, render::rendering::AttributeType::Float, false, false}, - {render::rendering::VertexAttribute::Color0, 12, 4, render::rendering::AttributeType::Uint8, true, false}, - }; - mesh_desc.data = std::as_bytes(std::span{vertices}); - mesh_desc.vertex_count = static_cast(vertices.size()); - mesh_desc.usage = render::rendering::BufferUsage::Immutable; + auto upload_cpu_mesh = [&](const render::rendering::CpuMeshData& cpu_mesh) { + render::rendering::MeshBufferDescription mesh_desc{}; + mesh_desc.layout = cpu_mesh.layout; + mesh_desc.data = std::span{cpu_mesh.vertex_data.data(), cpu_mesh.vertex_data.size()}; + mesh_desc.vertex_count = cpu_mesh.vertex_count; + mesh_desc.usage = render::rendering::BufferUsage::Immutable; - render::rendering::IndexBufferDescription index_desc{}; - index_desc.data = std::as_bytes(std::span{indices}); - index_desc.index_count = static_cast(indices.size()); + render::rendering::IndexBufferDescription index_desc{}; + index_desc.data = std::span{cpu_mesh.index_data.data(), cpu_mesh.index_data.size()}; + index_desc.index_count = cpu_mesh.index_count; + index_desc.index_type = cpu_mesh.index_type; + index_desc.usage = render::rendering::BufferUsage::Immutable; - const auto mesh_buffer = renderer.create_mesh_buffer(mesh_desc); - const auto index_buffer = renderer.create_index_buffer(index_desc); + return std::pair{renderer.create_mesh_buffer(mesh_desc), renderer.create_index_buffer(index_desc)}; + }; render::rendering::ShaderProgramLibrary shader_library{renderer, filesystem}; const render::rendering::ShaderProgramId shader_id{.category = "debug", .name = "debug_triangle", .variant = "default"}; @@ -169,28 +159,59 @@ int main(int argc, char** argv) { sun.casts_shadows = true; scene.set_light(sun_node, sun); - constexpr std::uint32_t kInstanceRows = 12; - std::vector nodes; - nodes.reserve(kInstanceRows * kInstanceRows); - for (std::uint32_t y = 0; y < kInstanceRows; ++y) { - for (std::uint32_t x = 0; x < kInstanceRows; ++x) { - const auto node = scene.create_node("tri_instance"); - scene.set_parent(node, root, render::scene::ReparentPolicy::KeepLocalTransform); - render::core::Transform t{}; - t.translation = {static_cast(x) - 6.0F, static_cast(y) - 6.0F, 0.0F}; - t.scale = {0.15F, 0.15F, 0.15F}; - scene.set_local_transform(node, t); - render::scene::RenderableComponent rc{}; - rc.mesh_buffer = mesh_buffer; - rc.index_buffer = index_buffer; - rc.index_count = static_cast(indices.size()); - rc.material.program = program; - rc.highlighted = (x == kInstanceRows / 2U && y == kInstanceRows / 2U); - rc.emissive_color = {0.1F, 0.4F, 1.0F}; - rc.emissive_intensity = (x + y) % 5U == 0U ? 2.0F : 0.0F; - scene.set_renderable(node, rc); - nodes.push_back(node); - } + const auto primitive_mesh = render::procgen::make_box({0.9F, 0.9F, 0.9F}, 1); + + render::procgen::SplinePath extrude_path{}; + extrude_path.control_points = {{-0.6F, 0.0F, 0.0F}, {-0.2F, 0.5F, 0.3F}, {0.3F, -0.2F, 0.8F}, {0.7F, 0.3F, 1.4F}}; + std::array extrude_profile{}; + for (std::size_t i = 0; i < extrude_profile.size(); ++i) { + const float a = static_cast(i) / static_cast(extrude_profile.size()) * render::core::kTwoPi; + extrude_profile[i] = {std::cos(a) * 0.15F, std::sin(a) * 0.15F}; + } + const auto extruded_mesh = render::procgen::extrude_profile_along_path( + extrude_profile, extrude_path, render::procgen::ExtrudeOptions{.path_samples = 24, .cap_ends = true, .closed_profile = true}); + + std::array lathe_profile{{{0.0F, -0.45F}, {0.25F, -0.35F}, {0.3F, 0.0F}, {0.15F, 0.45F}, {0.0F, 0.55F}}}; + const auto lathed_mesh = render::procgen::lathe_profile(lathe_profile, {.radial_segments = 28, .angle_radians = render::core::kTwoPi}); + + render::procgen::SegmentDescriptor seg{}; + seg.mesh = render::procgen::make_cylinder(0.12F, 0.65F, 16, 1, true); + seg.local_from_prev.translation = {0.0F, 0.48F, 0.0F}; + seg.yaw_jitter_radians = 0.4F; + seg.scale_jitter = 0.15F; + std::array seg_chain{seg, seg, seg, seg, seg, seg}; + const auto segmented_mesh = render::procgen::assemble_segments(seg_chain, {.seed = render::core::Seed::from_string("shell-segment-demo")}); + + auto blob_sdf = render::procgen::sdf_smooth_union(render::procgen::sdf_sphere({0.0F, 0.0F, 0.0F}, 0.55F), + render::procgen::sdf_sphere({0.32F, 0.1F, 0.08F}, 0.36F), 0.2F); + blob_sdf = render::procgen::sdf_union(blob_sdf, render::procgen::sdf_cylinder({0.0F, -0.45F, 0.0F}, 0.12F, 0.25F)); + render::procgen::ScalarField blob_field(28, 28, 28, 0.06F, {-0.9F, -0.9F, -0.9F}); + render::procgen::rasterize_sdf_to_field(blob_sdf, blob_field); + const auto sdf_mesh = render::procgen::extract_isosurface(blob_field, 0.0F); + + const std::array demo_meshes{primitive_mesh, extruded_mesh, lathed_mesh, segmented_mesh, sdf_mesh}; + const std::array demo_positions{{{-2.8F, -0.7F, 0.0F}, {-1.2F, -1.0F, -0.6F}, {0.2F, -0.8F, 0.0F}, {1.9F, -1.2F, -0.4F}, {3.4F, -0.9F, 0.0F}}}; + std::vector> demo_gpu_meshes{}; + demo_gpu_meshes.reserve(demo_meshes.size()); + + for (std::size_t idx = 0; idx < demo_meshes.size(); ++idx) { + const auto [mesh_buffer, index_buffer] = upload_cpu_mesh(demo_meshes[idx]); + demo_gpu_meshes.emplace_back(mesh_buffer, index_buffer); + const auto node = scene.create_node("proc_mesh_demo"); + scene.set_parent(node, root, render::scene::ReparentPolicy::KeepLocalTransform); + render::core::Transform t{}; + t.translation = demo_positions[idx]; + scene.set_local_transform(node, t); + + render::scene::RenderableComponent rc{}; + rc.mesh_buffer = mesh_buffer; + rc.index_buffer = index_buffer; + rc.index_count = demo_meshes[idx].index_count; + rc.material.program = program; + rc.highlighted = (idx == demo_meshes.size() - 1U); + rc.emissive_color = {0.15F, 0.5F, 1.0F}; + rc.emissive_intensity = (idx == 4U) ? 1.2F : 0.0F; + scene.set_renderable(node, rc); } for (std::uint32_t i = 0; i < 10U; ++i) { @@ -564,8 +585,10 @@ int main(int argc, char** argv) { renderer.destroy_program(debug_albedo); renderer.destroy_program(debug_depth); renderer.destroy_program(debug_overdraw); - renderer.destroy_buffer(mesh_buffer); - renderer.destroy_buffer(index_buffer); + for (const auto& handles : demo_gpu_meshes) { + renderer.destroy_buffer(handles.first); + renderer.destroy_buffer(handles.second); + } renderer.shutdown(); runtime.shutdown(); return 0; diff --git a/tests/procgen/procgen_tests.cpp b/tests/procgen/procgen_tests.cpp new file mode 100644 index 0000000..709a870 --- /dev/null +++ b/tests/procgen/procgen_tests.cpp @@ -0,0 +1,57 @@ +#include "engine/procgen/procgen.hpp" + +#include +#include + +int main() { + using namespace render::procgen; + + const auto box = make_box({1.0F, 2.0F, 3.0F}, 1); + const auto box_valid = validate_proc_mesh(box); + assert(box_valid.valid); + assert(box.vertex_count > 0); + assert(box.index_count > 0); + + render::core::Seed seed = render::core::Seed::from_string("procgen-seed"); + SegmentDescriptor seg{}; + seg.mesh = make_cylinder(0.2F, 1.0F, 12, 1, true); + seg.local_from_prev.translation = {0.0F, 0.65F, 0.0F}; + seg.yaw_jitter_radians = 0.25F; + seg.scale_jitter = 0.1F; + const std::array chain{seg, seg, seg}; + const auto assembled_a = assemble_segments(chain, {.seed = seed}); + const auto assembled_b = assemble_segments(chain, {.seed = seed}); + assert(assembled_a.vertex_data == assembled_b.vertex_data); + assert(assembled_a.index_data == assembled_b.index_data); + + SplinePath path{}; + path.control_points = {{-1.0F, 0.0F, 0.0F}, {-0.3F, 0.5F, 0.4F}, {0.4F, -0.2F, 0.8F}, {1.0F, 0.1F, 1.2F}}; + std::array profile{}; + for (std::size_t i = 0; i < profile.size(); ++i) { + const float a = static_cast(i) / static_cast(profile.size()) * render::core::kTwoPi; + profile[i] = {std::cos(a) * 0.2F, std::sin(a) * 0.2F}; + } + const auto extruded = extrude_profile_along_path(profile, path, {.path_samples = 24, .cap_ends = true, .closed_profile = true}); + assert(validate_proc_mesh(extruded).valid); + + std::array lathe_profile_data{{{0.0F, -0.6F}, {0.35F, -0.4F}, {0.25F, 0.25F}, {0.1F, 0.7F}}}; + const auto lathed = lathe_profile(lathe_profile_data, {.radial_segments = 24, .angle_radians = render::core::kTwoPi}); + assert(validate_proc_mesh(lathed).valid); + + auto blob = sdf_smooth_union(sdf_sphere({0.0F, 0.0F, 0.0F}, 0.7F), sdf_sphere({0.45F, 0.1F, 0.0F}, 0.5F), 0.25F); + blob = sdf_subtract(blob, sdf_box({0.0F, -0.5F, 0.0F}, {0.15F, 0.15F, 0.15F})); + ScalarField field(24, 24, 24, 0.08F, {-1.0F, -1.0F, -1.0F}); + rasterize_sdf_to_field(blob, field); + const auto iso = extract_isosurface(field, 0.0F); + const auto iso_valid = validate_proc_mesh(iso); + assert(iso_valid.valid); + assert(iso.index_count > 0); + + const auto cleaned = remove_degenerate_triangles(recalculate_normals(merge_meshes(std::array{box, lathed, extruded, assembled_a, iso}))); + const auto stats = mesh_stats(cleaned); + assert(stats.vertex_count > 0); + assert(stats.index_count > 0); + assert(std::isfinite(stats.bounds.min.x)); + + return 0; +}