diff --git a/.github/workflows/mono.yml b/.github/workflows/mono.yml index a7353114d..bd2476acd 100644 --- a/.github/workflows/mono.yml +++ b/.github/workflows/mono.yml @@ -194,6 +194,13 @@ jobs: repository: godotengine/godot ref: ${{ env.GODOT_BASE_BRANCH }} + # The version of ThorVG in 4.3-stable hits an error in latest MSVC (see godot#95861). + # We should no longer need this in 4.3.1 and later. + - name: Patch ThorVG + run: | + curl -LO https://github.com/godotengine/godot/commit/4abc358952a69427617b0683fd76427a14d6faa8.patch + git apply 4abc358952a69427617b0683fd76427a14d6faa8.patch + # Clone our module under the correct directory - uses: actions/checkout@v4 with: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index a6ac4e21d..653031807 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -63,6 +63,13 @@ jobs: with: path: modules/voxel + # The version of ThorVG in 4.3-stable hits an error in latest MSVC (see godot#95861). + # We should no longer need this in 4.3.1 and later. + - name: Patch ThorVG + run: | + curl -LO https://github.com/godotengine/godot/commit/4abc358952a69427617b0683fd76427a14d6faa8.patch + git apply 4abc358952a69427617b0683fd76427a14d6faa8.patch + # Upload cache on completion and check it out now # Editing this is pretty dangerous for Windows since it can break and needs to be properly tested with a fresh cache. - name: Load .scons_cache directory diff --git a/README.md b/README.md index 71e14d54f..55e6287ab 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ SummitCollie nulshift ddel-rio (Daniel del Río Román) Cyberphinx +Mia (Tigxette) ``` diff --git a/doc/classes/VoxelAStarGrid3D.xml b/doc/classes/VoxelAStarGrid3D.xml index eb969468f..fa1d55a11 100644 --- a/doc/classes/VoxelAStarGrid3D.xml +++ b/doc/classes/VoxelAStarGrid3D.xml @@ -6,7 +6,9 @@ This can be used to find paths between two voxel positions on blocky terrain. It is tuned for agents 2 voxels tall and 1 voxel wide, which must stand on solid voxels and can jump 1 voxel high. - Search radius may also be limited (50 voxels and above starts to be relatively expensive). + No navmesh is required, it uses voxels directly with no baking. However, search radius is limited by an area (50 voxels and above starts to be relatively expensive). + At the moment, this pathfinder only considers voxels with ID 0 to be air, and the rest is considered solid. + Note: "positions" in this class are expected to be in voxels. If your terrain is offset or if voxels are smaller or bigger than world units, you may have to convert coordinates. @@ -14,6 +16,7 @@ + Gets the list of voxel positions that were visited by the last pathfinding request (relates to how A* works under the hood). This is for debugging. @@ -21,6 +24,11 @@ + Calculates a path starting from a voxel position to a target voxel position. + Those positions should be air voxels just above ground with enough room for agents to fit in. + The returned path will be a series of contiguous voxel positions to walk through in order to get to the destination. + If no path is found, or if either start or destination position is outside of the search area, an empty array will be returned. + You may also use [method set_region] to specify the search area. @@ -28,28 +36,35 @@ + Same as [method find_path], but performs the calculation on a separate thread. The result will be emitted with the [signal async_search_completed] signal. + Only one asynchronous search can be active at a given time. Use [method is_running_async] to check this. + Gets the maximum region limit that will be considered for pathfinding, in voxels. + Returns true if a path is currently being calculated asynchronously. See [method find_path_async]. + Sets the maximum region limit that will be considered for pathfinding, in voxels. You should usually set this before calling [method find_path]. + The larger the region, the more expensive the search can get. Keep in mind voxel volumes scale cubically, so don't use this on large areas (for example 50 voxels is quite big). + Sets the terrain that will be used to do searches in. @@ -57,6 +72,7 @@ + Emitted when searches triggered with [method find_path_async] are complete. diff --git a/doc/classes/VoxelGeneratorNoise2D.xml b/doc/classes/VoxelGeneratorNoise2D.xml index 86e8741cc..bbf253b6b 100644 --- a/doc/classes/VoxelGeneratorNoise2D.xml +++ b/doc/classes/VoxelGeneratorNoise2D.xml @@ -11,6 +11,7 @@ When assigned, this curve will alter the distribution of height variations, allowing to give some kind of "profile" to the generated shapes. By default, a linear curve from 0 to 1 is used. + It is assumed that the curve's domain goes from 0 to 1. diff --git a/doc/classes/VoxelStream.xml b/doc/classes/VoxelStream.xml index 654cb7329..d7cf4e37e 100644 --- a/doc/classes/VoxelStream.xml +++ b/doc/classes/VoxelStream.xml @@ -29,18 +29,21 @@ - + + [code]out_buffer[/code]: Block of voxels to load. Must be a pre-created instance (not null). + [code]block_position[/code]: Position of the block in block coordinates within the specified LOD. - + [code]buffer[/code]: Block of voxels to save. It is strongly recommended to not keep a reference to that data afterward, because streams are allowed to cache it, and saved data must represent either snapshots (copies) or last references to the data after the volume they belonged to is destroyed. + [code]block_position[/code]: Position of the block in block coordinates within the specified LOD. diff --git a/doc/classes/VoxelTool.xml b/doc/classes/VoxelTool.xml index f014845df..31762d5da 100644 --- a/doc/classes/VoxelTool.xml +++ b/doc/classes/VoxelTool.xml @@ -161,7 +161,7 @@ - Runs a voxel-based raycast to find the first hit from an origin and a direction. + Runs a voxel-based raycast to find the first hit from an origin and a direction. Coordinates are in world space. Returns a result object if a voxel got hit, otherwise returns [code]null[/code]. This is useful when colliders cannot be relied upon. It might also be faster (at least at short range), and is more precise to find which voxel is hit. It internally uses the DDA algorithm. [code]collision_mask[/code] is currently only used with blocky voxels. It is combined with [member VoxelBlockyModel.collision_mask] to decide which voxel types the ray can collide with. diff --git a/doc/graph_nodes.xml b/doc/graph_nodes.xml index ca252de07..bb105444d 100644 --- a/doc/graph_nodes.xml +++ b/doc/graph_nodes.xml @@ -52,7 +52,7 @@ - Returns the value of a custom [code]curve[/code] at coordinate [code]x[/code], where [code]x[/code] is in the range [code]\[0..1][/code]. The [code]curve[/code] is specified with a [Curve] resource. + Returns the value of a custom [code]curve[/code] at coordinate [code]x[/code], where [code]x[/code] is in the range specified by its domain properties (in Godot 4.3 and earlier, it is in [code]\[0..1][/code]). The [code]curve[/code] is specified with a [Curve] resource. diff --git a/doc/source/changelog.md b/doc/source/changelog.md index 7193d1c69..c495f7631 100644 --- a/doc/source/changelog.md +++ b/doc/source/changelog.md @@ -12,9 +12,12 @@ Semver is not yet in place, so each version can have breaking changes, although Primarily developped with Godot 4.3. +- `VoxelBlockyModel`: Added option to turn off "LOD skirts" when used with `VoxelLodTerrain`, which may be useful with transparent models - `VoxelBlockyModelCube`: Added support for mesh rotation like `VoxelBlockyMesh` (prior to that, rotation buttons in the editor only swapped tiles around) - `VoxelEngine`: Added the `tasks.gpu` entry to the dictionary returned by `get_stats`, which may be useful for loading screens (notably asynchronous compiling of compute shaders, which can delay generation if GPU is enabled) - `VoxelInstanceGenerator`: Added `OnePerTriangle` emission mode +- `VoxelToolLodTerrain`: Implemented raycast when the mesher is `VoxelMesherBlocky` or `VoxelMesherCubes` +- `VoxelInstanceGenerator`: Added ability to filter spawning by voxel texture indices, when using `VoxelMesherTransvoxel` with `texturing_mode` set to `4-blend over 16 textures` - Fixes - Fixed potential deadlock when using detail rendering and various editing features (thanks to lenesxy, issue #693) @@ -25,7 +28,9 @@ Primarily developped with Godot 4.3. - Fixed blocks were saved with incorrect LOD index when they get unloaded using Clipbox, leading to holes and mismatched terrain (#691) - Fixed incorrect loading of chunks near terrain borders when viewers are far away from bounds, when using the Clipbox streaming system - `VoxelStreamSQLite`: fixed connection leaks (thanks to lenesxy, issue #713) - - `VoxelTerrain`: edits and copies across fixed bounds no longer behave as if terrain generates beyond (was causing "walls" to appear). + - `VoxelTerrain`: + - Edits and copies across fixed bounds no longer behave as if terrain generates beyond (was causing "walls" to appear). + - Viewers with collision-only should no longer cause visual meshes to appear - `VoxelGeneratorGraph`: - Fixed wrong values when using `OutputWeight` with optimized execution map enabled, when weights are determined to be locally constant - Fixed occasional holes in terrain when using `FastNoise3D` nodes with the `OpenSimplex2S` noise type @@ -39,6 +44,7 @@ Primarily developped with Godot 4.3. - `VoxelInstanceLibrary`: Items should no longer be accessed using generated properties (`item1`, `item2` etc). Use `get_item` instead. - `VoxelMesherTransvoxel`: Removed `deep_sampling` experimental option - `VoxelTool`: The `flat_direction` of `do_hemisphere` now points away from the flat side of the hemisphere (like its normal), instead of pointing towards it + - `VoxelToolLodTerrain`: `raycast` used to take coordinates in terrain space. It is now in world space, for consistency with `VoxelToolTerrain`. 1.3 - 17/08/2024 - branch `1.3` - tag `v1.3.0` diff --git a/doc/source/performance.md b/doc/source/performance.md index 7dc2dfc22..452d6fa71 100644 --- a/doc/source/performance.md +++ b/doc/source/performance.md @@ -48,16 +48,15 @@ Terrains are rendered with many unique meshes. That can amount for a lot of draw - Increase mesh block size: they default to 16, but it can be set to 32 instead. This reduces the number of draw calls, but may increase the time it takes to modify voxels. -Slow mesh updates issue with OpenGL ------------------------------------- +### Slow mesh updates issue with OpenGL -### Issue +#### Issue Godot 3.x is using OpenGL, and there is an issue which currently degrades performance of this voxel engine a lot. Framerate is not necessarily bad, but the speed at which voxel terrain updates is very low, compared to what it should be. So far the issue has been seen on Windows, on both Intel or nVidia cards. Note: Godot 4.x will have an OpenGL renderer, but this issue has not been tested here yet. -### Workarounds +#### Workarounds Note: you don't have to do them all at once, picking just one of them can improve the situation. @@ -66,7 +65,7 @@ Note: you don't have to do them all at once, picking just one of them can improv - Or turn off `display/window/vsync/use_vsync` in project settings. Not as effective and eats more resources, but improves performance. - Or turn on `display/window/vsync/vsync_via_compositor` in project settings. Not as effective but can improve performance in windowed mode. -### Explanation +#### Explanation The engine relies a lot on uploading many meshes at runtime, and this cannot be threaded efficiently in Godot 3.x so far. So instead, meshes are uploaded in the main thread, until part of the frame time elapsed. Beyond that time, the engine stops and continues next frame. This is intented to smooth out the load and avoid stutters *caused by the task CPU-side*. Other tasks that cannot be threaded are also put into the same queue, like creating colliders. @@ -76,10 +75,9 @@ When one workaround is used, like enabling `verbose_stdout`, this slowdown compl For more information, see [Godot issue #52801](https://github.com/godotengine/godot/issues/52801). -Slowdown when moving fast with Vulkan --------------------------------------- +### Slowdown when moving fast with Vulkan -### Issue +#### Issue If you move fast while near a terrain with a lot of chunks (mesh size 16 and high LOD detail), the renderer can cause noticeable slowdowns. This is because Godot4's Vulkan allocator is much slower to destroy mesh buffers than Godot 3 was, and it does that on the main thread. When you move fast, a lot of meshes get created in front of the camera, and a lot get destroyed behind the camera at the same time. Creation is cheap, destruction is expensive. @@ -93,7 +91,7 @@ This issue also was not noticeable in Godot 3. This problem reproduces specifically when a lot of small meshes are destroyed (small as in 16x16 pieces of terrain, variable size), while a lot of them (thousands) already exist at the same time. Note, some of them are not necessarily visible. -### Workarounds +#### Workarounds It is not possible for the module to just "pool the meshes", because when new meshes need to be created, the API requires to create new buffers anyways and drops the old ones (AFAIK). It is also not possible to use a thread on our side because the work is deferred to the end of the frame, not on the call site. @@ -106,7 +104,42 @@ The only workarounds involve limiting the game: - Reduce LOD distance so less blocks have to be destroyed, at the expense of quality -Iteration order +Physics +---------- + +The voxel engine offers two different approaches to physics: +- Standard Physics: the official API Godot exposes through `PhysicsServer3D` (Godot Physics, Godot Jolt...). +- Box Physics: a small specialized API that only works with axis-aligned boxes on blocky terrain, exposed with `VoxelBoxMover` and voxel raycasts. It is much more limited and requires some setup, but performs faster. + +### Standard Physics + +#### Mesh colliders simulation + +Similar to rendering 16x16x16 or 32x32x32 blocks, the voxel engine uses "mesh" colliders for every terrain block (or "chunk"). These colliders are static and can be concave. Therefore, any terrain shape should be supported, but depends a lot on how performant these colliders are in the underlying physics engine. + +Moving terrain remains possible, but do not expect physics to work correctly on the surface while moving it. + +#### Tunnelling + +Mesh colliders used by terrain have no "thickness". An object can sit undisturbed outside or inside of it, contrary to convex colliders which usually have a "depenetration force" pushing objects away from their inside. This makes mesh colliders more prone to "tunneling": if an object goes too fast, or is too small relative to its velocity, it can pass through the ground. + +- Limit speed of your objects +- For fast-moving objects (projectiles?), use elongated shapes, or just raycasts, making sure that the "trail" of the shape "connects" between each physics frame +- Enable Continuous Collision Detection, if the physics engine supports it +- Check voxel data to find out if a point is underground and move up the object + +#### Shape creation is very slow + +Similar to rendering, the voxel engine has to convert voxels into meshes ("meshing"), and does this with our own prioritised pool of threads. +It will use those meshes as colliders. Creating a collider from a mesh is actually much more expensive than meshing itself (about 3 to 5 times), because it involves creating an acceleration structure to speed up collision detection (BVH, octree...). + +Unfortunately, Godot does not offer a reliable way to safely create these shapes *including their acceleration structure* from within out meshing threads. So instead, we had to defer it all to the main thread, and spread it over multiple frames. This slows down terrain loading tremendously (compared to disabling collisions). + +- A [proposal](https://github.com/godotengine/godot-proposals/issues/483) has been opened to expose this issue, still not addressed +- [Godot Jolt](https://github.com/godotengine/godot/pull/99895) also has this issue, exacerbated by the fact it was implemented to defer shape setup to the very last moment, when entering the scene tree. So even if we were allowed to create mesh colliders from our threads, it still defers all the hard work to the main thread. + + +Voxel Iteration order ----------------- In this engine, voxels are stored in flat arrays indexed in ZXY order. Y is the "deepest" coordinate: when iterating a `VoxelBuffer` of dimensions `(size.x, size.y, size.z)`, adding 1 to the Y coordinate is equivalent to advancing by 1 element in memory. Conversely, adding 1 to the X coordinate advances by `size.y` elements, and adding 1 to the Z coordinate advances by `(size.x * size.y)` elements. diff --git a/doc/source/quick_start.md b/doc/source/quick_start.md index 861a7f364..e594185c1 100644 --- a/doc/source/quick_start.md +++ b/doc/source/quick_start.md @@ -99,6 +99,9 @@ Here are some reasons why you might not need it: - "I need to make a planet": you can make more efficient planets by stitching 6 spherified heightmaps together. Take a cube where each face is a heightmap, then puff that cube to turn it into a sphere. -- "I want to make Minecraft but free and with my own blocks": Minecraft is a lot more than voxels. While the module can replicate basic functionalities, it is more general than this at the moment, so it doesn't provide a lot of features found in Minecraft out of the box. Alternatively, you could create a mod with [Minetest](https://www.minetest.net/), which is a more specialized engine. +- "I want to make Minecraft but different and with my own blocks": Minecraft is a lot more than voxels. While the module can replicate basic functionalities, it is more general/low-level than this at the moment, so it doesn't provide a lot of features found in Minecraft out of the box. Alternatively, you could create a mod with [Minetest](https://www.minetest.net/), which is a more specialized engine. + +- "I want super small voxels like Teardown or John Lin's sandbox": these games use a very different tech than this module uses. They raytrace voxels in real-time. This module instead uses a classic polygon-based approach. While you could in theory make terrain that looks like that, it won't perform well. - "GridMap sucks": how large do you want your grid to be? How complex are your models? This module's blocky mesher is geared towards very large grids with simple geometry, so it has its own restrictions. + diff --git a/edition/floating_chunks.cpp b/edition/floating_chunks.cpp new file mode 100644 index 000000000..416fb1f76 --- /dev/null +++ b/edition/floating_chunks.cpp @@ -0,0 +1,503 @@ +#include "floating_chunks.h" +#include "../constants/voxel_string_names.h" +#include "../storage/voxel_buffer.h" +#include "../util/godot/classes/array_mesh.h" +#include "../util/godot/classes/collision_shape_3d.h" +#include "../util/godot/classes/convex_polygon_shape_3d.h" +#include "../util/godot/classes/mesh_instance_3d.h" +#include "../util/godot/classes/rendering_server.h" +#include "../util/godot/classes/rigid_body_3d.h" +#include "../util/godot/classes/shader.h" +#include "../util/godot/classes/shader_material.h" +#include "../util/godot/classes/timer.h" +#include "../util/island_finder.h" +#include "../util/profiling.h" +#include "voxel_tool.h" + +namespace zylann::voxel { + +void box_propagate_ccl(Span cells, const Vector3i size) { + ZN_PROFILE_SCOPE(); + + // Propagate non-zero cells towards zero cells in a 3x3x3 pattern. + // Used on a grid produced by Connected-Component-Labelling. + + // Z + { + ZN_PROFILE_SCOPE_NAMED("Z"); + Vector3i pos; + const int dz = size.x * size.y; + unsigned int i = 0; + for (pos.x = 0; pos.x < size.x; ++pos.x) { + for (pos.y = 0; pos.y < size.y; ++pos.y) { + // Note, border cells are not handled. Not just because it's more work, but also because that could + // make the label touch the edge, which is later interpreted as NOT being an island. + pos.z = 2; + i = Vector3iUtil::get_zxy_index(pos, size); + for (; pos.z < size.z - 2; ++pos.z, i += dz) { + const uint8_t c = cells[i]; + if (c != 0) { + if (cells[i - dz] == 0) { + cells[i - dz] = c; + } + if (cells[i + dz] == 0) { + cells[i + dz] = c; + // Skip next cell, otherwise it would cause endless propagation + i += dz; + ++pos.z; + } + } + } + } + } + } + + // X + { + ZN_PROFILE_SCOPE_NAMED("X"); + Vector3i pos; + const int dx = size.y; + unsigned int i = 0; + for (pos.z = 0; pos.z < size.z; ++pos.z) { + for (pos.y = 0; pos.y < size.y; ++pos.y) { + pos.x = 2; + i = Vector3iUtil::get_zxy_index(pos, size); + for (; pos.x < size.x - 2; ++pos.x, i += dx) { + const uint8_t c = cells[i]; + if (c != 0) { + if (cells[i - dx] == 0) { + cells[i - dx] = c; + } + if (cells[i + dx] == 0) { + cells[i + dx] = c; + i += dx; + ++pos.x; + } + } + } + } + } + } + + // Y + { + ZN_PROFILE_SCOPE_NAMED("Y"); + Vector3i pos; + const int dy = 1; + unsigned int i = 0; + for (pos.z = 0; pos.z < size.z; ++pos.z) { + for (pos.x = 0; pos.x < size.x; ++pos.x) { + pos.y = 2; + i = Vector3iUtil::get_zxy_index(pos, size); + for (; pos.y < size.y - 2; ++pos.y, i += dy) { + const uint8_t c = cells[i]; + if (c != 0) { + if (cells[i - dy] == 0) { + cells[i - dy] = c; + } + if (cells[i + dy] == 0) { + cells[i + dy] = c; + i += dy; + ++pos.y; + } + } + } + } + } + } +} + +// Turns floating chunks of voxels into rigidbodies: +// Detects separate groups of connected voxels within a box. Each group fully contained in the box is removed from +// the source volume, and turned into a rigidbody. +// This is one way of doing it, I don't know if it's the best way (there is rarely a best way) +// so there are probably other approaches that could be explored in the future, if they have better performance +Array separate_floating_chunks( + VoxelTool &voxel_tool, + Box3i world_box, + Node *parent_node, + Transform3D terrain_transform, + Ref mesher, + Array materials +) { + ZN_PROFILE_SCOPE(); + + // Checks + ERR_FAIL_COND_V(mesher.is_null(), Array()); + ERR_FAIL_COND_V(parent_node == nullptr, Array()); + + // Copy source data + + // TODO Do not assume channel, at the moment it's hardcoded for smooth terrain + static const int channels_mask = (1 << VoxelBuffer::CHANNEL_SDF); + static const VoxelBuffer::ChannelId main_channel = VoxelBuffer::CHANNEL_SDF; + + VoxelBuffer source_copy_buffer(VoxelBuffer::ALLOCATOR_POOL); + { + ZN_PROFILE_SCOPE_NAMED("Copy"); + source_copy_buffer.create(world_box.size); + voxel_tool.copy(world_box.position, source_copy_buffer, channels_mask); + } + + // Label distinct voxel groups + + // TODO Candidate for temp allocator + static thread_local StdVector ccl_output; + ccl_output.resize(Vector3iUtil::get_volume_u64(world_box.size)); + + unsigned int label_count = 0; + + { + // TODO Allow to run the algorithm at a different LOD, to trade precision for speed + ZN_PROFILE_SCOPE_NAMED("CCL scan"); + IslandFinder island_finder; + island_finder.scan_3d( + Box3i(Vector3i(), world_box.size), + [&source_copy_buffer](Vector3i pos) { + // TODO Can be optimized further with direct access + return source_copy_buffer.get_voxel_f(pos.x, pos.y, pos.z, main_channel) < 0.f; + }, + to_span(ccl_output), + &label_count + ); + } + + struct Bounds { + Vector3i min_pos; + Vector3i max_pos; // inclusive + bool valid = false; + }; + + if (main_channel == VoxelBuffer::CHANNEL_SDF) { + // Propagate labels to improve SDF quality, otherwise gradients of separated chunks would cut off abruptly. + // Limitation: if two islands are too close to each other, one will win over the other. + // An alternative could be to do this on individual chunks? + box_propagate_ccl(to_span(ccl_output), world_box.size); + } + + // Compute bounds of each group + + StdVector bounds_per_label; + { + ZN_PROFILE_SCOPE_NAMED("Bounds calculation"); + + // Adding 1 because label 0 is the index for "no label" + bounds_per_label.resize(label_count + 1); + + unsigned int ccl_index = 0; + for (int z = 0; z < world_box.size.z; ++z) { + for (int x = 0; x < world_box.size.x; ++x) { + for (int y = 0; y < world_box.size.y; ++y) { + CRASH_COND(ccl_index >= ccl_output.size()); + const uint8_t label = ccl_output[ccl_index]; + ++ccl_index; + + if (label == 0) { + continue; + } + + CRASH_COND(label >= bounds_per_label.size()); + Bounds &bounds = bounds_per_label[label]; + + if (bounds.valid == false) { + bounds.min_pos = Vector3i(x, y, z); + bounds.max_pos = bounds.min_pos; + bounds.valid = true; + + } else { + if (x < bounds.min_pos.x) { + bounds.min_pos.x = x; + } else if (x > bounds.max_pos.x) { + bounds.max_pos.x = x; + } + + if (y < bounds.min_pos.y) { + bounds.min_pos.y = y; + } else if (y > bounds.max_pos.y) { + bounds.max_pos.y = y; + } + + if (z < bounds.min_pos.z) { + bounds.min_pos.z = z; + } else if (z > bounds.max_pos.z) { + bounds.max_pos.z = z; + } + } + } + } + } + } + + // Eliminate groups that touch the box border, + // because that means we can't tell if they are truly hanging in the air or attached to land further away + + const Vector3i lbmax = world_box.size - Vector3i(1, 1, 1); + for (unsigned int label = 1; label < bounds_per_label.size(); ++label) { + CRASH_COND(label >= bounds_per_label.size()); + Bounds &local_bounds = bounds_per_label[label]; + ERR_CONTINUE(!local_bounds.valid); + + if ( // + local_bounds.min_pos.x == 0 // + || local_bounds.min_pos.y == 0 // + || local_bounds.min_pos.z == 0 // + || local_bounds.max_pos.x == lbmax.x // + || local_bounds.max_pos.y == lbmax.y // + || local_bounds.max_pos.z == lbmax.z) { + // + local_bounds.valid = false; + } + } + + // Create voxel buffer for each group + + struct InstanceInfo { + VoxelBuffer voxels; + Vector3i world_pos; + unsigned int label; + }; + StdVector instances_info; + + const int min_padding = 2; // mesher->get_minimum_padding(); + const int max_padding = 2; // mesher->get_maximum_padding(); + + { + ZN_PROFILE_SCOPE_NAMED("Extraction"); + + for (unsigned int label = 1; label < bounds_per_label.size(); ++label) { + CRASH_COND(label >= bounds_per_label.size()); + const Bounds local_bounds = bounds_per_label[label]; + + if (!local_bounds.valid) { + continue; + } + + const Vector3i world_pos = world_box.position + local_bounds.min_pos - Vector3iUtil::create(min_padding); + const Vector3i size = + local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding); + + instances_info.push_back(InstanceInfo{ VoxelBuffer(VoxelBuffer::ALLOCATOR_POOL), world_pos, label }); + + VoxelBuffer &buffer = instances_info.back().voxels; + buffer.create(size.x, size.y, size.z); + + // Read voxels from the source volume + voxel_tool.copy(world_pos, buffer, channels_mask); + + // Cleanup padding borders + const Box3i inner_box( + Vector3iUtil::create(min_padding), + buffer.get_size() - Vector3iUtil::create(min_padding + max_padding) + ); + Box3i(Vector3i(), buffer.get_size()).difference(inner_box, [&buffer](Box3i box) { + buffer.fill_area_f(constants::SDF_FAR_OUTSIDE, box.position, box.position + box.size, main_channel); + }); + + // Filter out voxels that don't belong to this label + for (int z = local_bounds.min_pos.z; z <= local_bounds.max_pos.z; ++z) { + for (int x = local_bounds.min_pos.x; x <= local_bounds.max_pos.x; ++x) { + for (int y = local_bounds.min_pos.y; y <= local_bounds.max_pos.y; ++y) { + const unsigned int ccl_index = Vector3iUtil::get_zxy_index(Vector3i(x, y, z), world_box.size); + CRASH_COND(ccl_index >= ccl_output.size()); + const uint8_t label2 = ccl_output[ccl_index]; + + if (label2 != 0 && label != label2) { + buffer.set_voxel_f( + constants::SDF_FAR_OUTSIDE, + min_padding + x - local_bounds.min_pos.x, + min_padding + y - local_bounds.min_pos.y, + min_padding + z - local_bounds.min_pos.z, + main_channel + ); + } + } + } + } + } + } + + // Erase voxels from source volume. + // Must be done after we copied voxels from it. + + { + ZN_PROFILE_SCOPE_NAMED("Erasing"); + + voxel_tool.set_channel(main_channel); + + for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) { + CRASH_COND(instance_index >= instances_info.size()); + const InstanceInfo &info = instances_info[instance_index]; + voxel_tool.sdf_stamp_erase(info.voxels, info.world_pos); + } + } + + // Find out which materials contain parameters that require instancing. + // + // Since 7dbc458bb4f3e0cc94e5070bd33bde41d214c98d it's no longer possible to quickly check if a + // shader has a uniform by name using Shader's parameter cache. Now it seems the only way is to get the whole list + // of parameters and find into it, which is slow, tedious to write and different between modules and GDExtension. + + uint32_t materials_to_instance_mask = 0; + { + StdVector params; + const String u_block_local_transform = VoxelStringNames::get_singleton().u_block_local_transform; + + ZN_ASSERT_RETURN_V_MSG( + materials.size() < 32, + Array(), + "Too many materials. If you need more, make a request or change the code." + ); + + for (int material_index = 0; material_index < materials.size(); ++material_index) { + Ref sm = materials[material_index]; + if (sm.is_null()) { + continue; + } + + Ref shader = sm->get_shader(); + if (shader.is_null()) { + continue; + } + + params.clear(); + zylann::godot::get_shader_parameter_list(shader->get_rid(), params); + + for (const zylann::godot::ShaderParameterInfo ¶m_info : params) { + if (param_info.name == u_block_local_transform) { + materials_to_instance_mask |= (1 << material_index); + break; + } + } + } + } + + // Create instances + + Array nodes; + + { + ZN_PROFILE_SCOPE_NAMED("Remeshing and instancing"); + + for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) { + CRASH_COND(instance_index >= instances_info.size()); + const InstanceInfo &info = instances_info[instance_index]; + + CRASH_COND(info.label >= bounds_per_label.size()); + const Bounds local_bounds = bounds_per_label[info.label]; + ERR_CONTINUE(!local_bounds.valid); + + // DEBUG + // print_line(String("--- Instance {0}").format(varray(instance_index))); + // for (int z = 0; z < info.voxels->get_size().z; ++z) { + // for (int x = 0; x < info.voxels->get_size().x; ++x) { + // String s; + // for (int y = 0; y < info.voxels->get_size().y; ++y) { + // float sdf = info.voxels->get_voxel_f(x, y, z, VoxelBuffer::CHANNEL_SDF); + // if (sdf < -0.1f) { + // s += "X "; + // } else if (sdf < 0.f) { + // s += "x "; + // } else { + // s += "- "; + // } + // } + // print_line(s); + // } + // print_line("//"); + // } + + const Transform3D local_transform( + Basis(), + info.world_pos + // Undo min padding + + Vector3i(1, 1, 1) + ); + + for (int i = 0; i < materials.size(); ++i) { + if ((materials_to_instance_mask & (1 << i)) != 0) { + Ref sm = materials[i]; + ZN_ASSERT_CONTINUE(sm.is_valid()); + sm = sm->duplicate(false); + // That parameter should have a valid default value matching the local transform relative to the + // volume, which is usually per-instance, but in Godot 3 we have no such feature, so we have to + // duplicate. + // TODO Try using per-instance parameters for scalar uniforms (Godot 4 doesn't support textures) + sm->set_shader_parameter( + VoxelStringNames::get_singleton().u_block_local_transform, local_transform + ); + materials[i] = sm; + } + } + + // TODO If normalmapping is used here with the Transvoxel mesher, we need to either turn it off just for + // this call, or to pass the right options + Ref mesh = mesher->build_mesh(info.voxels, materials, Dictionary()); + // The mesh is not supposed to be null, + // because we build these buffers from connected groups that had negative SDF. + ERR_CONTINUE(mesh.is_null()); + + if (zylann::godot::is_mesh_empty(**mesh)) { + continue; + } + + // DEBUG + // { + // Ref serializer; + // serializer.instance(); + // Ref peer; + // peer.instance(); + // serializer->serialize(peer, info.voxels, false); + // String fpath = String("debug_data/split_dump_{0}.bin").format(varray(instance_index)); + // FileAccess *f = FileAccess::open(fpath, FileAccess::WRITE); + // PoolByteArray bytes = peer->get_data_array(); + // PoolByteArray::Read bytes_read = bytes.read(); + // f->store_buffer(bytes_read.ptr(), bytes.size()); + // f->close(); + // memdelete(f); + // } + + // TODO Option to make multiple convex shapes + // TODO Use the fast way. This is slow because of the internal TriangleMesh thing and mesh data query. + // TODO Don't create a body if the mesh has no triangles + Ref shape = mesh->create_convex_shape(); + ERR_CONTINUE(shape.is_null()); + CollisionShape3D *collision_shape = memnew(CollisionShape3D); + collision_shape->set_shape(shape); + // Center the shape somewhat, because Godot is confusing node origin with center of mass + const Vector3i size = + local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding); + const Vector3 offset = -Vector3(size) * 0.5f; + collision_shape->set_position(offset); + + RigidBody3D *rigid_body = memnew(RigidBody3D); + rigid_body->set_transform(terrain_transform * local_transform.translated_local(-offset)); + rigid_body->add_child(collision_shape); + rigid_body->set_freeze_mode(RigidBody3D::FREEZE_MODE_KINEMATIC); + rigid_body->set_freeze_enabled(true); + + // Switch to rigid after a short time to workaround clipping with terrain, + // because colliders are updated asynchronously + Timer *timer = memnew(Timer); + timer->set_wait_time(0.2); + timer->set_one_shot(true); + timer->connect("timeout", callable_mp(rigid_body, &RigidBody3D::set_freeze_enabled).bind(false)); + // Cannot use start() here because it requires to be inside the SceneTree, + // and we don't know if it will be after we add to the parent. + timer->set_autostart(true); + rigid_body->add_child(timer); + + MeshInstance3D *mesh_instance = memnew(MeshInstance3D); + mesh_instance->set_mesh(mesh); + mesh_instance->set_position(offset); + rigid_body->add_child(mesh_instance); + + parent_node->add_child(rigid_body); + + nodes.append(rigid_body); + } + } + + return nodes; +} + +} // namespace zylann::voxel diff --git a/edition/floating_chunks.h b/edition/floating_chunks.h new file mode 100644 index 000000000..0efadde1a --- /dev/null +++ b/edition/floating_chunks.h @@ -0,0 +1,26 @@ +#ifndef VOXEL_FLOATING_CHUNKS_H +#define VOXEL_FLOATING_CHUNKS_H + +#include "../meshers/voxel_mesher.h" +#include "../util/godot/core/array.h" +#include "../util/math/box3i.h" +#include "../util/math/transform_3d.h" + +ZN_GODOT_FORWARD_DECLARE(class Node); + +namespace zylann::voxel { + +class VoxelTool; + +Array separate_floating_chunks( + VoxelTool &voxel_tool, + Box3i world_box, + Node *parent_node, + Transform3D terrain_transform, + Ref mesher, + Array materials +); + +} // namespace zylann::voxel + +#endif // VOXEL_FLOATING_CHUNKS_H diff --git a/edition/funcs.cpp b/edition/funcs.cpp index 4071f0cff..b126e2810 100644 --- a/edition/funcs.cpp +++ b/edition/funcs.cpp @@ -22,7 +22,7 @@ void copy_from_chunked_storage( const VoxelBuffer *(*get_block_func)(void *, Vector3i), void *get_block_func_ctx ) { - ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume(dst_buffer.get_size()) > 0, "The area to copy is empty"); + ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume_u64(dst_buffer.get_size()) > 0, "The area to copy is empty"); ZN_ASSERT_RETURN(get_block_func != nullptr); const Vector3i max_pos = min_pos + dst_buffer.get_size(); @@ -206,7 +206,7 @@ void run_blocky_random_tick( const Box3i block_voxel_box(block_origin, Vector3iUtil::create(block_size)); Box3i local_voxel_box = voxel_box.clipped(block_voxel_box); local_voxel_box.position -= block_origin; - const float volume_ratio = Vector3iUtil::get_volume(local_voxel_box.size) / block_volume; + const float volume_ratio = Vector3iUtil::get_volume_u64(local_voxel_box.size) / block_volume; const int local_batch_count = Math::ceil(batch_count * volume_ratio); // Choose a bunch of voxels at random within the block. @@ -347,7 +347,7 @@ void box_blur_slow_ref(const VoxelBuffer &src, VoxelBuffer &dst, int radius, Vec dst.create(dst_size); const int box_size = radius * 2 + 1; - const float box_volume = Vector3iUtil::get_volume(Vector3i(box_size, box_size, box_size)); + const float box_volume = Vector3iUtil::get_volume_u64(Vector3i(box_size, box_size, box_size)); const float sphere_radius_s = sphere_radius * sphere_radius; @@ -427,7 +427,7 @@ void box_blur(const VoxelBuffer &src, VoxelBuffer &dst, int radius, Vector3f sph // Temporary buffer with extra length in two axes StdVector tmp; const Vector3i tmp_size(dst_size.x + 2 * radius, dst_size.y, dst_size.z + 2 * radius); - tmp.resize(Vector3iUtil::get_volume(tmp_size)); + tmp.resize(Vector3iUtil::get_volume_u64(tmp_size)); // Y blur Vector3i dst_pos; diff --git a/edition/mesh_sdf.cpp b/edition/mesh_sdf.cpp index 287ae7840..a468b4c14 100644 --- a/edition/mesh_sdf.cpp +++ b/edition/mesh_sdf.cpp @@ -468,7 +468,7 @@ void partition_triangles( const Vector3i grid_max = to_vec3i(math::ceil(max_pos / chunk_size)); chunk_grid.size = grid_max - grid_min; - chunk_grid.chunks.resize(Vector3iUtil::get_volume(chunk_grid.size)); + chunk_grid.chunks.resize(Vector3iUtil::get_volume_u64(chunk_grid.size)); chunk_grid.chunk_size = chunk_size; chunk_grid.min_pos = to_vec3f(grid_min) * chunk_size; @@ -807,7 +807,7 @@ void generate_mesh_sdf_approx_interp( const float node_subdiv_threshold = 0.6f * node_size; StdVector node_grid; - node_grid.resize(Vector3iUtil::get_volume(node_grid_size)); + node_grid.resize(Vector3iUtil::get_volume_u64(node_grid_size)); // Fill SDF grid with far distances as "infinity", we'll use that to check if we computed it already sdf_grid.fill(FAR_SD); @@ -923,7 +923,7 @@ void generate_mesh_sdf_naive( ) { ZN_PROFILE_SCOPE(); ZN_ASSERT(Box3i(Vector3i(), res).contains(sub_box)); - ZN_ASSERT(int64_t(sdf_grid.size()) == Vector3iUtil::get_volume(res)); + ZN_ASSERT(sdf_grid.size() == Vector3iUtil::get_volume_u64(res)); const Vector3f mesh_size = max_pos - min_pos; const Vector3f cell_size = mesh_size / Vector3f(res.x, res.y, res.z); @@ -963,7 +963,7 @@ void generate_mesh_sdf_partitioned( ) { ZN_PROFILE_SCOPE(); ZN_ASSERT(Box3i(Vector3i(), res).contains(sub_box)); - ZN_ASSERT(int64_t(sdf_grid.size()) == Vector3iUtil::get_volume(res)); + ZN_ASSERT(sdf_grid.size() == Vector3iUtil::get_volume_u64(res)); const Vector3f mesh_size = max_pos - min_pos; const Vector3f cell_size = mesh_size / Vector3f(res.x, res.y, res.z); @@ -1411,7 +1411,7 @@ void generate_mesh_sdf_approx_floodfill( ZN_PROFILE_SCOPE(); StdVector flag_grid; - flag_grid.resize(Vector3iUtil::get_volume(res)); + flag_grid.resize(Vector3iUtil::get_volume_u64(res)); memset(flag_grid.data(), FLAG_NOT_VISITED, sizeof(uint8_t) * flag_grid.size()); generate_mesh_sdf_hull(sdf_grid, res, triangles, min_pos, max_pos, chunk_grid, to_span(flag_grid), FLAG_FROZEN); diff --git a/edition/raycast.cpp b/edition/raycast.cpp new file mode 100644 index 000000000..876564b53 --- /dev/null +++ b/edition/raycast.cpp @@ -0,0 +1,347 @@ +#include "raycast.h" +#include "../meshers/blocky/voxel_mesher_blocky.h" +#include "../meshers/cubes/voxel_mesher_cubes.h" +#include "../storage/voxel_buffer.h" +#include "../storage/voxel_data.h" +#include "../terrain/voxel_node.h" +#include "../util/godot/classes/ref_counted.h" +#include "../util/voxel_raycast.h" +#include "funcs.h" +#include "voxel_raycast_result.h" + +namespace zylann::voxel { + +// Binary search can be more accurate than linear regression because the SDF can be inaccurate in the first place. +// An alternative would be to polygonize a tiny area around the middle-phase hit position. +// `d1` is how far from `pos0` along `dir` the binary search will take place. +// The segment may be adjusted internally if it does not contain a zero-crossing of the +template +float approximate_distance_to_isosurface_binary_search( + const Volume_F &f, + const Vector3 pos0, + const Vector3 dir, + float d1, + const int iterations +) { + float d0 = 0.f; + float sdf0 = get_sdf_interpolated(f, pos0); + // The position given as argument may be a rough approximation coming from the middle-phase, + // so it can be slightly below the surface. We can adjust it a little so it is above. + for (int i = 0; i < 4 && sdf0 < 0.f; ++i) { + d0 -= 0.5f; + sdf0 = get_sdf_interpolated(f, pos0 + dir * d0); + } + + float sdf1 = get_sdf_interpolated(f, pos0 + dir * d1); + for (int i = 0; i < 4 && sdf1 > 0.f; ++i) { + d1 += 0.5f; + sdf1 = get_sdf_interpolated(f, pos0 + dir * d1); + } + + if ((sdf0 > 0) != (sdf1 > 0)) { + // Binary search + for (int i = 0; i < iterations; ++i) { + const float dm = 0.5f * (d0 + d1); + const float sdf_mid = get_sdf_interpolated(f, pos0 + dir * dm); + + if ((sdf_mid > 0) != (sdf0 > 0)) { + sdf1 = sdf_mid; + d1 = dm; + } else { + sdf0 = sdf_mid; + d0 = dm; + } + } + } + + // Pick distance closest to the surface + if (Math::abs(sdf0) < Math::abs(sdf1)) { + return d0; + } else { + return d1; + } +} + +Ref raycast_sdf( + const VoxelData &voxel_data, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint8_t binary_search_iterations +) { + // TODO Implement reverse raycast? (going from inside ground to air, could be useful for undigging) + + // TODO Optimization: voxel raycast uses `get_voxel` which is the slowest, but could be made faster. + // Instead, do a broad-phase on blocks. If a block's voxels need to be parsed, get all positions the ray could go + // through in that block, then query them all at once (better for bulk processing without going again through + // locking and data structures, and allows SIMD). Then check results in order. + // If no hit is found, carry on with next blocks. + + struct RaycastPredicate { + const VoxelData &data; + + bool operator()(const VoxelRaycastState &rs) { + // This is not particularly optimized, but runs fast enough for player raycasts + VoxelSingleValue defval; + defval.f = constants::SDF_FAR_OUTSIDE; + const VoxelSingleValue v = data.get_voxel(rs.hit_position, VoxelBuffer::CHANNEL_SDF, defval); + return v.f < 0; + } + }; + + Ref res; + + // We use grid-raycast as a middle-phase to roughly detect where the hit will be + RaycastPredicate predicate = { voxel_data }; + Vector3i hit_pos; + Vector3i prev_pos; + float hit_distance; + float hit_distance_prev; + // Voxels polygonized using marching cubes influence a region centered on their lower corner, + // and extend up to 0.5 units in all directions. + // + // o--------o--------o + // | A | B | Here voxel B is full, voxels A, C and D are empty. + // | xxx | Matter will show up at the lower corner of B due to interpolation. + // | xxxxxxx | + // o---xxxxxoxxxxx---o + // | xxxxxxx | + // | xxx | + // | C | D | + // o--------o--------o + // + // `voxel_raycast` operates on a discrete grid of cubic voxels, so to account for the smooth interpolation, + // we may offset the ray so that cubes act as if they were centered on the filtered result. + const Vector3 offset(0.5, 0.5, 0.5); + if (voxel_raycast( + ray_origin + offset, + ray_dir, + predicate, + max_distance, + hit_pos, + prev_pos, + hit_distance, + hit_distance_prev + )) { + // Approximate surface + + float d = hit_distance; + + if (binary_search_iterations > 0) { + // This is not particularly optimized, but runs fast enough for player raycasts + struct VolumeSampler { + const VoxelData &data; + + inline float operator()(const Vector3i &pos) const { + VoxelSingleValue defval; + defval.f = constants::SDF_FAR_OUTSIDE; + const VoxelSingleValue value = data.get_voxel(pos, VoxelBuffer::CHANNEL_SDF, defval); + return value.f; + } + }; + + VolumeSampler sampler{ voxel_data }; + d = hit_distance_prev + + approximate_distance_to_isosurface_binary_search( + sampler, + ray_origin + ray_dir * hit_distance_prev, + ray_dir, + hit_distance - hit_distance_prev, + binary_search_iterations + ); + } + + res.instantiate(); + res->position = hit_pos; + res->previous_position = prev_pos; + res->distance_along_ray = d; + } + + return res; +} + +Ref raycast_blocky( + const VoxelData &voxel_data, + const VoxelMesherBlocky &mesher, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint32_t p_collision_mask +) { + struct RaycastPredicateBlocky { + const VoxelData &data; + const VoxelBlockyLibraryBase::BakedData &baked_data; + const uint32_t collision_mask; + const Vector3 p_from; + const Vector3 p_to; + + bool operator()(const VoxelRaycastState &rs) const { + VoxelSingleValue defval; + defval.i = 0; + const int v = data.get_voxel(rs.hit_position, VoxelBuffer::CHANNEL_TYPE, defval).i; + + if (baked_data.has_model(v) == false) { + return false; + } + + const VoxelBlockyModel::BakedData &model = baked_data.models[v]; + if ((model.box_collision_mask & collision_mask) == 0) { + return false; + } + + for (const AABB &aabb : model.box_collision_aabbs) { + if (AABB(aabb.position + rs.hit_position, aabb.size).intersects_segment(p_from, p_to)) { + return true; + } + } + + return false; + } + }; + + Ref res; + + Ref library_ref = mesher.get_library(); + if (library_ref.is_null()) { + return res; + } + + RaycastPredicateBlocky predicate{ + voxel_data, // + library_ref->get_baked_data(), // + p_collision_mask, // + ray_origin, // + ray_origin + ray_dir * max_distance // + }; + + float hit_distance; + float hit_distance_prev; + Vector3i hit_pos; + Vector3i prev_pos; + + if (zylann::voxel_raycast( + ray_origin, ray_dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev + )) { + res.instantiate(); + res->position = hit_pos; + res->previous_position = prev_pos; + res->distance_along_ray = hit_distance; + } + + return res; +} + +Ref raycast_nonzero( + const VoxelData &voxel_data, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint8_t p_channel +) { + struct RaycastPredicateColor { + const VoxelData &data; + const uint8_t channel; + + bool operator()(const VoxelRaycastState &rs) const { + VoxelSingleValue defval; + defval.i = 0; + const uint64_t v = data.get_voxel(rs.hit_position, channel, defval).i; + return v != 0; + } + }; + + Ref res; + + RaycastPredicateColor predicate{ voxel_data, p_channel }; + + float hit_distance; + float hit_distance_prev; + Vector3i hit_pos; + Vector3i prev_pos; + + if (zylann::voxel_raycast( + ray_origin, ray_dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev + )) { + res.instantiate(); + res->position = hit_pos; + res->previous_position = prev_pos; + res->distance_along_ray = hit_distance; + } + + return res; +} + +Ref raycast_generic( + const VoxelData &voxel_data, + const Ref mesher, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint32_t p_collision_mask, + const uint8_t binary_search_iterations +) { + using namespace zylann::godot; + + Ref res; + + Ref mesher_blocky; + Ref mesher_cubes; + + if (try_get_as(mesher, mesher_blocky)) { + res = raycast_blocky(voxel_data, **mesher_blocky, ray_origin, ray_dir, max_distance, p_collision_mask); + + } else if (try_get_as(mesher, mesher_cubes)) { + res = raycast_nonzero(voxel_data, ray_origin, ray_dir, max_distance, VoxelBuffer::CHANNEL_COLOR); + + } else { + res = raycast_sdf(voxel_data, ray_origin, ray_dir, max_distance, 0); + } + + return res; +} + +Ref raycast_generic_world( + const VoxelData &voxel_data, + const Ref mesher, + const Transform3D &to_world, + const Vector3 ray_origin_world, + const Vector3 ray_dir_world, + const float max_distance_world, + const uint32_t p_collision_mask, + const uint8_t binary_search_iterations +) { + // TODO Implement broad-phase on blocks to minimize locking and increase performance + + // TODO Optimization: voxel raycast uses `get_voxel` which is the slowest, but could be made faster. + // See `VoxelToolLodTerrain` for information about how to implement improvements. + + // TODO Switch to "from/to" parameters instead of "from/dir/distance" + + const Vector3 ray_end_world = ray_origin_world + ray_dir_world * max_distance_world; + + const Transform3D to_local = to_world.affine_inverse(); + + const Vector3 pos0_local = to_local.xform(ray_origin_world); + const Vector3 pos1_local = to_local.xform(ray_end_world); + + const float max_distance_local_sq = pos0_local.distance_squared_to(pos1_local); + if (max_distance_local_sq < 0.000001f) { + return Ref(); + } + const float max_distance_local = Math::sqrt(max_distance_local_sq); + const Vector3 dir_local = (pos1_local - pos0_local) / max_distance_local; + + Ref res = + raycast_generic(voxel_data, mesher, pos0_local, dir_local, max_distance_local, p_collision_mask, 0); + + if (res.is_valid()) { + const float max_distance_world_sq = ray_origin_world.distance_squared_to(ray_end_world); + const float to_world_scale = max_distance_world_sq / max_distance_local_sq; + + res->distance_along_ray = res->distance_along_ray * to_world_scale; + } + + return res; +} + +} // namespace zylann::voxel diff --git a/edition/raycast.h b/edition/raycast.h new file mode 100644 index 000000000..29ca1b80b --- /dev/null +++ b/edition/raycast.h @@ -0,0 +1,62 @@ +#ifndef VOXEL_RAYCAST_FUNCS_H +#define VOXEL_RAYCAST_FUNCS_H + +#include "../meshers/voxel_mesher.h" +#include "../util/math/transform_3d.h" +#include "../util/math/vector3.h" +#include "voxel_raycast_result.h" + +namespace zylann::voxel { + +class VoxelData; +class VoxelMesherBlocky; + +Ref raycast_sdf( + const VoxelData &voxel_data, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint8_t binary_search_iterations +); + +Ref raycast_blocky( + const VoxelData &voxel_data, + const VoxelMesherBlocky &mesher, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint32_t p_collision_mask +); + +Ref raycast_nonzero( + const VoxelData &voxel_data, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint8_t p_channel +); + +Ref raycast_generic( + const VoxelData &voxel_data, + const Ref mesher, + const Vector3 ray_origin, + const Vector3 ray_dir, + const float max_distance, + const uint32_t p_collision_mask, + const uint8_t binary_search_iterations +); + +Ref raycast_generic_world( + const VoxelData &voxel_data, + const Ref mesher, + const Transform3D &to_world, + const Vector3 ray_origin_world, + const Vector3 ray_dir_world, + const float max_distance_world, + const uint32_t p_collision_mask, + const uint8_t binary_search_iterations +); + +} // namespace zylann::voxel + +#endif // VOXEL_RAYCAST_FUNCS_H diff --git a/edition/voxel_mesh_sdf_gd.cpp b/edition/voxel_mesh_sdf_gd.cpp index 1c801c581..400ea4ac1 100644 --- a/edition/voxel_mesh_sdf_gd.cpp +++ b/edition/voxel_mesh_sdf_gd.cpp @@ -447,7 +447,7 @@ Dictionary VoxelMeshSDF::_b_get_data() const { d["res"] = vb.get_size(); PackedFloat32Array sdf_f32; - sdf_f32.resize(Vector3iUtil::get_volume(vb.get_size())); + sdf_f32.resize(Vector3iUtil::get_volume_u64(vb.get_size())); Span channel; ERR_FAIL_COND_V(!vb.get_channel_data_read_only(VoxelBuffer::CHANNEL_SDF, channel), Dictionary()); memcpy(sdf_f32.ptrw(), channel.data(), channel.size() * sizeof(float)); diff --git a/edition/voxel_tool_lod_terrain.cpp b/edition/voxel_tool_lod_terrain.cpp index 73cf50cce..67d02efea 100644 --- a/edition/voxel_tool_lod_terrain.cpp +++ b/edition/voxel_tool_lod_terrain.cpp @@ -7,18 +7,14 @@ #include "../terrain/variable_lod/voxel_lod_terrain.h" #include "../util/containers/std_vector.h" #include "../util/dstack.h" -#include "../util/godot/classes/collision_shape_3d.h" -#include "../util/godot/classes/convex_polygon_shape_3d.h" -#include "../util/godot/classes/mesh.h" -#include "../util/godot/classes/mesh_instance_3d.h" -#include "../util/godot/classes/rigid_body_3d.h" -#include "../util/godot/classes/timer.h" #include "../util/island_finder.h" #include "../util/math/conv.h" #include "../util/string/format.h" #include "../util/tasks/async_dependency_tracker.h" #include "../util/voxel_raycast.h" +#include "floating_chunks.h" #include "funcs.h" +#include "raycast.h" #include "voxel_mesh_sdf_gd.h" namespace zylann::voxel { @@ -34,144 +30,22 @@ bool VoxelToolLodTerrain::is_area_editable(const Box3i &box) const { return _terrain->get_storage().is_area_loaded(box); } -// Binary search can be more accurate than linear regression because the SDF can be inaccurate in the first place. -// An alternative would be to polygonize a tiny area around the middle-phase hit position. -// `d1` is how far from `pos0` along `dir` the binary search will take place. -// The segment may be adjusted internally if it does not contain a zero-crossing of the -template -float approximate_distance_to_isosurface_binary_search( - const Volume_F &f, - Vector3 pos0, - Vector3 dir, - float d1, - int iterations -) { - float d0 = 0.f; - float sdf0 = get_sdf_interpolated(f, pos0); - // The position given as argument may be a rough approximation coming from the middle-phase, - // so it can be slightly below the surface. We can adjust it a little so it is above. - for (int i = 0; i < 4 && sdf0 < 0.f; ++i) { - d0 -= 0.5f; - sdf0 = get_sdf_interpolated(f, pos0 + dir * d0); - } - - float sdf1 = get_sdf_interpolated(f, pos0 + dir * d1); - for (int i = 0; i < 4 && sdf1 > 0.f; ++i) { - d1 += 0.5f; - sdf1 = get_sdf_interpolated(f, pos0 + dir * d1); - } - - if ((sdf0 > 0) != (sdf1 > 0)) { - // Binary search - for (int i = 0; i < iterations; ++i) { - const float dm = 0.5f * (d0 + d1); - const float sdf_mid = get_sdf_interpolated(f, pos0 + dir * dm); - - if ((sdf_mid > 0) != (sdf0 > 0)) { - sdf1 = sdf_mid; - d1 = dm; - } else { - sdf0 = sdf_mid; - d0 = dm; - } - } - } - - // Pick distance closest to the surface - if (Math::abs(sdf0) < Math::abs(sdf1)) { - return d0; - } else { - return d1; - } -} - Ref VoxelToolLodTerrain::raycast( Vector3 pos, Vector3 dir, float max_distance, uint32_t collision_mask ) { - // TODO Transform input if the terrain is rotated - // TODO Implement reverse raycast? (going from inside ground to air, could be useful for undigging) - - // TODO Optimization: voxel raycast uses `get_voxel` which is the slowest, but could be made faster. - // Instead, do a broad-phase on blocks. If a block's voxels need to be parsed, get all positions the ray could go - // through in that block, then query them all at once (better for bulk processing without going again through - // locking and data structures, and allows SIMD). Then check results in order. - // If no hit is found, carry on with next blocks. - - struct RaycastPredicate { - VoxelData &data; - - bool operator()(const VoxelRaycastState &rs) { - // This is not particularly optimized, but runs fast enough for player raycasts - VoxelSingleValue defval; - defval.f = constants::SDF_FAR_OUTSIDE; - const VoxelSingleValue v = data.get_voxel(rs.hit_position, VoxelBuffer::CHANNEL_SDF, defval); - return v.f < 0; - } - }; - - Ref res; - - // We use grid-raycast as a middle-phase to roughly detect where the hit will be - RaycastPredicate predicate = { _terrain->get_storage() }; - Vector3i hit_pos; - Vector3i prev_pos; - float hit_distance; - float hit_distance_prev; - // Voxels polygonized using marching cubes influence a region centered on their lower corner, - // and extend up to 0.5 units in all directions. - // - // o--------o--------o - // | A | B | Here voxel B is full, voxels A, C and D are empty. - // | xxx | Matter will show up at the lower corner of B due to interpolation. - // | xxxxxxx | - // o---xxxxxoxxxxx---o - // | xxxxxxx | - // | xxx | - // | C | D | - // o--------o--------o - // - // `voxel_raycast` operates on a discrete grid of cubic voxels, so to account for the smooth interpolation, - // we may offset the ray so that cubes act as if they were centered on the filtered result. - const Vector3 offset(0.5, 0.5, 0.5); - if (voxel_raycast(pos + offset, dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev)) { - // Approximate surface - - float d = hit_distance; - - if (_raycast_binary_search_iterations > 0) { - // This is not particularly optimized, but runs fast enough for player raycasts - struct VolumeSampler { - VoxelData &data; - - inline float operator()(const Vector3i &pos) const { - VoxelSingleValue defval; - defval.f = constants::SDF_FAR_OUTSIDE; - const VoxelSingleValue value = data.get_voxel(pos, VoxelBuffer::CHANNEL_SDF, defval); - return value.f; - } - }; - - VolumeSampler sampler{ _terrain->get_storage() }; - d = hit_distance_prev + - approximate_distance_to_isosurface_binary_search( - sampler, - pos + dir * hit_distance_prev, - dir, - hit_distance - hit_distance_prev, - _raycast_binary_search_iterations - ); - } - - res.instantiate(); - res->position = hit_pos; - res->previous_position = prev_pos; - res->distance_along_ray = d; - } - - return res; + return raycast_generic_world( + _terrain->get_storage(), + _terrain->get_mesher(), + _terrain->get_global_transform(), + pos, + dir, + max_distance, + collision_mask, + _raycast_binary_search_iterations + ); } void VoxelToolLodTerrain::do_box(Vector3i begin, Vector3i end) { @@ -428,489 +302,6 @@ void VoxelToolLodTerrain::set_raycast_binary_search_iterations(int iterations) { _raycast_binary_search_iterations = math::clamp(iterations, 0, 16); } -void box_propagate_ccl(Span cells, const Vector3i size) { - ZN_PROFILE_SCOPE(); - - // Propagate non-zero cells towards zero cells in a 3x3x3 pattern. - // Used on a grid produced by Connected-Component-Labelling. - - // Z - { - ZN_PROFILE_SCOPE_NAMED("Z"); - Vector3i pos; - const int dz = size.x * size.y; - unsigned int i = 0; - for (pos.x = 0; pos.x < size.x; ++pos.x) { - for (pos.y = 0; pos.y < size.y; ++pos.y) { - // Note, border cells are not handled. Not just because it's more work, but also because that could - // make the label touch the edge, which is later interpreted as NOT being an island. - pos.z = 2; - i = Vector3iUtil::get_zxy_index(pos, size); - for (; pos.z < size.z - 2; ++pos.z, i += dz) { - const uint8_t c = cells[i]; - if (c != 0) { - if (cells[i - dz] == 0) { - cells[i - dz] = c; - } - if (cells[i + dz] == 0) { - cells[i + dz] = c; - // Skip next cell, otherwise it would cause endless propagation - i += dz; - ++pos.z; - } - } - } - } - } - } - - // X - { - ZN_PROFILE_SCOPE_NAMED("X"); - Vector3i pos; - const int dx = size.y; - unsigned int i = 0; - for (pos.z = 0; pos.z < size.z; ++pos.z) { - for (pos.y = 0; pos.y < size.y; ++pos.y) { - pos.x = 2; - i = Vector3iUtil::get_zxy_index(pos, size); - for (; pos.x < size.x - 2; ++pos.x, i += dx) { - const uint8_t c = cells[i]; - if (c != 0) { - if (cells[i - dx] == 0) { - cells[i - dx] = c; - } - if (cells[i + dx] == 0) { - cells[i + dx] = c; - i += dx; - ++pos.x; - } - } - } - } - } - } - - // Y - { - ZN_PROFILE_SCOPE_NAMED("Y"); - Vector3i pos; - const int dy = 1; - unsigned int i = 0; - for (pos.z = 0; pos.z < size.z; ++pos.z) { - for (pos.x = 0; pos.x < size.x; ++pos.x) { - pos.y = 2; - i = Vector3iUtil::get_zxy_index(pos, size); - for (; pos.y < size.y - 2; ++pos.y, i += dy) { - const uint8_t c = cells[i]; - if (c != 0) { - if (cells[i - dy] == 0) { - cells[i - dy] = c; - } - if (cells[i + dy] == 0) { - cells[i + dy] = c; - i += dy; - ++pos.y; - } - } - } - } - } - } -} - -// Turns floating chunks of voxels into rigidbodies: -// Detects separate groups of connected voxels within a box. Each group fully contained in the box is removed from -// the source volume, and turned into a rigidbody. -// This is one way of doing it, I don't know if it's the best way (there is rarely a best way) -// so there are probably other approaches that could be explored in the future, if they have better performance -Array separate_floating_chunks( - VoxelTool &voxel_tool, - Box3i world_box, - Node *parent_node, - Transform3D transform, - Ref mesher, - Array materials -) { - ZN_PROFILE_SCOPE(); - - // Checks - ERR_FAIL_COND_V(mesher.is_null(), Array()); - ERR_FAIL_COND_V(parent_node == nullptr, Array()); - - // Copy source data - - // TODO Do not assume channel, at the moment it's hardcoded for smooth terrain - static const int channels_mask = (1 << VoxelBuffer::CHANNEL_SDF); - static const VoxelBuffer::ChannelId main_channel = VoxelBuffer::CHANNEL_SDF; - - VoxelBuffer source_copy_buffer(VoxelBuffer::ALLOCATOR_POOL); - { - ZN_PROFILE_SCOPE_NAMED("Copy"); - source_copy_buffer.create(world_box.size); - voxel_tool.copy(world_box.position, source_copy_buffer, channels_mask); - } - - // Label distinct voxel groups - - static thread_local StdVector ccl_output; - ccl_output.resize(Vector3iUtil::get_volume(world_box.size)); - - unsigned int label_count = 0; - - { - // TODO Allow to run the algorithm at a different LOD, to trade precision for speed - ZN_PROFILE_SCOPE_NAMED("CCL scan"); - IslandFinder island_finder; - island_finder.scan_3d( - Box3i(Vector3i(), world_box.size), - [&source_copy_buffer](Vector3i pos) { - // TODO Can be optimized further with direct access - return source_copy_buffer.get_voxel_f(pos.x, pos.y, pos.z, main_channel) < 0.f; - }, - to_span(ccl_output), - &label_count - ); - } - - struct Bounds { - Vector3i min_pos; - Vector3i max_pos; // inclusive - bool valid = false; - }; - - if (main_channel == VoxelBuffer::CHANNEL_SDF) { - // Propagate labels to improve SDF quality, otherwise gradients of separated chunks would cut off abruptly. - // Limitation: if two islands are too close to each other, one will win over the other. - // An alternative could be to do this on individual chunks? - box_propagate_ccl(to_span(ccl_output), world_box.size); - } - - // Compute bounds of each group - - StdVector bounds_per_label; - { - ZN_PROFILE_SCOPE_NAMED("Bounds calculation"); - - // Adding 1 because label 0 is the index for "no label" - bounds_per_label.resize(label_count + 1); - - unsigned int ccl_index = 0; - for (int z = 0; z < world_box.size.z; ++z) { - for (int x = 0; x < world_box.size.x; ++x) { - for (int y = 0; y < world_box.size.y; ++y) { - CRASH_COND(ccl_index >= ccl_output.size()); - const uint8_t label = ccl_output[ccl_index]; - ++ccl_index; - - if (label == 0) { - continue; - } - - CRASH_COND(label >= bounds_per_label.size()); - Bounds &bounds = bounds_per_label[label]; - - if (bounds.valid == false) { - bounds.min_pos = Vector3i(x, y, z); - bounds.max_pos = bounds.min_pos; - bounds.valid = true; - - } else { - if (x < bounds.min_pos.x) { - bounds.min_pos.x = x; - } else if (x > bounds.max_pos.x) { - bounds.max_pos.x = x; - } - - if (y < bounds.min_pos.y) { - bounds.min_pos.y = y; - } else if (y > bounds.max_pos.y) { - bounds.max_pos.y = y; - } - - if (z < bounds.min_pos.z) { - bounds.min_pos.z = z; - } else if (z > bounds.max_pos.z) { - bounds.max_pos.z = z; - } - } - } - } - } - } - - // Eliminate groups that touch the box border, - // because that means we can't tell if they are truly hanging in the air or attached to land further away - - const Vector3i lbmax = world_box.size - Vector3i(1, 1, 1); - for (unsigned int label = 1; label < bounds_per_label.size(); ++label) { - CRASH_COND(label >= bounds_per_label.size()); - Bounds &local_bounds = bounds_per_label[label]; - ERR_CONTINUE(!local_bounds.valid); - - if ( // - local_bounds.min_pos.x == 0 // - || local_bounds.min_pos.y == 0 // - || local_bounds.min_pos.z == 0 // - || local_bounds.max_pos.x == lbmax.x // - || local_bounds.max_pos.y == lbmax.y // - || local_bounds.max_pos.z == lbmax.z) { - // - local_bounds.valid = false; - } - } - - // Create voxel buffer for each group - - struct InstanceInfo { - VoxelBuffer voxels; - Vector3i world_pos; - unsigned int label; - }; - StdVector instances_info; - - const int min_padding = 2; // mesher->get_minimum_padding(); - const int max_padding = 2; // mesher->get_maximum_padding(); - - { - ZN_PROFILE_SCOPE_NAMED("Extraction"); - - for (unsigned int label = 1; label < bounds_per_label.size(); ++label) { - CRASH_COND(label >= bounds_per_label.size()); - const Bounds local_bounds = bounds_per_label[label]; - - if (!local_bounds.valid) { - continue; - } - - const Vector3i world_pos = world_box.position + local_bounds.min_pos - Vector3iUtil::create(min_padding); - const Vector3i size = - local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding); - - instances_info.push_back(InstanceInfo{ VoxelBuffer(VoxelBuffer::ALLOCATOR_POOL), world_pos, label }); - - VoxelBuffer &buffer = instances_info.back().voxels; - buffer.create(size.x, size.y, size.z); - - // Read voxels from the source volume - voxel_tool.copy(world_pos, buffer, channels_mask); - - // Cleanup padding borders - const Box3i inner_box( - Vector3iUtil::create(min_padding), - buffer.get_size() - Vector3iUtil::create(min_padding + max_padding) - ); - Box3i(Vector3i(), buffer.get_size()).difference(inner_box, [&buffer](Box3i box) { - buffer.fill_area_f(constants::SDF_FAR_OUTSIDE, box.position, box.position + box.size, main_channel); - }); - - // Filter out voxels that don't belong to this label - for (int z = local_bounds.min_pos.z; z <= local_bounds.max_pos.z; ++z) { - for (int x = local_bounds.min_pos.x; x <= local_bounds.max_pos.x; ++x) { - for (int y = local_bounds.min_pos.y; y <= local_bounds.max_pos.y; ++y) { - const unsigned int ccl_index = Vector3iUtil::get_zxy_index(Vector3i(x, y, z), world_box.size); - CRASH_COND(ccl_index >= ccl_output.size()); - const uint8_t label2 = ccl_output[ccl_index]; - - if (label2 != 0 && label != label2) { - buffer.set_voxel_f( - constants::SDF_FAR_OUTSIDE, - min_padding + x - local_bounds.min_pos.x, - min_padding + y - local_bounds.min_pos.y, - min_padding + z - local_bounds.min_pos.z, - main_channel - ); - } - } - } - } - } - } - - // Erase voxels from source volume. - // Must be done after we copied voxels from it. - - { - ZN_PROFILE_SCOPE_NAMED("Erasing"); - - voxel_tool.set_channel(main_channel); - - for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) { - CRASH_COND(instance_index >= instances_info.size()); - const InstanceInfo &info = instances_info[instance_index]; - voxel_tool.sdf_stamp_erase(info.voxels, info.world_pos); - } - } - - // Find out which materials contain parameters that require instancing. - // - // Since 7dbc458bb4f3e0cc94e5070bd33bde41d214c98d it's no longer possible to quickly check if a - // shader has a uniform by name using Shader's parameter cache. Now it seems the only way is to get the whole list - // of parameters and find into it, which is slow, tedious to write and different between modules and GDExtension. - - uint32_t materials_to_instance_mask = 0; - { - StdVector params; - const String u_block_local_transform = VoxelStringNames::get_singleton().u_block_local_transform; - - ZN_ASSERT_RETURN_V_MSG( - materials.size() < 32, - Array(), - "Too many materials. If you need more, make a request or change the code." - ); - - for (int material_index = 0; material_index < materials.size(); ++material_index) { - Ref sm = materials[material_index]; - if (sm.is_null()) { - continue; - } - - Ref shader = sm->get_shader(); - if (shader.is_null()) { - continue; - } - - params.clear(); - zylann::godot::get_shader_parameter_list(shader->get_rid(), params); - - for (const zylann::godot::ShaderParameterInfo ¶m_info : params) { - if (param_info.name == u_block_local_transform) { - materials_to_instance_mask |= (1 << material_index); - break; - } - } - } - } - - // Create instances - - Array nodes; - - { - ZN_PROFILE_SCOPE_NAMED("Remeshing and instancing"); - - for (unsigned int instance_index = 0; instance_index < instances_info.size(); ++instance_index) { - CRASH_COND(instance_index >= instances_info.size()); - const InstanceInfo &info = instances_info[instance_index]; - - CRASH_COND(info.label >= bounds_per_label.size()); - const Bounds local_bounds = bounds_per_label[info.label]; - ERR_CONTINUE(!local_bounds.valid); - - // DEBUG - // print_line(String("--- Instance {0}").format(varray(instance_index))); - // for (int z = 0; z < info.voxels->get_size().z; ++z) { - // for (int x = 0; x < info.voxels->get_size().x; ++x) { - // String s; - // for (int y = 0; y < info.voxels->get_size().y; ++y) { - // float sdf = info.voxels->get_voxel_f(x, y, z, VoxelBuffer::CHANNEL_SDF); - // if (sdf < -0.1f) { - // s += "X "; - // } else if (sdf < 0.f) { - // s += "x "; - // } else { - // s += "- "; - // } - // } - // print_line(s); - // } - // print_line("//"); - // } - - const Transform3D local_transform( - Basis(), - info.world_pos - // Undo min padding - + Vector3i(1, 1, 1) - ); - - for (int i = 0; i < materials.size(); ++i) { - if ((materials_to_instance_mask & (1 << i)) != 0) { - Ref sm = materials[i]; - ZN_ASSERT_CONTINUE(sm.is_valid()); - sm = sm->duplicate(false); - // That parameter should have a valid default value matching the local transform relative to the - // volume, which is usually per-instance, but in Godot 3 we have no such feature, so we have to - // duplicate. - // TODO Try using per-instance parameters for scalar uniforms (Godot 4 doesn't support textures) - sm->set_shader_parameter( - VoxelStringNames::get_singleton().u_block_local_transform, local_transform - ); - materials[i] = sm; - } - } - - // TODO If normalmapping is used here with the Transvoxel mesher, we need to either turn it off just for - // this call, or to pass the right options - Ref mesh = mesher->build_mesh(info.voxels, materials, Dictionary()); - // The mesh is not supposed to be null, - // because we build these buffers from connected groups that had negative SDF. - ERR_CONTINUE(mesh.is_null()); - - if (zylann::godot::is_mesh_empty(**mesh)) { - continue; - } - - // DEBUG - // { - // Ref serializer; - // serializer.instance(); - // Ref peer; - // peer.instance(); - // serializer->serialize(peer, info.voxels, false); - // String fpath = String("debug_data/split_dump_{0}.bin").format(varray(instance_index)); - // FileAccess *f = FileAccess::open(fpath, FileAccess::WRITE); - // PoolByteArray bytes = peer->get_data_array(); - // PoolByteArray::Read bytes_read = bytes.read(); - // f->store_buffer(bytes_read.ptr(), bytes.size()); - // f->close(); - // memdelete(f); - // } - - // TODO Option to make multiple convex shapes - // TODO Use the fast way. This is slow because of the internal TriangleMesh thing and mesh data query. - // TODO Don't create a body if the mesh has no triangles - Ref shape = mesh->create_convex_shape(); - ERR_CONTINUE(shape.is_null()); - CollisionShape3D *collision_shape = memnew(CollisionShape3D); - collision_shape->set_shape(shape); - // Center the shape somewhat, because Godot is confusing node origin with center of mass - const Vector3i size = - local_bounds.max_pos - local_bounds.min_pos + Vector3iUtil::create(1 + max_padding + min_padding); - const Vector3 offset = -Vector3(size) * 0.5f; - collision_shape->set_position(offset); - - RigidBody3D *rigid_body = memnew(RigidBody3D); - rigid_body->set_transform(transform * local_transform.translated_local(-offset)); - rigid_body->add_child(collision_shape); - rigid_body->set_freeze_mode(RigidBody3D::FREEZE_MODE_KINEMATIC); - rigid_body->set_freeze_enabled(true); - - // Switch to rigid after a short time to workaround clipping with terrain, - // because colliders are updated asynchronously - Timer *timer = memnew(Timer); - timer->set_wait_time(0.2); - timer->set_one_shot(true); - timer->connect("timeout", callable_mp(rigid_body, &RigidBody3D::set_freeze_enabled).bind(false)); - // Cannot use start() here because it requires to be inside the SceneTree, - // and we don't know if it will be after we add to the parent. - timer->set_autostart(true); - rigid_body->add_child(timer); - - MeshInstance3D *mesh_instance = memnew(MeshInstance3D); - mesh_instance->set_mesh(mesh); - mesh_instance->set_position(offset); - rigid_body->add_child(mesh_instance); - - parent_node->add_child(rigid_body); - - nodes.append(rigid_body); - } - } - - return nodes; -} - #if defined(ZN_GODOT) Array VoxelToolLodTerrain::separate_floating_chunks(AABB world_box, Node *parent_node) { #elif defined(ZN_GODOT_EXTENSION) @@ -1038,7 +429,7 @@ void VoxelToolLodTerrain::do_graph(Ref graph, Transform3D t // Convert input SDF static thread_local StdVector tls_in_sdf_full; - tls_in_sdf_full.resize(Vector3iUtil::get_volume(buffer.get_size())); + tls_in_sdf_full.resize(Vector3iUtil::get_volume_u64(buffer.get_size())); Span in_sdf_full = to_span(tls_in_sdf_full); get_unscaled_sdf(buffer, in_sdf_full); diff --git a/edition/voxel_tool_terrain.cpp b/edition/voxel_tool_terrain.cpp index 489248ebd..46126a67f 100644 --- a/edition/voxel_tool_terrain.cpp +++ b/edition/voxel_tool_terrain.cpp @@ -9,7 +9,7 @@ #include "../util/godot/core/array.h" #include "../util/godot/core/packed_arrays.h" #include "../util/math/conv.h" -#include "../util/voxel_raycast.h" +#include "raycast.h" using namespace zylann::godot; @@ -37,126 +37,16 @@ Ref VoxelToolTerrain::raycast( float p_max_distance, uint32_t p_collision_mask ) { - // TODO Implement broad-phase on blocks to minimize locking and increase performance - - // TODO Optimization: voxel raycast uses `get_voxel` which is the slowest, but could be made faster. - // See `VoxelToolLodTerrain` for information about how to implement improvements. - - struct RaycastPredicateColor { - const VoxelData &data; - - bool operator()(const VoxelRaycastState &rs) const { - VoxelSingleValue defval; - defval.i = 0; - const uint64_t v = data.get_voxel(rs.hit_position, VoxelBuffer::CHANNEL_COLOR, defval).i; - return v != 0; - } - }; - - struct RaycastPredicateSDF { - const VoxelData &data; - - bool operator()(const VoxelRaycastState &rs) const { - const float v = data.get_voxel_f(rs.hit_position, VoxelBuffer::CHANNEL_SDF); - return v < 0; - } - }; - - struct RaycastPredicateBlocky { - const VoxelData &data; - const VoxelBlockyLibraryBase::BakedData &baked_data; - const uint32_t collision_mask; - const Vector3 p_from; - const Vector3 p_to; - - bool operator()(const VoxelRaycastState &rs) const { - VoxelSingleValue defval; - defval.i = 0; - const int v = data.get_voxel(rs.hit_position, VoxelBuffer::CHANNEL_TYPE, defval).i; - - if (baked_data.has_model(v) == false) { - return false; - } - - const VoxelBlockyModel::BakedData &model = baked_data.models[v]; - if ((model.box_collision_mask & collision_mask) == 0) { - return false; - } - - for (const AABB &aabb : model.box_collision_aabbs) { - if (AABB(aabb.position + rs.hit_position, aabb.size).intersects_segment(p_from, p_to)) { - return true; - } - } - - return false; - } - }; - - Ref res; - - Ref mesher_blocky; - Ref mesher_cubes; - - Vector3i hit_pos; - Vector3i prev_pos; - - const Transform3D to_world = _terrain->get_global_transform(); - const Transform3D to_local = to_world.affine_inverse(); - const Vector3 local_pos = to_local.xform(p_pos); - const Vector3 local_dir = to_local.basis.xform(p_dir).normalized(); - const float to_world_scale = to_world.basis.get_column(Vector3::AXIS_X).length(); - const float max_distance = p_max_distance / to_world_scale; - - if (try_get_as(_terrain->get_mesher(), mesher_blocky)) { - Ref library_ref = mesher_blocky->get_library(); - if (library_ref.is_null()) { - return res; - } - RaycastPredicateBlocky predicate{ _terrain->get_storage(), - library_ref->get_baked_data(), - p_collision_mask, - local_pos, - local_pos + local_dir * max_distance }; - float hit_distance; - float hit_distance_prev; - if (zylann::voxel_raycast( - local_pos, local_dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev - )) { - res.instantiate(); - res->position = hit_pos; - res->previous_position = prev_pos; - res->distance_along_ray = hit_distance * to_world_scale; - } - - } else if (try_get_as(_terrain->get_mesher(), mesher_cubes)) { - RaycastPredicateColor predicate{ _terrain->get_storage() }; - float hit_distance; - float hit_distance_prev; - if (zylann::voxel_raycast( - local_pos, local_dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev - )) { - res.instantiate(); - res->position = hit_pos; - res->previous_position = prev_pos; - res->distance_along_ray = hit_distance * to_world_scale; - } - - } else { - RaycastPredicateSDF predicate{ _terrain->get_storage() }; - float hit_distance; - float hit_distance_prev; - if (zylann::voxel_raycast( - local_pos, local_dir, predicate, max_distance, hit_pos, prev_pos, hit_distance, hit_distance_prev - )) { - res.instantiate(); - res->position = hit_pos; - res->previous_position = prev_pos; - res->distance_along_ray = hit_distance * to_world_scale; - } - } - - return res; + return raycast_generic_world( + _terrain->get_storage(), + _terrain->get_mesher(), + _terrain->get_global_transform(), + p_pos, + p_dir, + p_max_distance, + p_collision_mask, + 0 + ); } void VoxelToolTerrain::copy(Vector3i pos, VoxelBuffer &dst, uint8_t channels_mask) const { diff --git a/editor/about_window.cpp b/editor/about_window.cpp index d7c7d5f00..79422d457 100644 --- a/editor/about_window.cpp +++ b/editor/about_window.cpp @@ -255,7 +255,8 @@ VoxelAboutWindow::VoxelAboutWindow() { "SummitCollie\n" "nulshift\n" "ddel-rio (Daniel del Río Román)\n" - "Cyberphinx"; + "Cyberphinx\n" + "Mia (Tigxette)"; { Dictionary d; diff --git a/editor/vox/vox_mesh_importer.cpp b/editor/vox/vox_mesh_importer.cpp index 54e958bfc..b833d80fd 100644 --- a/editor/vox/vox_mesh_importer.cpp +++ b/editor/vox/vox_mesh_importer.cpp @@ -209,7 +209,7 @@ bool make_single_voxel_grid(Span instances, Vector3i &out_o // Extra sanity check // 3 gigabytes const size_t limit = 3'000'000'000ull; - const size_t volume = Vector3iUtil::get_volume(bounding_box.size); + const size_t volume = Vector3iUtil::get_volume_u64(bounding_box.size); ERR_FAIL_COND_V_MSG( volume > limit, false, diff --git a/engine/detail_rendering/detail_rendering.cpp b/engine/detail_rendering/detail_rendering.cpp index 504770c63..d19f67dbf 100644 --- a/engine/detail_rendering/detail_rendering.cpp +++ b/engine/detail_rendering/detail_rendering.cpp @@ -307,7 +307,7 @@ bool try_query_edited_blocks( { const Box3i voxel_box = Box3i::from_min_max(query_min_pos_i, query_max_pos_i); const Vector3i block_box_size = voxel_box.size >> constants::DEFAULT_BLOCK_SIZE_PO2; - const int64_t block_volume = Vector3iUtil::get_volume(block_box_size); + const int64_t block_volume = Vector3iUtil::get_volume_u64(block_box_size); // TODO Don't hardcode block size (even though for now I have no plan to make it configurable) if (block_volume > math::cubed(MAX_EDITED_BLOCKS_ACROSS)) { // Box too big for quick sparse readings, won't handle edits. Fallback on generator. @@ -699,7 +699,7 @@ void compute_detail_texture_data( Ref store_lookup_to_image(const StdVector &tiles, Vector3i block_size) { ZN_PROFILE_SCOPE(); - const unsigned int sqri = get_square_grid_size_from_item_count(Vector3iUtil::get_volume(block_size)); + const unsigned int sqri = get_square_grid_size_from_item_count(Vector3iUtil::get_volume_u64(block_size)); PackedByteArray bytes; { diff --git a/engine/gpu/compute_shader_resource.cpp b/engine/gpu/compute_shader_resource.cpp index 779bcef80..19feffdf8 100644 --- a/engine/gpu/compute_shader_resource.cpp +++ b/engine/gpu/compute_shader_resource.cpp @@ -140,12 +140,15 @@ void ComputeShaderResourceInternal::create_texture_2d(RenderingDevice &rd, const PackedByteArray data; data.resize(width * sizeof(float)); + const math::Interval curve_domain = zylann::godot::get_curve_domain(curve); + const float curve_domain_range = curve_domain.length(); + { uint8_t *wd8 = data.ptrw(); float *wd = (float *)wd8; for (unsigned int i = 0; i < width; ++i) { - const float t = i / static_cast(width); + const float t = curve_domain.min + curve_domain_range + i / static_cast(width); // TODO Thread-safety: `sample_baked` can actually be a WRITING method! The baked cache is lazily created wd[i] = curve.sample_baked(t); // print_line(String("X: {0}, Y: {1}").format(varray(t, wd[i]))); @@ -160,7 +163,7 @@ template void zxy_grid_to_zyx(Span src, Span dst, Vector3i size) { ZN_PROFILE_SCOPE(); ZN_ASSERT(Vector3iUtil::is_valid_size(size)); - ZN_ASSERT(Vector3iUtil::get_volume(size) == int64_t(src.size())); + ZN_ASSERT(Vector3iUtil::get_volume_u64(size) == src.size()); ZN_ASSERT(src.size() == dst.size()); Vector3i pos; for (pos.z = 0; pos.z < size.z; ++pos.z) { @@ -183,7 +186,8 @@ void ComputeShaderResourceInternal::create_texture_3d_float32( ZN_PRINT_VERBOSE(format("Creating VoxelRD texture3d {}x{}x{} float32", size.x, size.y, size.z)); ZN_ASSERT(Vector3iUtil::is_valid_size(size)); - const size_t expected_size_in_bytes = Vector3iUtil::get_volume(size) * sizeof(float); + + const size_t expected_size_in_bytes = Vector3iUtil::get_volume_u64(size) * sizeof(float); ZN_ASSERT(expected_size_in_bytes == static_cast(data.size())); clear(rd); @@ -315,7 +319,7 @@ std::shared_ptr ComputeShaderResourceFactory::create_text // Note, this array is refcounted so we can pass it to the async queue. It is also what the RD expects so we // minimize allocations for intermediate objects PackedByteArray pba; - pba.resize(sizeof(float) * Vector3iUtil::get_volume(size)); + pba.resize(sizeof(float) * Vector3iUtil::get_volume_u64(size)); uint8_t *pba_w = pba.ptrw(); zxy_grid_to_zyx(fdata_zxy, Span(reinterpret_cast(pba_w), pba.size()), size); diff --git a/generators/generate_block_gpu_task.cpp b/generators/generate_block_gpu_task.cpp index 54ce47481..a6a48c49c 100644 --- a/generators/generate_block_gpu_task.cpp +++ b/generators/generate_block_gpu_task.cpp @@ -28,7 +28,7 @@ GenerateBlockGPUTask::~GenerateBlockGPUTask() { unsigned int GenerateBlockGPUTask::get_required_shared_output_buffer_size() const { unsigned int volume = 0; for (const Box3i &box : boxes_to_generate) { - volume += Vector3iUtil::get_volume(box.size); + volume += Vector3iUtil::get_volume_u64(box.size); } // All outputs are floats at the moment... return generator_shader_outputs->outputs.size() * volume * sizeof(float); @@ -73,7 +73,7 @@ void GenerateBlockGPUTask::prepare(GPUTaskContext &ctx) { BoxData &bd = _boxes_data[i]; const Box3i box = boxes_to_generate[i]; const Vector3i buffer_resolution = box.size; - const unsigned int buffer_volume = Vector3iUtil::get_volume(buffer_resolution); + const unsigned int buffer_volume = Vector3iUtil::get_volume_u64(buffer_resolution); // Params @@ -441,7 +441,7 @@ void GenerateBlockGPUTask::collect(GPUTaskContext &ctx) { const Box3i box = boxes_to_generate[box_index]; // Every output is the same size for now - const unsigned int size_per_output = Vector3iUtil::get_volume(box.size) * sizeof(float); + const unsigned int size_per_output = Vector3iUtil::get_volume_u64(box.size) * sizeof(float); for (unsigned int output_index = 0; output_index < generator_shader_outputs->outputs.size(); ++output_index) { const VoxelGenerator::ShaderOutput &output_info = generator_shader_outputs->outputs[output_index]; diff --git a/generators/graph/nodes/curve.h b/generators/graph/nodes/curve.h index 2c84d8a07..2923e5508 100644 --- a/generators/graph/nodes/curve.h +++ b/generators/graph/nodes/curve.h @@ -75,13 +75,22 @@ void register_curve_node(Span types) { } std::shared_ptr res = ComputeShaderResourceFactory::create_texture_2d(curve); const StdString uniform_texture = ctx.add_uniform(std::move(res)); + + // In Godot 4.4 Curves can be defined beyond 0..1 + const Interval curve_domain = zylann::godot::get_curve_domain(**curve); + const float curve_domain_range = curve_domain.length(); + const float x_remap_a = 1.f / math::max(curve_domain_range, 0.0001f); + const float x_remap_b = -curve_domain.min * x_remap_a; + // We are offsetting X to match the interpolation Godot's Curve does, because the default linear // interpolation sampler is offset by half a pixel ctx.add_format( - "{} = texture({}, vec2({} + 0.5 / float(textureSize({}, 0).x), 0.0)).r;\n", + "{} = texture({}, vec2({} * {} + {} + 0.5 / float(textureSize({}, 0).x), 0.0)).r;\n", ctx.get_output_name(0), uniform_texture, + x_remap_a, ctx.get_input_name(0), + x_remap_b, uniform_texture ); }; diff --git a/generators/graph/program_graph.cpp b/generators/graph/program_graph.cpp index 26e3abfce..0f38c94eb 100644 --- a/generators/graph/program_graph.cpp +++ b/generators/graph/program_graph.cpp @@ -163,7 +163,8 @@ void ProgramGraph::connect(PortLocation src, PortLocation dst) { ZN_ASSERT_RETURN_MSG(src.port_index < src_node.outputs.size(), "Source port doesn't exist"); ZN_ASSERT_RETURN_MSG(dst.port_index < dst_node.inputs.size(), "Destination port doesn't exist"); ZN_ASSERT_RETURN_MSG( - dst_node.inputs[dst.port_index].connections.size() == 0, "Destination node's port is already connected"); + dst_node.inputs[dst.port_index].connections.size() == 0, "Destination node's port is already connected" + ); src_node.outputs[src.port_index].connections.push_back(dst); dst_node.inputs[dst.port_index].connections.push_back(src); } @@ -268,16 +269,19 @@ void ProgramGraph::find_terminal_nodes(StdVector &node_ids) const { } void ProgramGraph::find_dependencies(uint32_t node_id, StdVector &out_order) const { - StdVector nodes_to_process; - nodes_to_process.push_back(node_id); - find_dependencies(nodes_to_process, out_order); + find_dependencies(to_single_element_span(node_id), out_order); } // Finds dependencies of the given nodes, and returns them in the order they should be processed. // Given nodes are included in the result. -void ProgramGraph::find_dependencies(StdVector nodes_to_process, StdVector &out_order) const { +void ProgramGraph::find_dependencies(Span p_nodes_to_process, StdVector &out_order) const { StdUnorderedSet visited_nodes; + // TODO Candidate for temp allocator + StdVector nodes_to_process; + nodes_to_process.resize(p_nodes_to_process.size()); + p_nodes_to_process.copy_to(to_span(nodes_to_process)); + while (nodes_to_process.size() > 0) { found: // The loop can come back multiple times to the same node, until all its dependencies have been processed. diff --git a/generators/graph/program_graph.h b/generators/graph/program_graph.h index 5a275ac0f..57b11ac9f 100644 --- a/generators/graph/program_graph.h +++ b/generators/graph/program_graph.h @@ -88,7 +88,7 @@ class ProgramGraph : NonCopyable { bool has_path(uint32_t p_src_node_id, uint32_t p_dst_node_id) const; void find_dependencies(uint32_t node_id, StdVector &out_order) const; - void find_dependencies(StdVector nodes_to_process, StdVector &out_order) const; + void find_dependencies(Span p_nodes_to_process, StdVector &out_order) const; void find_immediate_dependencies(uint32_t node_id, StdVector &deps) const; void find_terminal_nodes(StdVector &node_ids) const; diff --git a/generators/graph/range_utility.cpp b/generators/graph/range_utility.cpp index c8ec71de8..0822fbdae 100644 --- a/generators/graph/range_utility.cpp +++ b/generators/graph/range_utility.cpp @@ -11,13 +11,16 @@ using namespace math; // Curve /////////////////////////////////////////////////////////////////////////////////////////////////////////////// void get_curve_monotonic_sections(Curve &curve, StdVector §ions) { + const Interval curve_domain = zylann::godot::get_curve_domain(curve); + const float curve_domain_range = curve_domain.length(); + const int res = curve.get_bake_resolution(); - float prev_y = curve.sample_baked(0.f); + float prev_y = curve.sample_baked(curve_domain.min); sections.clear(); CurveMonotonicSection section; - section.x_min = 0.f; - section.y_min = curve.sample_baked(0.f); + section.x_min = curve_domain.min; + section.y_min = curve.sample_baked(curve_domain.min); float prev_x = 0.f; bool current_stationary = true; @@ -27,7 +30,7 @@ void get_curve_monotonic_sections(Curve &curve, StdVector // made it apparent that our code didn't properly include the end of the curve) for (int i = 1; i < res; ++i) { // We do -1 because [res-1] is the last value in the baked array, therefore `x` must be 1 - const float x = static_cast(i) / (res - 1); + const float x = curve_domain.min + curve_domain_range * static_cast(i) / (res - 1); const float y = curve.sample_baked(x); // Curve can sometimes appear flat but it still oscillates by very small amounts due to float imprecision // which occurred during bake(). Attempting to workaround that by taking the error into account @@ -56,8 +59,8 @@ void get_curve_monotonic_sections(Curve &curve, StdVector prev_y = y; } - // Forcing 1 because the iteration doesn't go up to `res` - section.x_max = 1.f; + // Forcing max because the iteration doesn't go up to `res` + section.x_max = curve_domain.max; section.y_max = prev_y; sections.push_back(section); } @@ -67,9 +70,10 @@ Interval get_curve_range(Curve &curve, const StdVector &s // If a curve has too many points, we may consider dynamically choosing a different algorithm. Interval y; unsigned int i = 0; - if (x.min < sections[0].x_min) { + const float x_min = sections[0].x_min; + if (x.min < x_min) { // X range starts before the curve's minimum X - y = Interval::from_single_value(curve.sample_baked(0.f)); + y = Interval::from_single_value(curve.sample_baked(x_min)); } else { // Find section from where the range starts for (; i < sections.size(); ++i) { @@ -108,12 +112,15 @@ Interval get_curve_range(Curve &curve, bool &is_monotonic_increasing) { // TODO Would be nice to have the cache directly const int res = curve.get_bake_resolution(); Interval range; - float prev_v = curve.sample_baked(0.f); - if (curve.sample_baked(1.f) > prev_v) { + const Interval curve_domain = zylann::godot::get_curve_domain(curve); + const float curve_domain_range = curve_domain.length(); + float prev_v = curve.sample_baked(curve_domain.min); + if (curve.sample_baked(curve_domain.max) > prev_v) { is_monotonic_increasing = true; } for (int i = 0; i < res; ++i) { - const float v = curve.sample_baked(static_cast(i) / res); + const float a = curve_domain.min + curve_domain_range * static_cast(i) / res; + const float v = curve.sample_baked(a); range.add_point(v); if (v < prev_v) { is_monotonic_increasing = false; diff --git a/generators/graph/voxel_generator_graph.cpp b/generators/graph/voxel_generator_graph.cpp index 937e4f86e..9a00e93e5 100644 --- a/generators/graph/voxel_generator_graph.cpp +++ b/generators/graph/voxel_generator_graph.cpp @@ -599,7 +599,7 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel cache.input_sdf_slice_cache.resize(slice_buffer_size); input_sdf_slice_cache = to_span(cache.input_sdf_slice_cache); - const int64_t volume = Vector3iUtil::get_volume(bs); + const size_t volume = Vector3iUtil::get_volume_u64(bs); cache.input_sdf_full_cache.resize(volume); input_sdf_full_cache = to_span(cache.input_sdf_full_cache); diff --git a/generators/graph/voxel_graph_compiler.cpp b/generators/graph/voxel_graph_compiler.cpp index ecc40168f..5da607eb7 100644 --- a/generators/graph/voxel_graph_compiler.cpp +++ b/generators/graph/voxel_graph_compiler.cpp @@ -1343,7 +1343,7 @@ void compute_node_execution_order( }); } - graph.find_dependencies(terminal_nodes, order); + graph.find_dependencies(to_span(terminal_nodes), order); } } // namespace diff --git a/generators/graph/voxel_graph_function.cpp b/generators/graph/voxel_graph_function.cpp index 2bbbc85cf..15992c621 100644 --- a/generators/graph/voxel_graph_function.cpp +++ b/generators/graph/voxel_graph_function.cpp @@ -715,7 +715,7 @@ uint64_t VoxelGraphFunction::get_output_graph_hash() const { std::sort(terminal_nodes.begin(), terminal_nodes.end()); StdVector order; - _graph.find_dependencies(terminal_nodes, order); + _graph.find_dependencies(to_span(terminal_nodes), order); StdVector node_hashes; uint64_t hash = hash_djb2_one_64(0); @@ -754,9 +754,7 @@ uint64_t VoxelGraphFunction::get_output_graph_hash() const { #endif void VoxelGraphFunction::find_dependencies(uint32_t node_id, StdVector &out_dependencies) const { - StdVector dst; - dst.push_back(node_id); - _graph.find_dependencies(dst, out_dependencies); + _graph.find_dependencies(to_single_element_span(node_id), out_dependencies); } const ProgramGraph &VoxelGraphFunction::get_graph() const { diff --git a/generators/graph/voxel_graph_shader_generator.cpp b/generators/graph/voxel_graph_shader_generator.cpp index c1a51bb01..c17c3d392 100644 --- a/generators/graph/voxel_graph_shader_generator.cpp +++ b/generators/graph/voxel_graph_shader_generator.cpp @@ -80,7 +80,7 @@ CompilationResult generate_shader( // return type.debug_only; // }); - expanded_graph.find_dependencies(terminal_nodes, order); + expanded_graph.find_dependencies(to_span(terminal_nodes), order); StdStringStream main_ss; StdStringStream lib_ss; diff --git a/generators/multipass/voxel_generator_multipass_cb.cpp b/generators/multipass/voxel_generator_multipass_cb.cpp index 4fdd16767..3a7ed8ce7 100644 --- a/generators/multipass/voxel_generator_multipass_cb.cpp +++ b/generators/multipass/voxel_generator_multipass_cb.cpp @@ -287,6 +287,8 @@ void VoxelGeneratorMultipassCB::process_viewer_diff_internal(Box3i p_requested_b const int column_height = internal->column_height_blocks; load_requested_box.difference(prev_load_requested_box, [&map, column_height](Box2i new_box) { { + ZN_PROFILE_SCOPE_NAMED("Enter box"); + SpatialLock2D::Write swlock(map.spatial_lock, new_box); MutexLock mlock(map.mutex); @@ -308,9 +310,18 @@ void VoxelGeneratorMultipassCB::process_viewer_diff_internal(Box3i p_requested_b // Blocks to unview prev_load_requested_box.difference(load_requested_box, [&map, &task_scheduler](Box2i old_box) { + ZN_PROFILE_SCOPE_NAMED("Leave box (locking)"); + + // TODO This can be a bottleneck if the generator is slow and a player teleports far away while columns are + // still generating. Could take a second of freezing. + // Not sure of the best approach to this. Delegate this somehow to generation tasks so the main thread is not + // affected? Use a coroutine that keeps continuing from here and retries to lock? This is tricky because we need + // symmetry when entering new chunks to ensure consistency, which is also done on the main thread earlier + SpatialLock2D::Write swlock(map.spatial_lock, old_box); + MutexLock mlock(map.mutex); + { - SpatialLock2D::Write swlock(map.spatial_lock, old_box); - MutexLock mlock(map.mutex); + ZN_PROFILE_SCOPE_NAMED("Leave box"); old_box.for_each_cell_yx([&map, &task_scheduler](Vector2i cpos) { auto it = map.columns.find(cpos); diff --git a/generators/simple/voxel_generator_noise.h b/generators/simple/voxel_generator_noise.h index 8a0fa2077..2c9417fe4 100644 --- a/generators/simple/voxel_generator_noise.h +++ b/generators/simple/voxel_generator_noise.h @@ -47,8 +47,8 @@ class VoxelGeneratorNoise : public VoxelGenerator { struct Parameters { VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; Ref noise; - float height_start = 0; - float height_range = 300; + float height_start = -100; + float height_range = 200; }; Parameters _parameters; diff --git a/meshers/blocky/voxel_blocky_model.cpp b/meshers/blocky/voxel_blocky_model.cpp index d16ac9e79..c99f83736 100644 --- a/meshers/blocky/voxel_blocky_model.cpp +++ b/meshers/blocky/voxel_blocky_model.cpp @@ -217,6 +217,14 @@ void VoxelBlockyModel::set_culls_neighbors(bool cn) { _culls_neighbors = cn; } +void VoxelBlockyModel::set_lod_skirts_enabled(bool enabled) { + _lod_skirts = enabled; +} + +bool VoxelBlockyModel::get_lod_skirts_enabled() const { + return _lod_skirts; +} + void VoxelBlockyModel::set_surface_count(unsigned int new_count) { if (new_count != _surface_count) { _surface_count = new_count; @@ -241,9 +249,12 @@ void VoxelBlockyModel::bake(BakedData &baked_data, bool bake_tangents, MaterialI baked_data.is_random_tickable = _random_tickable; baked_data.box_collision_mask = _collision_mask; baked_data.box_collision_aabbs = _collision_aabbs; + baked_data.lod_skirts = _lod_skirts; BakedData::Model &model = baked_data.model; + // Note: mesh rotation is not implemented here, it is done in derived classes. + // Set empty sides mask model.empty_sides_mask = 0; for (unsigned int side = 0; side < Cube::SIDE_COUNT; ++side) { @@ -589,11 +600,15 @@ void VoxelBlockyModel::_bind_methods() { // Bound for editor purposes ClassDB::bind_method(D_METHOD("rotate_90", "axis", "clockwise"), &VoxelBlockyModel::_b_rotate_90); + ClassDB::bind_method(D_METHOD("set_lod_skirts_enabled", "enabled"), &VoxelBlockyModel::set_lod_skirts_enabled); + ClassDB::bind_method(D_METHOD("get_lod_skirts_enabled"), &VoxelBlockyModel::get_lod_skirts_enabled); + // TODO Update to StringName in Godot 4 ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); ADD_PROPERTY(PropertyInfo(Variant::INT, "transparency_index"), "set_transparency_index", "get_transparency_index"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "culls_neighbors"), "set_culls_neighbors", "get_culls_neighbors"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "random_tickable"), "set_random_tickable", "is_random_tickable"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "lod_skirts_enabled"), "set_lod_skirts_enabled", "get_lod_skirts_enabled"); ADD_GROUP("Box collision", ""); @@ -607,12 +622,15 @@ void VoxelBlockyModel::_bind_methods() { "get_collision_aabbs" ); ADD_PROPERTY( + // TODO This collision mask might not actually be related to Godot standard physics. + // It is mostly used in voxel raycasts, box collision and maybe other things PropertyInfo(Variant::INT, "collision_mask", PROPERTY_HINT_LAYERS_3D_PHYSICS), "set_collision_mask", "get_collision_mask" ); - ADD_GROUP("Rotation", ""); + // Note: rotation property is currently exposed only in derived classes. + // It will not necessarily be supported by all derived classes. BIND_ENUM_CONSTANT(SIDE_NEGATIVE_X); BIND_ENUM_CONSTANT(SIDE_POSITIVE_X); diff --git a/meshers/blocky/voxel_blocky_model.h b/meshers/blocky/voxel_blocky_model.h index 1099804f7..bddc8e3b8 100644 --- a/meshers/blocky/voxel_blocky_model.h +++ b/meshers/blocky/voxel_blocky_model.h @@ -110,6 +110,7 @@ class VoxelBlockyModel : public Resource { bool empty; bool is_random_tickable; bool is_transparent; + bool lod_skirts; uint32_t box_collision_mask; StdVector box_collision_aabbs; @@ -171,6 +172,9 @@ class VoxelBlockyModel : public Resource { void set_mesh_ortho_rotation_index(int i); int get_mesh_ortho_rotation_index() const; + void set_lod_skirts_enabled(bool rt); + bool get_lod_skirts_enabled() const; + //------------------------------------------ // Properties for internal usage only @@ -257,6 +261,8 @@ class VoxelBlockyModel : public Resource { bool _random_tickable = false; uint8_t _mesh_ortho_rotation = 0; + bool _lod_skirts = true; + Color _color; LegacyProperties _legacy_properties; diff --git a/meshers/blocky/voxel_blocky_model_cube.cpp b/meshers/blocky/voxel_blocky_model_cube.cpp index 2bcf63e99..4a38dcf70 100644 --- a/meshers/blocky/voxel_blocky_model_cube.cpp +++ b/meshers/blocky/voxel_blocky_model_cube.cpp @@ -331,6 +331,14 @@ void VoxelBlockyModelCube::_bind_methods() { ADD_PROPERTY( PropertyInfo(Variant::VECTOR2I, "atlas_size_in_tiles"), "set_atlas_size_in_tiles", "get_atlas_size_in_tiles" ); + + // ADD_GROUP("Rotation", ""); + + ADD_PROPERTY( + PropertyInfo(Variant::INT, "mesh_ortho_rotation_index", PROPERTY_HINT_RANGE, "0,24"), + "set_mesh_ortho_rotation_index", + "get_mesh_ortho_rotation_index" + ); } } // namespace zylann::voxel diff --git a/meshers/blocky/voxel_mesher_blocky.cpp b/meshers/blocky/voxel_mesher_blocky.cpp index a99812cc1..efcd16734 100644 --- a/meshers/blocky/voxel_mesher_blocky.cpp +++ b/meshers/blocky/voxel_mesher_blocky.cpp @@ -876,7 +876,7 @@ int get_side_sign(const VoxelBlockyModel::Side side) { // cracks, but the assumption is that it will do most of the time. // AO is not handled, and probably doesn't need to be template -void append_side_seams( +void append_side_skirts( Span buffer, const Vector3T jump, const int z, // Coordinate of the first or last voxel (not within the padded region) @@ -931,6 +931,21 @@ void append_side_seams( const Vector3f pos = side_to_block_coordinates(Vector3f(x - pad, y - pad, z - (side_sign + 1)), side); const VoxelBlockyModel::BakedData &voxel = library.models[nv4]; + + if (!voxel.lod_skirts) { + // A typical issue is making an ocean: + // - Skirts will show up behind the water surface so it's not a good solution in that case. + // - If sea level does not line up at different LODs, then there will be LOD "cracks" anyways. I don't + // have a good solution for this. One way to workaround is to choose a sea level that lines up at every + // LOD (such as Y=0), and let the seams occur in other cases which are usually way less frequent. + // - Another way is to only reduce LOD resolution horizontally and not vertically, but that has a high + // memory cost on large distances, so not silver bullet. + // - Make water opaque when at large distances? If acceptable, this can be a good fix (Distant Horizons + // mod was doing this at some point) but either require custom shader or the ability to specify + // different models for different LODs in the library + continue; + } + const VoxelBlockyModel::BakedData::Model &model = voxel.model; for (unsigned int surface_index = 0; surface_index < model.surface_count; ++surface_index) { @@ -1005,7 +1020,7 @@ void append_side_seams( } template -void append_seams( +void append_skirts( Span buffer, const Vector3i size, StdVector &out_arrays_per_material, @@ -1024,12 +1039,12 @@ void append_seams( constexpr VoxelBlockyModel::Side NEGATIVE_Z = VoxelBlockyModel::SIDE_NEGATIVE_Z; constexpr VoxelBlockyModel::Side POSITIVE_Z = VoxelBlockyModel::SIDE_POSITIVE_Z; - append_side_seams(buffer, jump.xyz(), 0, size.x, size.y, NEGATIVE_Z, library, out); - append_side_seams(buffer, jump.xyz(), (size.z - 1), size.x, size.y, POSITIVE_Z, library, out); - append_side_seams(buffer, jump.zyx(), 0, size.z, size.y, NEGATIVE_X, library, out); - append_side_seams(buffer, jump.zyx(), (size.x - 1), size.z, size.y, POSITIVE_X, library, out); - append_side_seams(buffer, jump.zxy(), 0, size.z, size.x, NEGATIVE_Y, library, out); - append_side_seams(buffer, jump.zxy(), (size.y - 1), size.z, size.x, POSITIVE_Y, library, out); + append_side_skirts(buffer, jump.xyz(), 0, size.x, size.y, NEGATIVE_Z, library, out); + append_side_skirts(buffer, jump.xyz(), (size.z - 1), size.x, size.y, POSITIVE_Z, library, out); + append_side_skirts(buffer, jump.zyx(), 0, size.z, size.y, NEGATIVE_X, library, out); + append_side_skirts(buffer, jump.zyx(), (size.x - 1), size.z, size.y, POSITIVE_X, library, out); + append_side_skirts(buffer, jump.zxy(), 0, size.z, size.x, NEGATIVE_Y, library, out); + append_side_skirts(buffer, jump.zxy(), (size.y - 1), size.z, size.x, POSITIVE_Y, library, out); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1182,17 +1197,17 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In switch (channel_depth) { case VoxelBuffer::DEPTH_8_BIT: - generate_blocky_mesh( // - arrays_per_material, // - collision_surface, // - raw_channel, // - block_size, // - library_baked_data, // - params.bake_occlusion, // - baked_occlusion_darkness // + generate_blocky_mesh( + arrays_per_material, + collision_surface, + raw_channel, + block_size, + library_baked_data, + params.bake_occlusion, + baked_occlusion_darkness ); if (input.lod_index > 0) { - append_seams(raw_channel, block_size, arrays_per_material, library_baked_data); + append_skirts(raw_channel, block_size, arrays_per_material, library_baked_data); } break; @@ -1208,7 +1223,7 @@ void VoxelMesherBlocky::build(VoxelMesher::Output &output, const VoxelMesher::In baked_occlusion_darkness ); if (input.lod_index > 0) { - append_seams(model_ids, block_size, arrays_per_material, library_baked_data); + append_skirts(model_ids, block_size, arrays_per_material, library_baked_data); } } break; diff --git a/meshers/transvoxel/transvoxel.cpp b/meshers/transvoxel/transvoxel.cpp index a41780f93..ba3e45738 100644 --- a/meshers/transvoxel/transvoxel.cpp +++ b/meshers/transvoxel/transvoxel.cpp @@ -1109,7 +1109,7 @@ Span get_or_decompress_channel(const VoxelBuffer &voxels, StdVector ); if (voxels.get_channel_compression(channel) == VoxelBuffer::COMPRESSION_UNIFORM) { - backing_buffer.resize(Vector3iUtil::get_volume(voxels.get_size())); + backing_buffer.resize(Vector3iUtil::get_volume_u64(voxels.get_size())); const T v = voxels.get_voxel(Vector3i(), channel); // TODO Could use a fast fill using 8-byte blocks or intrinsics? for (unsigned int i = 0; i < backing_buffer.size(); ++i) { @@ -1231,10 +1231,14 @@ inline void build_regular_mesh_dispatch_sd( ); } break; - case VoxelBuffer::DEPTH_64_BIT: - ZN_PRINT_ERROR("Double-precision SDF channel is not supported"); - // Not worth growing executable size for relatively pointless double-precision sdf - break; + case VoxelBuffer::DEPTH_64_BIT: { + static bool s_once = false; + if (s_once == false) { + s_once = true; + ZN_PRINT_ERROR("Double-precision SDF channel is not supported"); + // Not worth growing executable size for relatively pointless double-precision sdf + } + } break; default: ZN_PRINT_ERROR("Invalid channel"); @@ -1256,7 +1260,7 @@ DefaultTextureIndicesData build_regular_mesh( ZN_PROFILE_SCOPE(); // From this point, we expect the buffer to contain allocated data in the relevant channels. - const unsigned int voxels_count = Vector3iUtil::get_volume(voxels.get_size()); + const unsigned int voxels_count = Vector3iUtil::get_volume_u64(voxels.get_size()); output.clear(); @@ -1400,7 +1404,7 @@ void build_transition_mesh( ZN_PROFILE_SCOPE(); // From this point, we expect the buffer to contain allocated data in the relevant channels. - const unsigned int voxels_count = Vector3iUtil::get_volume(voxels.get_size()); + const unsigned int voxels_count = Vector3iUtil::get_volume_u64(voxels.get_size()); switch (texturing_mode) { case TEXTURES_NONE: diff --git a/modifiers/voxel_modifier_stack.cpp b/modifiers/voxel_modifier_stack.cpp index e0e6071f8..12d4b51a9 100644 --- a/modifiers/voxel_modifier_stack.cpp +++ b/modifiers/voxel_modifier_stack.cpp @@ -18,7 +18,7 @@ StdVector &get_tls_positions() { } void get_positions_buffer(Vector3i buffer_size, Vector3f origin, Vector3f size, StdVector &positions) { - positions.resize(Vector3iUtil::get_volume(buffer_size)); + positions.resize(Vector3iUtil::get_volume_u64(buffer_size)); const Vector3f end = origin + size; const Vector3f inv_bsf = Vector3f(1.0, 1.0, 1.0) / to_vec3f(buffer_size); @@ -64,7 +64,7 @@ Span get_positions_temporary( void decompress_sdf_to_buffer(VoxelBuffer &voxels, StdVector &sdf) { ZN_DSTACK(); - sdf.resize(Vector3iUtil::get_volume(voxels.get_size())); + sdf.resize(Vector3iUtil::get_volume_u64(voxels.get_size())); const VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; voxels.decompress_channel(channel); @@ -221,7 +221,7 @@ void VoxelModifierStack::apply(VoxelBuffer &voxels, AABB aabb) const { modifier_box.clip(Box3i(origin_voxels, voxels.get_size())); const Vector3i local_origin_in_voxels = modifier_box.position - origin_voxels; - const int64_t volume = Vector3iUtil::get_volume(modifier_box.size); + const size_t volume = Vector3iUtil::get_volume_u64(modifier_box.size); area_sdf.resize(volume); copy_3d_region_zxy( to_span(area_sdf), diff --git a/storage/funcs.cpp b/storage/funcs.cpp index 2652f0732..6c6d439bb 100644 --- a/storage/funcs.cpp +++ b/storage/funcs.cpp @@ -33,8 +33,8 @@ void copy_3d_region_zxy( ZN_PRINT_ERROR("Different overlapping spans are not allowed"); return; } - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) * item_size <= dst.size()); - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) * item_size <= src.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) * item_size <= dst.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) * item_size <= src.size()); #endif if (area_size == src_size && area_size == dst_size) { diff --git a/storage/funcs.h b/storage/funcs.h index 43a1d75e3..81dd418fe 100644 --- a/storage/funcs.h +++ b/storage/funcs.h @@ -96,7 +96,7 @@ void fill_3d_region_zxy(Span dst, Vector3i dst_size, Vector3i dst_min, Vector } #ifdef DEBUG_ENABLED - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) <= dst.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) <= dst.size()); #endif if (area_size == dst_size) { @@ -200,8 +200,8 @@ Vector3i transform_3d_array_zxy(Span src_grid, Span dst_grid, Vector ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.x), src_size); ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.y), src_size); ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.z), src_size); - ZN_ASSERT_RETURN_V(src_grid.size() == static_cast(Vector3iUtil::get_volume(src_size)), src_size); - ZN_ASSERT_RETURN_V(dst_grid.size() == static_cast(Vector3iUtil::get_volume(src_size)), src_size); + ZN_ASSERT_RETURN_V(src_grid.size() == Vector3iUtil::get_volume_u64(src_size), src_size); + ZN_ASSERT_RETURN_V(dst_grid.size() == Vector3iUtil::get_volume_u64(src_size), src_size); const int xa = basis.x.x != 0 ? 0 : basis.x.y != 0 ? 1 : 2; const int ya = basis.y.x != 0 ? 0 : basis.y.y != 0 ? 1 : 2; diff --git a/storage/voxel_buffer.cpp b/storage/voxel_buffer.cpp index 9d92b6683..b8718c06d 100644 --- a/storage/voxel_buffer.cpp +++ b/storage/voxel_buffer.cpp @@ -1088,7 +1088,7 @@ void VoxelBuffer::copy_voxel_metadata(const VoxelBuffer &src_buffer) { void get_unscaled_sdf(const VoxelBuffer &voxels, Span sdf) { ZN_PROFILE_SCOPE(); ZN_DSTACK(); - const uint64_t volume = Vector3iUtil::get_volume(voxels.get_size()); + const uint64_t volume = Vector3iUtil::get_volume_u64(voxels.get_size()); ZN_ASSERT_RETURN(volume == sdf.size()); const VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; diff --git a/storage/voxel_buffer.h b/storage/voxel_buffer.h index aba76c8ec..03be47fc0 100644 --- a/storage/voxel_buffer.h +++ b/storage/voxel_buffer.h @@ -432,7 +432,7 @@ class VoxelBuffer { } inline uint64_t get_volume() const { - return Vector3iUtil::get_volume(_size); + return Vector3iUtil::get_volume_u64(_size); } bool get_channel_as_bytes(unsigned int channel_index, Span &slice); diff --git a/storage/voxel_data.cpp b/storage/voxel_data.cpp index e7ca68b5f..823eed3cd 100644 --- a/storage/voxel_data.cpp +++ b/storage/voxel_data.cpp @@ -1026,7 +1026,7 @@ void VoxelData::get_blocks_with_voxel_data( Span> out_blocks ) const { ZN_PROFILE_SCOPE(); - ZN_ASSERT(int64_t(out_blocks.size()) >= Vector3iUtil::get_volume(p_blocks_box.size)); + ZN_ASSERT(out_blocks.size() >= Vector3iUtil::get_volume_u64(p_blocks_box.size)); const Lod &data_lod = _lods[lod_index]; diff --git a/storage/voxel_data_grid.h b/storage/voxel_data_grid.h index 5c35d832c..c3b1397a0 100644 --- a/storage/voxel_data_grid.h +++ b/storage/voxel_data_grid.h @@ -250,7 +250,7 @@ class VoxelDataGrid { inline void create(Vector3i size, unsigned int block_size) { ZN_PROFILE_SCOPE(); _blocks.clear(); - _blocks.resize(Vector3iUtil::get_volume(size)); + _blocks.resize(Vector3iUtil::get_volume_u64(size)); _size_in_blocks = size; _block_size = block_size; } diff --git a/storage/voxel_data_map.cpp b/storage/voxel_data_map.cpp index 507dd8878..5e4886b22 100644 --- a/storage/voxel_data_map.cpp +++ b/storage/voxel_data_map.cpp @@ -196,7 +196,7 @@ void VoxelDataMap::copy( ) const { // TODO Reimplement using `copy_from_chunked_storage`? - ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume(dst_buffer.get_size()) > 0, "The area to copy is empty"); + ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume_u64(dst_buffer.get_size()) > 0, "The area to copy is empty"); const Vector3i max_pos = min_pos + dst_buffer.get_size(); const Vector3i min_block_pos = voxel_to_block(min_pos); diff --git a/streams/region/region_file.cpp b/streams/region/region_file.cpp index 3d67c8cde..e79ebaabd 100644 --- a/streams/region/region_file.cpp +++ b/streams/region/region_file.cpp @@ -38,10 +38,10 @@ bool RegionFormat::validate() const { for (unsigned int i = 0; i < channel_depths.size(); ++i) { bytes_per_block += VoxelBuffer::get_depth_bit_count(channel_depths[i]) / 8; } - bytes_per_block *= Vector3iUtil::get_volume(Vector3iUtil::create(1 << block_size_po2)); + bytes_per_block *= Vector3iUtil::get_volume_u64(Vector3iUtil::create(1 << block_size_po2)); const size_t sectors_per_block = (bytes_per_block - 1) / sector_size + 1; ERR_FAIL_COND_V(sectors_per_block > RegionBlockInfo::MAX_SECTOR_COUNT, false); - const size_t max_potential_sectors = Vector3iUtil::get_volume(region_size) * sectors_per_block; + const size_t max_potential_sectors = Vector3iUtil::get_volume_u64(region_size) * sectors_per_block; ERR_FAIL_COND_V(max_potential_sectors > RegionBlockInfo::MAX_SECTOR_INDEX, false); return true; @@ -61,11 +61,15 @@ uint32_t get_header_size_v3(const RegionFormat &format) { // Which file offset blocks data is starting // magic + version + blockinfos return MAGIC_AND_VERSION_SIZE + FIXED_HEADER_DATA_SIZE + (format.has_palette ? PALETTE_SIZE_IN_BYTES : 0) + - Vector3iUtil::get_volume(format.region_size) * sizeof(RegionBlockInfo); + Vector3iUtil::get_volume_u64(format.region_size) * sizeof(RegionBlockInfo); } bool save_header( - FileAccess &f, uint8_t version, const RegionFormat &format, const StdVector &block_infos) { + FileAccess &f, + uint8_t version, + const RegionFormat &format, + const StdVector &block_infos +) { // `f` could be anywhere in the file, we seek to ensure we start at the beginning f.seek(0); @@ -98,9 +102,12 @@ bool save_header( } // TODO Deal with endianness, this should be little-endian - zylann::godot::store_buffer(f, - Span(reinterpret_cast(block_infos.data()), - block_infos.size() * sizeof(RegionBlockInfo))); + zylann::godot::store_buffer( + f, + Span( + reinterpret_cast(block_infos.data()), block_infos.size() * sizeof(RegionBlockInfo) + ) + ); #ifdef DEBUG_ENABLED const size_t blocks_begin_offset = f.get_position(); @@ -111,14 +118,19 @@ bool save_header( } bool load_header( - FileAccess &f, uint8_t &out_version, RegionFormat &out_format, StdVector &out_block_infos) { + FileAccess &f, + uint8_t &out_version, + RegionFormat &out_format, + StdVector &out_block_infos +) { ERR_FAIL_COND_V(f.get_position() != 0, false); ERR_FAIL_COND_V(f.get_length() < MAGIC_AND_VERSION_SIZE, false); FixedArray magic; fill(magic, '\0'); ERR_FAIL_COND_V( - zylann::godot::get_buffer(f, Span(reinterpret_cast(magic.data()), 4)) != 4, false); + zylann::godot::get_buffer(f, Span(reinterpret_cast(magic.data()), 4)) != 4, false + ); ERR_FAIL_COND_V(strcmp(magic.data(), FORMAT_REGION_MAGIC) != 0, false); const uint8_t version = f.get_8(); @@ -160,7 +172,7 @@ bool load_header( } out_version = version; - out_block_infos.resize(Vector3iUtil::get_volume(out_format.region_size)); + out_block_infos.resize(Vector3iUtil::get_volume_u64(out_format.region_size)); // TODO Deal with endianness const size_t blocks_len = out_block_infos.size() * sizeof(RegionBlockInfo); @@ -250,10 +262,13 @@ Error RegionFile::open(const String &fpath, bool create_if_not_found) { } } - std::sort(blocks_sorted_by_offset.begin(), blocks_sorted_by_offset.end(), + std::sort( + blocks_sorted_by_offset.begin(), + blocks_sorted_by_offset.end(), [](const BlockInfoAndIndex &a, const BlockInfoAndIndex &b) { return a.b.get_sector_index() < b.b.get_sector_index(); - }); + } + ); CRASH_COND(_sectors.size() != 0); for (unsigned int i = 0; i < blocks_sorted_by_offset.size(); ++i) { @@ -308,7 +323,7 @@ bool RegionFile::set_format(const RegionFormat &format) { // This will be the format used to create the next file if not found on open() _header.format = format; - _header.blocks.resize(Vector3iUtil::get_volume(format.region_size)); + _header.blocks.resize(Vector3iUtil::get_volume_u64(format.region_size)); return true; } @@ -353,8 +368,11 @@ Error RegionFile::load_block(Vector3i position, VoxelBuffer &out_block) { unsigned int block_data_size = f.get_32(); CRASH_COND(f.eof_reached()); - ERR_FAIL_COND_V_MSG(!BlockSerializer::decompress_and_deserialize(f, block_data_size, out_block), ERR_PARSE_ERROR, - String("Failed to read block {0}").format(varray(position))); + ERR_FAIL_COND_V_MSG( + !BlockSerializer::decompress_and_deserialize(f, block_data_size, out_block), + ERR_PARSE_ERROR, + String("Failed to read block {0}").format(varray(position)) + ); return OK; } @@ -391,9 +409,11 @@ Error RegionFile::save_block(Vector3i position, VoxelBuffer &block) { zylann::godot::store_buffer(f, to_span(res.data)); const unsigned int end_pos = f.get_position(); - CRASH_COND_MSG(written_size != (end_pos - block_offset), + CRASH_COND_MSG( + written_size != (end_pos - block_offset), String("written_size: {0}, block_offset: {1}, end_pos: {2}") - .format(varray(written_size, block_offset, end_pos))); + .format(varray(written_size, block_offset, end_pos)) + ); pad_to_sector_size(f); block_info.set_sector_index((block_offset - _blocks_begin_offset) / _header.format.sector_size); @@ -538,8 +558,10 @@ void RegionFile::remove_sectors_from_block(Vector3i block_pos, unsigned int p_se // but FileAccess doesn't have any function to do that... so can't rely on EOF either // Erase sectors from cache - _sectors.erase(_sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count() - p_sector_count), - _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count())); + _sectors.erase( + _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count() - p_sector_count), + _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count()) + ); const unsigned int old_sector_index = block_info.get_sector_index(); @@ -581,7 +603,7 @@ bool RegionFile::migrate_from_v2_to_v3(FileAccess &f, RegionFormat &format) { // Which file offset blocks data is starting // magic + version + blockinfos - const unsigned int old_header_size = Vector3iUtil::get_volume(format.region_size) * sizeof(uint32_t); + const unsigned int old_header_size = Vector3iUtil::get_volume_u64(format.region_size) * sizeof(uint32_t); const unsigned int new_header_size = get_header_size_v3(format) - MAGIC_AND_VERSION_SIZE; ERR_FAIL_COND_V_MSG(new_header_size < old_header_size, false, "New version is supposed to have larger header"); @@ -676,7 +698,8 @@ void RegionFile::debug_check() { const unsigned int block_begin = _blocks_begin_offset + sector_index * _header.format.sector_size; if (block_begin >= file_len) { ZN_PRINT_ERROR(format( - "LUT {} {}: offset {} is larger than file size {}", lut_index, position, block_begin, file_len)); + "LUT {} {}: offset {} is larger than file size {}", lut_index, position, block_begin, file_len + )); continue; } f.seek(block_begin); @@ -684,8 +707,14 @@ void RegionFile::debug_check() { const size_t pos = f.get_position(); const size_t remaining_size = file_len - pos; if (block_data_size > remaining_size) { - ZN_PRINT_ERROR(format("LUT {} {}: block size {} at offset {} is larger than remaining size {}", lut_index, - position, block_data_size, block_begin, remaining_size)); + ZN_PRINT_ERROR( + format("LUT {} {}: block size {} at offset {} is larger than remaining size {}", + lut_index, + position, + block_data_size, + block_begin, + remaining_size) + ); } } } diff --git a/streams/voxel_block_serializer.cpp b/streams/voxel_block_serializer.cpp index 581512f91..8876b7a3d 100644 --- a/streams/voxel_block_serializer.cpp +++ b/streams/voxel_block_serializer.cpp @@ -295,7 +295,7 @@ SerializeResult serialize(const VoxelBuffer &voxel_buffer) { metadata_tmp.clear(); // Cannot serialize an empty block - ERR_FAIL_COND_V(Vector3iUtil::get_volume(voxel_buffer.get_size()) == 0, SerializeResult(dst_data, false)); + ERR_FAIL_COND_V(Vector3iUtil::get_volume_u64(voxel_buffer.get_size()) == 0, SerializeResult(dst_data, false)); size_t expected_metadata_size = 0; const size_t expected_data_size = get_size_in_bytes(voxel_buffer, expected_metadata_size); diff --git a/streams/voxel_stream.cpp b/streams/voxel_stream.cpp index 6e5a66bce..7d1a25f0d 100644 --- a/streams/voxel_stream.cpp +++ b/streams/voxel_stream.cpp @@ -88,22 +88,22 @@ void VoxelStream::flush() { VoxelStream::ResultCode VoxelStream::_b_load_voxel_block( Ref out_buffer, - Vector3i origin_in_voxels, + Vector3i block_position, int lod_index ) { ERR_FAIL_COND_V(lod_index < 0, RESULT_ERROR); ERR_FAIL_COND_V(lod_index >= static_cast(constants::MAX_LOD), RESULT_ERROR); ERR_FAIL_COND_V(out_buffer.is_null(), RESULT_ERROR); - VoxelQueryData q{ out_buffer->get_buffer(), origin_in_voxels, static_cast(lod_index), RESULT_ERROR }; + VoxelQueryData q{ out_buffer->get_buffer(), block_position, static_cast(lod_index), RESULT_ERROR }; load_voxel_block(q); return q.result; } -void VoxelStream::_b_save_voxel_block(Ref buffer, Vector3i origin_in_voxels, int lod_index) { +void VoxelStream::_b_save_voxel_block(Ref buffer, Vector3i block_position, int lod_index) { ERR_FAIL_COND(lod_index < 0); ERR_FAIL_COND(lod_index >= static_cast(constants::MAX_LOD)); ERR_FAIL_COND(buffer.is_null()); - VoxelQueryData q{ buffer->get_buffer(), origin_in_voxels, static_cast(lod_index), RESULT_ERROR }; + VoxelQueryData q{ buffer->get_buffer(), block_position, static_cast(lod_index), RESULT_ERROR }; save_voxel_block(q); } @@ -117,11 +117,10 @@ Vector3 VoxelStream::_b_get_block_size() const { void VoxelStream::_bind_methods() { ClassDB::bind_method( - D_METHOD("load_voxel_block", "out_buffer", "origin_in_voxels", "lod_index"), - &VoxelStream::_b_load_voxel_block + D_METHOD("load_voxel_block", "out_buffer", "block_position", "lod_index"), &VoxelStream::_b_load_voxel_block ); ClassDB::bind_method( - D_METHOD("save_voxel_block", "buffer", "origin_in_voxels", "lod_index"), &VoxelStream::_b_save_voxel_block + D_METHOD("save_voxel_block", "buffer", "block_position", "lod_index"), &VoxelStream::_b_save_voxel_block ); ClassDB::bind_method(D_METHOD("get_used_channels_mask"), &VoxelStream::_b_get_used_channels_mask); diff --git a/streams/voxel_stream.h b/streams/voxel_stream.h index cb30fddb1..933041da3 100644 --- a/streams/voxel_stream.h +++ b/streams/voxel_stream.h @@ -137,8 +137,8 @@ class VoxelStream : public Resource { private: static void _bind_methods(); - ResultCode _b_load_voxel_block(Ref out_buffer, Vector3i origin_in_voxels, int lod_index); - void _b_save_voxel_block(Ref buffer, Vector3i origin_in_voxels, int lod_index); + ResultCode _b_load_voxel_block(Ref out_buffer, Vector3i block_position, int lod_index); + void _b_save_voxel_block(Ref buffer, Vector3i block_position, int lod_index); int _b_get_used_channels_mask() const; Vector3 _b_get_block_size() const; diff --git a/terrain/fixed_lod/voxel_terrain.cpp b/terrain/fixed_lod/voxel_terrain.cpp index 62ec55593..0adf1d748 100644 --- a/terrain/fixed_lod/voxel_terrain.cpp +++ b/terrain/fixed_lod/voxel_terrain.cpp @@ -484,6 +484,7 @@ void VoxelTerrain::unview_mesh_block(Vector3i bpos, bool mesh_flag, bool collisi if (block->mesh_viewers.get() == 0) { // Mesh no longer required block->drop_mesh(); + block->set_visible(false); } } @@ -492,6 +493,7 @@ void VoxelTerrain::unview_mesh_block(Vector3i bpos, bool mesh_flag, bool collisi if (block->collision_viewers.get() == 0) { // Collision no longer required block->drop_collision(); + block->set_collision_enabled(false); } } @@ -1475,36 +1477,42 @@ void VoxelTerrain::process_viewer_data_box_change( _unloaded_saving_blocks[bts.position] = bts.voxels; } - // Remove loading blocks (those were loaded and had their refcount reach zero) - for (const Vector3i bpos : tls_found_blocks_positions) { - emit_data_block_unloaded(bpos); - // TODO If they were loaded, why would they be in loading blocks? - // Probably in case we move so fast that blocks haven't even finished loading - _loading_blocks.erase(bpos); + { + ZN_PROFILE_SCOPE_NAMED("Unload signals"); + // Remove loading blocks (those were loaded and had their refcount reach zero) + for (const Vector3i bpos : tls_found_blocks_positions) { + emit_data_block_unloaded(bpos); + // TODO If they were loaded, why would they be in loading blocks? + // Probably in case we move so fast that blocks haven't even finished loading + _loading_blocks.erase(bpos); + } } // Remove refcount from loading blocks, and cancel loading if it reaches zero - for (const Vector3i bpos : tls_missing_blocks) { - auto loading_block_it = _loading_blocks.find(bpos); - if (loading_block_it == _loading_blocks.end()) { - ZN_PRINT_VERBOSE("Request to unview a loading block that was never requested"); - // Not expected, but fine I guess - return; - } - - LoadingBlock &loading_block = loading_block_it->second; - loading_block.viewers.remove(); - - if (loading_block.viewers.get() == 0) { - // No longer want to load it - _loading_blocks.erase(loading_block_it); + { + ZN_PROFILE_SCOPE_NAMED("Cancel missing blocks"); + for (const Vector3i bpos : tls_missing_blocks) { + auto loading_block_it = _loading_blocks.find(bpos); + if (loading_block_it == _loading_blocks.end()) { + ZN_PRINT_VERBOSE("Request to unview a loading block that was never requested"); + // Not expected, but fine I guess + return; + } - // TODO Do we really need that vector after all? - for (size_t i = 0; i < _blocks_pending_load.size(); ++i) { - if (_blocks_pending_load[i] == bpos) { - _blocks_pending_load[i] = _blocks_pending_load.back(); - _blocks_pending_load.pop_back(); - break; + LoadingBlock &loading_block = loading_block_it->second; + loading_block.viewers.remove(); + + if (loading_block.viewers.get() == 0) { + // No longer want to load it + _loading_blocks.erase(loading_block_it); + + // TODO Do we really need that vector after all? + for (size_t i = 0; i < _blocks_pending_load.size(); ++i) { + if (_blocks_pending_load[i] == bpos) { + _blocks_pending_load[i] = _blocks_pending_load.back(); + _blocks_pending_load.pop_back(); + break; + } } } } @@ -1531,33 +1539,37 @@ void VoxelTerrain::process_viewer_data_box_change( }); // Schedule loading of missing blocks - for (const Vector3i missing_bpos : tls_missing_blocks) { - auto loading_block_it = _loading_blocks.find(missing_bpos); + { + ZN_PROFILE_SCOPE_NAMED("Gather missing blocks"); + for (const Vector3i missing_bpos : tls_missing_blocks) { + auto loading_block_it = _loading_blocks.find(missing_bpos); - if (loading_block_it == _loading_blocks.end()) { - // First viewer to request it - LoadingBlock new_loading_block; - new_loading_block.viewers.add(); + if (loading_block_it == _loading_blocks.end()) { + // First viewer to request it + LoadingBlock new_loading_block; + new_loading_block.viewers.add(); - if (require_notifications) { - new_loading_block.viewers_to_notify.push_back(viewer_id); - } + if (require_notifications) { + new_loading_block.viewers_to_notify.push_back(viewer_id); + } - _loading_blocks.insert({ missing_bpos, new_loading_block }); - _blocks_pending_load.push_back(missing_bpos); + _loading_blocks.insert({ missing_bpos, new_loading_block }); + _blocks_pending_load.push_back(missing_bpos); - } else { - // More viewers - LoadingBlock &loading_block = loading_block_it->second; - loading_block.viewers.add(); + } else { + // More viewers + LoadingBlock &loading_block = loading_block_it->second; + loading_block.viewers.add(); - if (require_notifications) { - loading_block.viewers_to_notify.push_back(viewer_id); + if (require_notifications) { + loading_block.viewers_to_notify.push_back(viewer_id); + } } } } if (require_notifications) { + ZN_PROFILE_SCOPE_NAMED("Enter notifications"); // Notifications for blocks that were already loaded for (unsigned int i = 0; i < tls_found_blocks.size(); ++i) { const Vector3i bpos = tls_found_blocks_positions[i]; @@ -1806,12 +1818,13 @@ void VoxelTerrain::process_meshing() { task->mesh_block_position = mesh_block_pos; task->lod_index = 0; task->meshing_dependency = _meshing_dependency; - task->collision_hint = _generate_collisions; + task->require_visual = mesh_block->mesh_viewers.get() > 0; + task->collision_hint = _generate_collisions && mesh_block->collision_viewers.get() > 0; task->data = _data; // This iteration order is specifically chosen to match VoxelEngine and threaded access _data->get_blocks_with_voxel_data(data_box, 0, to_span(task->blocks)); - task->blocks_count = Vector3iUtil::get_volume(data_box.size); + task->blocks_count = Vector3iUtil::get_volume_u64(data_box.size); #ifdef DEBUG_ENABLED { @@ -1882,22 +1895,24 @@ void VoxelTerrain::apply_mesh_update(const VoxelEngine::BlockMeshOutput &ob) { Ref mesh; Ref shadow_occluder_mesh; StdVector material_indices; - if (ob.has_mesh_resource) { - // The mesh was already built as part of the threaded task - mesh = ob.mesh; - shadow_occluder_mesh = ob.shadow_occluder_mesh; - // It can be empty - material_indices = std::move(ob.mesh_material_indices); - } else { - // Can't build meshes in threads, do it here - material_indices.clear(); - mesh = build_mesh( - to_span_const(ob.surfaces.surfaces), - ob.surfaces.primitive_type, - ob.surfaces.mesh_flags, - material_indices - ); - shadow_occluder_mesh = build_mesh(ob.surfaces.shadow_occluder); + if (ob.visual_was_required) { + if (ob.has_mesh_resource) { + // The mesh was already built as part of the threaded task + mesh = ob.mesh; + shadow_occluder_mesh = ob.shadow_occluder_mesh; + // It can be empty + material_indices = std::move(ob.mesh_material_indices); + } else { + // Can't build meshes in threads, do it here + material_indices.clear(); + mesh = build_mesh( + to_span_const(ob.surfaces.surfaces), + ob.surfaces.primitive_type, + ob.surfaces.mesh_flags, + material_indices + ); + shadow_occluder_mesh = build_mesh(ob.surfaces.shadow_occluder); + } } if (mesh.is_valid()) { const unsigned int surface_count = mesh->get_surface_count(); @@ -1948,15 +1963,27 @@ void VoxelTerrain::apply_mesh_update(const VoxelEngine::BlockMeshOutput &ob) { const bool gen_collisions = _generate_collisions && block->collision_viewers.get() > 0; if (gen_collisions) { Ref collision_shape = make_collision_shape_from_mesher_output(ob.surfaces, **_mesher); - const bool debug_collisions = is_inside_tree() ? get_tree()->is_debugging_collisions_hint() : false; + + bool debug_collisions = false; + if (is_inside_tree()) { + const SceneTree *scene_tree = get_tree(); +#if DEBUG_ENABLED + if (collision_shape.is_valid()) { + const Color debug_color = scene_tree->get_debug_collisions_color(); + collision_shape->set_debug_color(debug_color); + } +#endif + debug_collisions = scene_tree->is_debugging_collisions_hint(); + } + block->set_collision_shape(collision_shape, debug_collisions, this, _collision_margin); block->set_collision_layer(_collision_layer); block->set_collision_mask(_collision_mask); } - block->set_visible(true); - block->set_collision_enabled(true); + block->set_visible(block->mesh_viewers.get() > 0); + block->set_collision_enabled(gen_collisions); block->set_parent_visible(is_visible()); block->set_parent_transform(get_global_transform()); // TODO We don't set MESH_UP_TO_DATE anywhere, but it seems to work? @@ -2165,6 +2192,30 @@ void VoxelTerrain::process_debug_draw() { } } + if (debug_get_draw_flag(DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS)) { + const int mesh_block_size = get_mesh_block_size(); + _mesh_map.for_each_block([&parent_transform, &dr, mesh_block_size](const VoxelMeshBlockVT &block) { + Color8 color; + const bool visual = block.is_visible(); + const bool collision = block.is_collision_enabled(); + if (visual && collision) { + color = Color8(255, 255, 0, 255); + } else if (visual) { + color = Color8(0, 255, 0, 255); + } else if (collision) { + color = Color8(255, 0, 0, 255); + } else { + return; + } + const Vector3i voxel_pos = block.position * mesh_block_size; + const Transform3D local_transform( + Basis().scaled(Vector3(mesh_block_size, mesh_block_size, mesh_block_size)), voxel_pos + ); + const Transform3D t = parent_transform * local_transform; + dr.draw_box(t, color); + }); + } + dr.end(); } @@ -2426,6 +2477,7 @@ void VoxelTerrain::_bind_methods() { ); ADD_DEBUG_DRAW_FLAG("debug_draw_volume_bounds", DEBUG_DRAW_VOLUME_BOUNDS); + ADD_DEBUG_DRAW_FLAG("debug_draw_visual_and_collision_blocks", DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS); ADD_PROPERTY( PropertyInfo(Variant::BOOL, "debug_draw_shadow_occluders", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), diff --git a/terrain/fixed_lod/voxel_terrain.h b/terrain/fixed_lod/voxel_terrain.h index 13c63ca69..8e504288a 100644 --- a/terrain/fixed_lod/voxel_terrain.h +++ b/terrain/fixed_lod/voxel_terrain.h @@ -151,8 +151,9 @@ class VoxelTerrain : public VoxelNode { enum DebugDrawFlag { DEBUG_DRAW_VOLUME_BOUNDS = 0, + DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS = 1, - DEBUG_DRAW_FLAGS_COUNT = 1 + DEBUG_DRAW_FLAGS_COUNT = 2 }; void debug_set_draw_enabled(bool enabled); diff --git a/terrain/instancing/voxel_instance_generator.cpp b/terrain/instancing/voxel_instance_generator.cpp index 8aed502ef..19b38b69b 100644 --- a/terrain/instancing/voxel_instance_generator.cpp +++ b/terrain/instancing/voxel_instance_generator.cpp @@ -4,6 +4,7 @@ #include "../../util/godot/classes/array_mesh.h" #include "../../util/godot/classes/engine.h" #include "../../util/godot/core/array.h" +#include "../../util/godot/core/packed_arrays.h" #include "../../util/godot/core/random_pcg.h" #include "../../util/math/conv.h" #include "../../util/math/triangle.h" @@ -55,9 +56,10 @@ void VoxelInstanceGenerator::generate_transforms( PackedVector3Array normals = surface_arrays[ArrayMesh::ARRAY_NORMAL]; ERR_FAIL_COND(normals.size() == 0); - PackedInt32Array indices = surface_arrays[ArrayMesh::ARRAY_INDEX]; - ERR_FAIL_COND(indices.size() == 0); - ERR_FAIL_COND(indices.size() % 3 != 0); + PackedInt32Array mesh_indices_pba = surface_arrays[ArrayMesh::ARRAY_INDEX]; + ERR_FAIL_COND(mesh_indices_pba.size() == 0); + ERR_FAIL_COND(mesh_indices_pba.size() % 3 != 0); + Span mesh_indices = to_span(mesh_indices_pba); const uint32_t block_pos_hash = Vector3iHasher::hash(grid_position); @@ -75,6 +77,7 @@ void VoxelInstanceGenerator::generate_transforms( // TODO Candidates for temp allocator static thread_local StdVector g_vertex_cache; static thread_local StdVector g_normal_cache; + static thread_local StdVector g_index_cache; static thread_local StdVector g_noise_cache; // static thread_local StdVector g_noise_graph_output_cache; static thread_local StdVector g_noise_graph_x_cache; @@ -83,9 +86,16 @@ void VoxelInstanceGenerator::generate_transforms( StdVector &vertex_cache = g_vertex_cache; StdVector &normal_cache = g_normal_cache; + StdVector &index_cache = g_index_cache; vertex_cache.clear(); normal_cache.clear(); + index_cache.clear(); + + const bool voxel_material_filter_enabled = _voxel_material_filter_enabled; + const uint32_t voxel_material_filter_mask = _voxel_material_filter_mask; + + const bool index_cache_used = voxel_material_filter_enabled; // Pick random points { @@ -121,13 +131,16 @@ void VoxelInstanceGenerator::generate_transforms( } vertex_cache.push_back(pos); normal_cache.push_back(to_vec3f(normals[i])); + if (index_cache_used) { + index_cache.push_back(i); + } } } break; case EMIT_FROM_FACES_FAST: { // PoolIntArray::Read indices_r = indices.read(); - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; // Assumes triangles are all roughly under the same size, and Transvoxel ones do (when not simplified), // so we can use number of triangles as a metric proportional to the number of instances @@ -140,9 +153,9 @@ void VoxelInstanceGenerator::generate_transforms( // Pick a random triangle const uint32_t ii = (pcg0.rand() % triangle_count) * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3 &pa = vertices[ia]; const Vector3 &pb = vertices[ib]; @@ -164,6 +177,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache[instance_index] = to_vec3f(p); normal_cache[instance_index] = to_vec3f(n); + + if (index_cache_used) { + index_cache.push_back(ii); + } } } break; @@ -171,7 +188,7 @@ void VoxelInstanceGenerator::generate_transforms( case EMIT_FROM_FACES: { // PackedInt32Array::Read indices_r = indices.read(); - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; // static thread_local StdVector g_area_cache; // StdVector &area_cache = g_area_cache; @@ -192,9 +209,9 @@ void VoxelInstanceGenerator::generate_transforms( for (int triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { const uint32_t ii = triangle_index * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3f &pa = to_vec3f(vertices[ia]); const Vector3f &pb = to_vec3f(vertices[ib]); @@ -226,6 +243,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache.push_back(rp); normal_cache.push_back(rn); + + if (index_cache_used) { + index_cache.push_back(ii); + } } area_accumulator -= count_in_triangle * inv_density; @@ -236,7 +257,7 @@ void VoxelInstanceGenerator::generate_transforms( case EMIT_ONE_PER_TRIANGLE: { // Density has no effect here. - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; const float one_third = 1.f / 3.f; const float triangle_area_threshold = math::squared(1 << lod_index) * _triangle_area_threshold_lod0; @@ -246,9 +267,9 @@ void VoxelInstanceGenerator::generate_transforms( for (int triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { const uint32_t ii = triangle_index * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3f &pa = to_vec3f(vertices[ia]); const Vector3f &pb = to_vec3f(vertices[ib]); @@ -288,6 +309,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache.push_back(p); normal_cache.push_back(n); } + + if (index_cache_used) { + index_cache.push_back(ii); + } } } break; @@ -308,11 +333,108 @@ void VoxelInstanceGenerator::generate_transforms( if ((octant_mask & (1 << octant_index)) == 0) { unordered_remove(vertex_cache, i); unordered_remove(normal_cache, i); + if (index_cache_used) { + unordered_remove(index_cache, i); + } --i; } } } + // Filter out by voxel materials + // Assuming 4x8-bit weights and 4x8-bit indices as used in VoxelMesherTransvoxel for now, but might have other + // formats in the future + if (voxel_material_filter_enabled && surface_arrays.size() >= Mesh::ARRAY_CUSTOM1) { + ZN_PROFILE_SCOPE(); + + struct Attrib { + uint32_t packed_indices; + uint32_t packed_weights; + }; + const PackedFloat32Array src_vertex_data = surface_arrays[Mesh::ARRAY_CUSTOM1]; + const Span attrib_array = to_span(src_vertex_data).reinterpret_cast_to(); + + const unsigned int weight_threshold = 128; + + struct L { + static inline bool vertex_contains_enough_material( + const Attrib attrib, + const unsigned int threshold, + const uint32_t material_mask + ) { + for (unsigned int i = 0; i < 4; ++i) { + const unsigned int vmat_weight = (attrib.packed_weights >> (i * 8)) & 0xff; + if (vmat_weight > threshold) { + const unsigned int vmat_index = (attrib.packed_indices >> (i * 8)) & 0xff; + if (((1 << vmat_index) & material_mask) != 0) { + return true; + } + } + } + return false; + } + + static inline bool triangle_contains_enough_material( + const Span attrib_array, + const Span mesh_indices, + const unsigned int ii0, + const unsigned int threshold, + const uint32_t material_mask + ) { + const uint32_t vi0 = mesh_indices[ii0 + 0]; + const uint32_t vi1 = mesh_indices[ii0 + 1]; + const uint32_t vi2 = mesh_indices[ii0 + 2]; + + return vertex_contains_enough_material(attrib_array[vi0], threshold, material_mask) || + vertex_contains_enough_material(attrib_array[vi1], threshold, material_mask) || + vertex_contains_enough_material(attrib_array[vi2], threshold, material_mask); + } + }; + + switch (_emit_mode) { + case EMIT_FROM_VERTICES: { + // Indices are vertices + for (unsigned int instance_index = 0; instance_index < vertex_cache.size();) { + const unsigned int vi = index_cache[instance_index]; + const Attrib attrib = attrib_array[vi]; + if (L::vertex_contains_enough_material(attrib, weight_threshold, voxel_material_filter_mask)) { + instance_index += 1; + } else { + // Remove instance + unordered_remove(vertex_cache, instance_index); + unordered_remove(normal_cache, instance_index); + unordered_remove(index_cache, instance_index); + } + } + } break; + case EMIT_FROM_FACES: + case EMIT_FROM_FACES_FAST: + case EMIT_ONE_PER_TRIANGLE: { + // Indices are the index in the index buffer of the first vertex of the triangle in which the instance + // was spawned in + for (unsigned int instance_index = 0; instance_index < vertex_cache.size();) { + const uint32_t ii0 = index_cache[instance_index]; + if (L::triangle_contains_enough_material( + attrib_array, mesh_indices, ii0, weight_threshold, voxel_material_filter_mask + )) { + instance_index += 1; + } else { + // Remove instance + unordered_remove(vertex_cache, instance_index); + unordered_remove(normal_cache, instance_index); + unordered_remove(index_cache, instance_index); + } + } + } break; + default: + ZN_PRINT_ERROR_ONCE("Unhandled emit mode"); + break; + } + + // Index cache has no use yet after this. To detect future mistakes if any, make it obvious by clearing it + index_cache.clear(); + } + // Position of the block relative to the instancer node. // Use full-precision here because we deal with potentially large coordinates const Vector3 mesh_block_origin_d = grid_position * block_size; @@ -475,6 +597,10 @@ void VoxelInstanceGenerator::generate_transforms( unordered_remove(vertex_cache, i); unordered_remove(normal_cache, i); unordered_remove(noise_cache, i); + // We don't use the index cache after this... for now + // if (index_cache_used) { + // unordered_remove(index_cache, i); + // } --i; } } @@ -944,6 +1070,63 @@ float VoxelInstanceGenerator::get_noise_on_scale() const { return _noise_on_scale; } +void VoxelInstanceGenerator::set_voxel_material_filter_enabled(bool enabled) { + if (enabled == _voxel_material_filter_enabled) { + return; + } + _voxel_material_filter_enabled = enabled; + emit_changed(); +} + +bool VoxelInstanceGenerator::is_voxel_material_filter_enabled() const { + return _voxel_material_filter_enabled; +} + +void VoxelInstanceGenerator::set_voxel_material_filter_mask(const uint32_t mask) { + if (mask == _voxel_material_filter_mask) { + return; + } + _voxel_material_filter_mask = mask; + emit_changed(); +} + +uint32_t VoxelInstanceGenerator::get_voxel_material_filter_mask() const { + return _voxel_material_filter_mask; +} + +PackedInt32Array VoxelInstanceGenerator::_b_get_voxel_material_filter_array() const { + const unsigned int bit_count = sizeof(_voxel_material_filter_mask) * 8; + PackedInt32Array array; + for (unsigned int i = 0; i < bit_count; ++i) { + if ((_voxel_material_filter_mask & (1 << i)) != 0) { + array.append(i); + } + } + return array; +} + +void VoxelInstanceGenerator::_b_set_voxel_material_filter_array(PackedInt32Array material_indices) { + const unsigned int bit_count = sizeof(_voxel_material_filter_mask) * 8; + uint32_t mask = 0; + Span indices = to_span(material_indices); + for (const int32_t si : indices) { + ZN_ASSERT_CONTINUE(si >= 0); + const unsigned int i = static_cast(si); + ZN_ASSERT_CONTINUE(i < bit_count); + mask |= (1 << i); + } +#if TOOLS_ENABLED + // Only warn when running the game, because when users add new items to the array in the editor, + // it is likely to have duplicates temporarily, until they set the desired values. + if (!Engine::get_singleton()->is_editor_hint()) { + if (has_duplicate(indices)) { + ZN_PRINT_WARNING("The array of indices contains a duplicate."); + } + } +#endif + set_voxel_material_filter_mask(mask); +} + void VoxelInstanceGenerator::_on_noise_changed() { emit_changed(); } @@ -1079,6 +1262,19 @@ void VoxelInstanceGenerator::_bind_methods() { ClassDB::bind_method(D_METHOD("set_noise_on_scale", "amount"), &Self::set_noise_on_scale); ClassDB::bind_method(D_METHOD("get_noise_on_scale"), &Self::get_noise_on_scale); + ClassDB::bind_method( + D_METHOD("set_voxel_texture_filter_enabled", "enabled"), &Self::set_voxel_material_filter_enabled + ); + ClassDB::bind_method(D_METHOD("is_voxel_texture_filter_enabled"), &Self::is_voxel_material_filter_enabled); + + ClassDB::bind_method(D_METHOD("set_voxel_texture_filter_mask", "mask"), &Self::set_voxel_material_filter_mask); + ClassDB::bind_method(D_METHOD("get_voxel_texture_filter_mask"), &Self::get_voxel_material_filter_mask); + + ClassDB::bind_method( + D_METHOD("set_voxel_texture_filter_array", "texture_indices"), &Self::_b_set_voxel_material_filter_array + ); + ClassDB::bind_method(D_METHOD("get_voxel_texture_filter_array"), &Self::_b_get_voxel_material_filter_array); + ADD_GROUP("Emission", ""); ADD_PROPERTY( @@ -1180,6 +1376,24 @@ void VoxelInstanceGenerator::_bind_methods() { "get_noise_on_scale" ); + ADD_GROUP("Filtering", ""); + + ADD_PROPERTY( + PropertyInfo(Variant::BOOL, "voxel_texture_filter_enabled"), + "set_voxel_texture_filter_enabled", + "is_voxel_texture_filter_enabled" + ); + // ADD_PROPERTY( + // PropertyInfo(Variant::INT, "voxel_texture_filter_mask"), + // "set_voxel_texture_filter_mask", + // "get_voxel_texture_filter_mask" + // ); + ADD_PROPERTY( + PropertyInfo(Variant::PACKED_INT32_ARRAY, "voxel_texture_filter_array"), + "set_voxel_texture_filter_array", + "get_voxel_texture_filter_array" + ); + BIND_ENUM_CONSTANT(EMIT_FROM_VERTICES); BIND_ENUM_CONSTANT(EMIT_FROM_FACES_FAST); BIND_ENUM_CONSTANT(EMIT_FROM_FACES); diff --git a/terrain/instancing/voxel_instance_generator.h b/terrain/instancing/voxel_instance_generator.h index 316b44f6f..8a385eb52 100644 --- a/terrain/instancing/voxel_instance_generator.h +++ b/terrain/instancing/voxel_instance_generator.h @@ -124,6 +124,12 @@ class VoxelInstanceGenerator : public Resource { void set_noise_on_scale(float amount); float get_noise_on_scale() const; + void set_voxel_material_filter_enabled(bool enabled); + bool is_voxel_material_filter_enabled() const; + + void set_voxel_material_filter_mask(const uint32_t mask); + uint32_t get_voxel_material_filter_mask() const; + static inline int get_octant_index(const Vector3f pos, float half_block_size) { return get_octant_index(pos.x > half_block_size, pos.y > half_block_size, pos.z > half_block_size); } @@ -141,6 +147,9 @@ class VoxelInstanceGenerator : public Resource { void _on_noise_changed(); void _on_noise_graph_changed(); + PackedInt32Array _b_get_voxel_material_filter_array() const; + void _b_set_voxel_material_filter_array(PackedInt32Array material_indices); + static void _bind_methods(); float _density = 0.1f; @@ -161,6 +170,8 @@ class VoxelInstanceGenerator : public Resource { Ref _noise; Dimension _noise_dimension = DIMENSION_3D; float _noise_on_scale = 0.f; + bool _voxel_material_filter_enabled = false; + uint32_t _voxel_material_filter_mask = 1; // TODO Protect noise and noise graph members from multithreaded access diff --git a/terrain/variable_lod/voxel_lod_terrain.cpp b/terrain/variable_lod/voxel_lod_terrain.cpp index 087ad860f..b06af6d9a 100644 --- a/terrain/variable_lod/voxel_lod_terrain.cpp +++ b/terrain/variable_lod/voxel_lod_terrain.cpp @@ -1737,6 +1737,32 @@ void VoxelLodTerrain::apply_data_block_response(VoxelEngine::BlockDataOutput &ob // } } +inline void set_block_collision_shape( + const VoxelLodTerrain &terrain, + VoxelMeshBlockVLT &block, + Ref shape, + const uint64_t now +) { + bool debug_collisions = false; + if (terrain.is_inside_tree()) { + const SceneTree *scene_tree = terrain.get_tree(); +#if DEBUG_ENABLED + if (shape.is_valid()) { + const Color debug_color = scene_tree->get_debug_collisions_color(); + shape->set_debug_color(debug_color); + } +#endif + debug_collisions = scene_tree->is_debugging_collisions_hint(); + } + + block.set_collision_shape(shape, debug_collisions, &terrain, terrain.get_collision_margin()); + + block.set_collision_layer(terrain.get_collision_layer()); + block.set_collision_mask(terrain.get_collision_mask()); + block.last_collider_update_time = now; + block.deferred_collider_data.reset(); +} + void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) { // The following is done on the main thread because Godot doesn't really support everything done here. // Building meshes can be done in the threaded task when using Vulkan, but not OpenGL. @@ -2017,14 +2043,8 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) { static_cast(now - block->last_collider_update_time) > _collision_update_delay) { ZN_ASSERT(_mesher.is_valid()); Ref collision_shape = make_collision_shape_from_mesher_output(ob.surfaces, **_mesher); - const bool debug_collisions = is_inside_tree() ? get_tree()->is_debugging_collisions_hint() : false; - block->set_collision_shape(collision_shape, debug_collisions, this, _collision_margin); - - block->set_collision_layer(_collision_layer); - block->set_collision_mask(_collision_mask); + set_block_collision_shape(*this, *block, collision_shape, now); block->set_collision_enabled(collision_active); - block->last_collider_update_time = now; - block->deferred_collider_data.reset(); } else { if (block->deferred_collider_data == nullptr) { @@ -2270,13 +2290,7 @@ void VoxelLodTerrain::process_deferred_collision_updates(uint32_t timeout_msec) make_collision_shape_from_mesher_output(*block->deferred_collider_data, **_mesher); } - block->set_collision_shape( - collision_shape, get_tree()->is_debugging_collisions_hint(), this, _collision_margin - ); - block->set_collision_layer(_collision_layer); - block->set_collision_mask(_collision_mask); - block->last_collider_update_time = now; - block->deferred_collider_data.reset(); + set_block_collision_shape(*this, *block, collision_shape, now); unordered_remove(deferred_collision_updates, i); --i; @@ -3470,7 +3484,7 @@ Array VoxelLodTerrain::_b_debug_print_sdf_top_down(Vector3i center, Vector3i ext for (unsigned int lod_index = 0; lod_index < lod_count; ++lod_index) { const Box3i world_box = Box3i::from_center_extents(center >> lod_index, extents >> lod_index); - if (Vector3iUtil::get_volume(world_box.size) == 0) { + if (Vector3iUtil::get_volume_u64(world_box.size) == 0) { continue; } diff --git a/terrain/variable_lod/voxel_lod_terrain_update_task.cpp b/terrain/variable_lod/voxel_lod_terrain_update_task.cpp index 401c8b763..378f52977 100644 --- a/terrain/variable_lod/voxel_lod_terrain_update_task.cpp +++ b/terrain/variable_lod/voxel_lod_terrain_update_task.cpp @@ -46,8 +46,10 @@ void init_sparse_octree_priority_dependency( // // Distance beyond which it is safe to drop a block without risking to block LOD subdivision. // This does not depend on viewer's view distance, but on LOD precision instead. // TODO Should `data_block_size` be used here? Should it be mesh_block_size instead? - dep.drop_distance_squared = math::squared(2.f * transformed_block_radius * - VoxelEngine::get_octree_lod_block_region_extent(octree_lod_distance, data_block_size)); + dep.drop_distance_squared = math::squared( + 2.f * transformed_block_radius * + VoxelEngine::get_octree_lod_block_region_extent(octree_lod_distance, data_block_size) + ); } // This is only if we want to cache voxel data @@ -85,8 +87,15 @@ void request_block_generate( // params.use_gpu = settings.generator_use_gpu; params.cancellation_token = cancellation_token; - init_sparse_octree_priority_dependency(params.priority_dependency, block_pos, lod_index, data_block_size, - shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + params.priority_dependency, + block_pos, + lod_index, + data_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); IThreadedTask *task = stream_dependency->generator->create_block_task(params); @@ -130,20 +139,50 @@ void request_block_load( // if (stream_dependency->stream.is_valid()) { PriorityDependency priority_dependency; - init_sparse_octree_priority_dependency(priority_dependency, block_pos, lod_index, data_block_size, - shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + priority_dependency, + block_pos, + lod_index, + data_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); const bool request_instances = false; - LoadBlockDataTask *task = ZN_NEW(LoadBlockDataTask(volume_id, block_pos, lod_index, data_block_size, - request_instances, stream_dependency, priority_dependency, settings.cache_generated_blocks, - settings.generator_use_gpu, data, cancellation_token)); + LoadBlockDataTask *task = ZN_NEW(LoadBlockDataTask( + volume_id, + block_pos, + lod_index, + data_block_size, + request_instances, + stream_dependency, + priority_dependency, + settings.cache_generated_blocks, + settings.generator_use_gpu, + data, + cancellation_token + )); task_scheduler.push_io_task(task); } else if (settings.cache_generated_blocks) { // Directly generate the block without checking the stream. - request_block_generate(volume_id, data_block_size, stream_dependency, data, block_pos, lod_index, - shared_viewers_data, volume_transform, settings, nullptr, true, task_scheduler, cancellation_token); + request_block_generate( + volume_id, + data_block_size, + stream_dependency, + data, + block_pos, + lod_index, + shared_viewers_data, + volume_transform, + settings, + nullptr, + true, + task_scheduler, + cancellation_token + ); } else { ZN_PRINT_WARNING("Requesting a block load when it should not have been necessary"); @@ -308,8 +347,8 @@ void send_mesh_requests( // // Don't update a detail texture if one update is already processing if (settings.detail_texture_settings.enabled && - lod_index >= settings.detail_texture_settings.begin_lod_index && - mesh_block.detail_texture_state != VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING) { + lod_index >= settings.detail_texture_settings.begin_lod_index && + mesh_block.detail_texture_state != VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING) { mesh_block.detail_texture_state = VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING; task->require_detail_texture = true; } @@ -322,13 +361,20 @@ void send_mesh_requests( // // The array also implicitly encodes block position due to the convention being used, // so there is no need to also include positions in the request data.get_blocks_with_voxel_data(data_box, lod_index, to_span(task->blocks)); - task->blocks_count = Vector3iUtil::get_volume(data_box.size); + task->blocks_count = Vector3iUtil::get_volume_u64(data_box.size); // TODO There is inconsistency with coordinates sent to this function. // Sometimes we send data block coordinates, sometimes we send mesh block coordinates. They aren't always // the same, it might cause issues in priority sorting? - init_sparse_octree_priority_dependency(task->priority_dependency, task->mesh_block_position, - task->lod_index, mesh_block_size, shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + task->priority_dependency, + task->mesh_block_position, + task->lod_index, + mesh_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); task_scheduler.push_main_task(task); @@ -362,7 +408,8 @@ std::shared_ptr preload_boxes_async( // VoxelData &data = *data_ptr; ZN_ASSERT_RETURN_V_MSG( - data.is_streaming_enabled() == false, nullptr, "This function can only be used in full load mode"); + data.is_streaming_enabled() == false, nullptr, "This function can only be used in full load mode" + ); struct TaskArguments { Vector3i block_pos; @@ -417,9 +464,12 @@ std::shared_ptr preload_boxes_async( // // This may first run the generation tasks, and then the edits tracker = make_shared_instance( - todo.size(), next_tasks, [](Span p_next_tasks) { + todo.size(), + next_tasks, + [](Span p_next_tasks) { // VoxelEngine::get_singleton().push_async_tasks(p_next_tasks); - }); + } + ); for (unsigned int i = 0; i < todo.size(); ++i) { const TaskArguments args = todo[i]; @@ -480,8 +530,9 @@ void process_async_edits( // boxes_to_preload.push_back(edit.box); tasks_to_schedule.push_back(edit.task); - state.running_async_edits.push_back( - VoxelLodTerrainUpdateData::RunningAsyncEdit{ edit.task_tracker, edit.box }); + state.running_async_edits.push_back( // + VoxelLodTerrainUpdateData::RunningAsyncEdit{ edit.task_tracker, edit.box } + ); } if (boxes_to_preload.size() > 0) { @@ -519,7 +570,7 @@ void process_changed_generated_areas( // VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index]; for (auto box_it = state.changed_generated_areas.begin(); box_it != state.changed_generated_areas.end(); - ++box_it) { + ++box_it) { const Box3i &voxel_box = *box_it; const Box3i bbox = voxel_box.padded(1).downscaled(mesh_block_size << lod_index); @@ -530,8 +581,12 @@ void process_changed_generated_areas( // bbox.for_each_cell_zxy([&lod](const Vector3i bpos) { auto block_it = lod.mesh_map_state.map.find(bpos); if (block_it != lod.mesh_map_state.map.end()) { - VoxelLodTerrainUpdateTask::schedule_mesh_update(block_it->second, bpos, - lod.mesh_blocks_pending_update, block_it->second.mesh_viewers.get() > 0); + VoxelLodTerrainUpdateTask::schedule_mesh_update( + block_it->second, + bpos, + lod.mesh_blocks_pending_update, + block_it->second.mesh_viewers.get() > 0 + ); } }); } @@ -703,7 +758,7 @@ uint8_t VoxelLodTerrainUpdateTask::get_transition_mask( // auto lower_neighbor_block_it = lower_lod.mesh_map_state.map.find(lower_neighbor_pos); if (lower_neighbor_block_it != lower_lod.mesh_map_state.map.end() && - lower_neighbor_block_it->second.visual_active) { + lower_neighbor_block_it->second.visual_active) { // The block has a visible neighbor of lower LOD transition_mask |= dir_mask; continue; @@ -727,7 +782,7 @@ uint8_t VoxelLodTerrainUpdateTask::get_transition_mask( // auto upper_neighbor_block_it = upper_lod.mesh_map_state.map.find(upper_neighbor_pos); if (upper_neighbor_block_it == upper_lod.mesh_map_state.map.end() || - upper_neighbor_block_it->second.visual_active == false) { + upper_neighbor_block_it->second.visual_active == false) { // The block has no visible neighbor yet. World border? Assume lower LOD. transition_mask |= dir_mask; } @@ -779,8 +834,9 @@ void update_transition_masks( // if (recomputed_mask != it->second.transition_mask) { mesh_block.transition_mask = recomputed_mask; - lod.mesh_blocks_to_update_transitions.push_back( - VoxelLodTerrainUpdateData::TransitionUpdate{ it->first, recomputed_mask }); + lod.mesh_blocks_to_update_transitions.push_back( // + VoxelLodTerrainUpdateData::TransitionUpdate{ it->first, recomputed_mask } + ); } } } diff --git a/terrain/voxel_a_star_grid_3d.cpp b/terrain/voxel_a_star_grid_3d.cpp index fb4844e6e..f5996bb7a 100644 --- a/terrain/voxel_a_star_grid_3d.cpp +++ b/terrain/voxel_a_star_grid_3d.cpp @@ -3,6 +3,7 @@ // #include "../util/string/format.h" #include "../constants/voxel_string_names.h" #include "../util/math/conv.h" +#include "../util/string/format.h" namespace zylann::voxel { @@ -122,10 +123,18 @@ void VoxelAStarGrid3D::check_params(Vector3i from_position, Vector3i to_position ZN_PRINT_WARNING("The region is empty or not defined, no path will be found"); } if (!get_region().contains(from_position)) { - ZN_PRINT_WARNING("The current region does not contain the source position, no path will be found"); + ZN_PRINT_WARNING( + format("The current region {} does not contain the source position {}, no path will be found", + get_region(), + from_position) + ); } - if (!get_region().contains(from_position)) { - ZN_PRINT_WARNING("The current region does not contain the destination, no path will be found"); + if (!get_region().contains(to_position)) { + ZN_PRINT_WARNING( + format("The current region {} does not contain the destination {}, no path will be found", + get_region(), + to_position) + ); } } #endif @@ -230,17 +239,20 @@ void VoxelAStarGrid3D::_bind_methods() { ClassDB::bind_method(D_METHOD("find_path", "from_position", "to_position"), &VoxelAStarGrid3D::find_path); ClassDB::bind_method( - D_METHOD("find_path_async", "from_position", "to_position"), &VoxelAStarGrid3D::find_path_async); + D_METHOD("find_path_async", "from_position", "to_position"), &VoxelAStarGrid3D::find_path_async + ); ClassDB::bind_method(D_METHOD("is_running_async"), &VoxelAStarGrid3D::is_running_async); ClassDB::bind_method(D_METHOD("debug_get_visited_positions"), &VoxelAStarGrid3D::debug_get_visited_positions); // Internal ClassDB::bind_method( - D_METHOD("_on_async_search_completed", "path"), &VoxelAStarGrid3D::_b_on_async_search_completed); + D_METHOD("_on_async_search_completed", "path"), &VoxelAStarGrid3D::_b_on_async_search_completed + ); ADD_SIGNAL(MethodInfo( - "async_search_completed", PropertyInfo(Variant::ARRAY, "path", PROPERTY_HINT_ARRAY_TYPE, "Vector3i"))); + "async_search_completed", PropertyInfo(Variant::ARRAY, "path", PROPERTY_HINT_ARRAY_TYPE, "Vector3i") + )); } } // namespace zylann::voxel diff --git a/terrain/voxel_mesh_block.cpp b/terrain/voxel_mesh_block.cpp index 0607a1260..dfe0e8330 100644 --- a/terrain/voxel_mesh_block.cpp +++ b/terrain/voxel_mesh_block.cpp @@ -48,8 +48,12 @@ void VoxelMeshBlock::set_render_layers_mask(int mask) { } } -void VoxelMeshBlock::set_mesh(Ref mesh, GeometryInstance3D::GIMode gi_mode, - RenderingServer::ShadowCastingSetting shadow_setting, int render_layers_mask) { +void VoxelMeshBlock::set_mesh( + Ref mesh, + GeometryInstance3D::GIMode gi_mode, + RenderingServer::ShadowCastingSetting shadow_setting, + int render_layers_mask +) { // TODO Don't add mesh instance to the world if it's not visible. // I suspect Godot is trying to include invisible mesh instances into the culling process, // which is killing performance when LOD is used (i.e many meshes are in pool but hidden) @@ -139,7 +143,7 @@ void VoxelMeshBlock::set_parent_transform(const Transform3D &parent_transform) { } } -void VoxelMeshBlock::set_collision_shape(Ref shape, bool debug_collision, Node3D *node, float margin) { +void VoxelMeshBlock::set_collision_shape(Ref shape, bool debug_collision, const Node3D *node, float margin) { ERR_FAIL_COND(node == nullptr); ERR_FAIL_COND_MSG(node->get_world_3d() != _world, "Physics body and attached node must be from the same world"); @@ -211,7 +215,9 @@ bool VoxelMeshBlock::is_collision_enabled() const { } Ref make_collision_shape_from_mesher_output( - const VoxelMesher::Output &mesher_output, const VoxelMesher &mesher) { + const VoxelMesher::Output &mesher_output, + const VoxelMesher &mesher +) { using namespace zylann::godot; Ref shape; @@ -221,13 +227,15 @@ Ref make_collision_shape_from_mesher_output( // Use a sub-region of the render mesh if (mesher_output.surfaces.size() > 0) { shape = create_concave_polygon_shape( - mesher_output.surfaces[0].arrays, mesher_output.collision_surface.submesh_index_end); + mesher_output.surfaces[0].arrays, mesher_output.collision_surface.submesh_index_end + ); } } else { // Use specialized collision mesh - shape = create_concave_polygon_shape(to_span(mesher_output.collision_surface.positions), - to_span(mesher_output.collision_surface.indices)); + shape = create_concave_polygon_shape( + to_span(mesher_output.collision_surface.positions), to_span(mesher_output.collision_surface.indices) + ); } } else { diff --git a/terrain/voxel_mesh_block.h b/terrain/voxel_mesh_block.h index c7c1c5d8e..9dd860d70 100644 --- a/terrain/voxel_mesh_block.h +++ b/terrain/voxel_mesh_block.h @@ -35,8 +35,12 @@ class VoxelMeshBlock : public NonCopyable { // Visuals - void set_mesh(Ref mesh, GeometryInstance3D::GIMode gi_mode, - RenderingServer::ShadowCastingSetting shadow_setting, int render_layers_mask); + void set_mesh( + Ref mesh, + GeometryInstance3D::GIMode gi_mode, + RenderingServer::ShadowCastingSetting shadow_setting, + int render_layers_mask + ); Ref get_mesh() const; bool has_mesh() const; void drop_mesh(); @@ -61,7 +65,7 @@ class VoxelMeshBlock : public NonCopyable { // Collisions - void set_collision_shape(Ref shape, bool debug_collision, Node3D *node, float margin); + void set_collision_shape(Ref shape, bool debug_collision, const Node3D *node, float margin); bool has_collision_shape() const; void set_collision_layer(int layer); void set_collision_mask(int mask); @@ -97,7 +101,9 @@ class VoxelMeshBlock : public NonCopyable { }; Ref make_collision_shape_from_mesher_output( - const VoxelMesher::Output &mesher_output, const VoxelMesher &mesher); + const VoxelMesher::Output &mesher_output, + const VoxelMesher &mesher +); } // namespace zylann::voxel diff --git a/tests/util/test_island_finder.cpp b/tests/util/test_island_finder.cpp index b0b3407a2..5c8bccb76 100644 --- a/tests/util/test_island_finder.cpp +++ b/tests/util/test_island_finder.cpp @@ -39,10 +39,10 @@ void test_island_finder() { ; const Vector3i grid_size(5, 5, 5); - ZN_TEST_ASSERT(Vector3iUtil::get_volume(grid_size) == static_cast(strlen(cdata) / 2)); + ZN_TEST_ASSERT(Vector3iUtil::get_volume_u64(grid_size) == (strlen(cdata) / 2)); StdVector grid; - grid.resize(Vector3iUtil::get_volume(grid_size)); + grid.resize(Vector3iUtil::get_volume_u64(grid_size)); for (unsigned int i = 0; i < grid.size(); ++i) { const char c = cdata[i * 2]; if (c == 'X') { @@ -55,7 +55,7 @@ void test_island_finder() { } StdVector output; - output.resize(Vector3iUtil::get_volume(grid_size)); + output.resize(Vector3iUtil::get_volume_u64(grid_size)); unsigned int label_count; IslandFinder island_finder; diff --git a/tests/util/test_spatial_lock.cpp b/tests/util/test_spatial_lock.cpp index 8643d02c4..92adde0b7 100644 --- a/tests/util/test_spatial_lock.cpp +++ b/tests/util/test_spatial_lock.cpp @@ -82,7 +82,7 @@ void test_spatial_lock_spam() { public: Map(Vector3i p_size) { _size = p_size; - _cells.resize(Vector3iUtil::get_volume(_size), 0); + _cells.resize(Vector3iUtil::get_volume_u64(_size), 0); } inline Vector3i get_size() const { @@ -146,7 +146,7 @@ void test_spatial_lock_spam() { StdVector &expected_values = reusable_vector; expected_values.clear(); - expected_values.reserve(Vector3iUtil::get_volume(box.size)); + expected_values.reserve(Vector3iUtil::get_volume_u64(box.size)); box.for_each_cell([&map, &expected_values](Vector3i pos) { // expected_values.push_back(map.at(pos)); }); diff --git a/tests/voxel/test_octree.cpp b/tests/voxel/test_octree.cpp index c56d018e8..f8418ed7b 100644 --- a/tests/voxel/test_octree.cpp +++ b/tests/voxel/test_octree.cpp @@ -236,7 +236,7 @@ void test_octree_find_in_box() { }); ZN_TEST_ASSERT(checksum2 == checksum); const int for_each_cell_time = profiling_clock.restart(); - const float single_query_time = float(for_each_cell_time) / Vector3iUtil::get_volume(full_box.size); + const float single_query_time = float(for_each_cell_time) / Vector3iUtil::get_volume_u64(full_box.size); print_line(String("for_each_cell time with {0} lods: total {1} us, single query {2} us, checksum: {3}") .format(varray(lods, for_each_cell_time, single_query_time, checksum2))); } diff --git a/tests/voxel/test_storage_funcs.cpp b/tests/voxel/test_storage_funcs.cpp index 47be1d338..960ab9334 100644 --- a/tests/voxel/test_storage_funcs.cpp +++ b/tests/voxel/test_storage_funcs.cpp @@ -22,8 +22,15 @@ void test_encode_weights_packed_u16() { void test_copy_3d_region_zxy() { struct L { - static void compare(Span srcs, Vector3i src_size, Vector3i src_min, Vector3i src_max, - Span dsts, Vector3i dst_size, Vector3i dst_min) { + static void compare( + Span srcs, + Vector3i src_size, + Vector3i src_min, + Vector3i src_max, + Span dsts, + Vector3i dst_size, + Vector3i dst_min + ) { Vector3i pos; for (pos.z = src_min.z; pos.z < src_max.z; ++pos.z) { for (pos.x = src_min.x; pos.x < src_max.x; ++pos.x) { @@ -42,8 +49,8 @@ void test_copy_3d_region_zxy() { StdVector dst; const Vector3i src_size(8, 8, 8); const Vector3i dst_size(3, 4, 5); - src.resize(Vector3iUtil::get_volume(src_size), 0); - dst.resize(Vector3iUtil::get_volume(dst_size), 0); + src.resize(Vector3iUtil::get_volume_u64(src_size), 0); + dst.resize(Vector3iUtil::get_volume_u64(dst_size), 0); for (unsigned int i = 0; i < src.size(); ++i) { src[i] = i; } @@ -95,8 +102,8 @@ void test_copy_3d_region_zxy() { StdVector dst; const Vector3i src_size(3, 4, 5); const Vector3i dst_size(3, 4, 5); - src.resize(Vector3iUtil::get_volume(src_size), 0); - dst.resize(Vector3iUtil::get_volume(dst_size), 0); + src.resize(Vector3iUtil::get_volume_u64(src_size), 0); + dst.resize(Vector3iUtil::get_volume_u64(dst_size), 0); for (unsigned int i = 0; i < src.size(); ++i) { src[i] = i; } @@ -115,26 +122,26 @@ void test_copy_3d_region_zxy() { void test_transform_3d_array_zxy() { // YXZ int src_grid[] = { - 0, 1, 2, 3, // - 4, 5, 6, 7, // - 8, 9, 10, 11, // + 0, 1, 2, 3, // + 4, 5, 6, 7, // + 8, 9, 10, 11, // 12, 13, 14, 15, // 16, 17, 18, 19, // 20, 21, 22, 23 // }; const Vector3i src_size(3, 4, 2); - const unsigned int volume = Vector3iUtil::get_volume(src_size); + const unsigned int volume = Vector3iUtil::get_volume_u64(src_size); FixedArray dst_grid; ZN_TEST_ASSERT(dst_grid.size() == volume); { int expected_dst_grid[] = { - 0, 4, 8, // - 1, 5, 9, // - 2, 6, 10, // - 3, 7, 11, // + 0, 4, 8, // + 1, 5, 9, // + 2, 6, 10, // + 3, 7, 11, // 12, 16, 20, // 13, 17, 21, // @@ -158,9 +165,9 @@ void test_transform_3d_array_zxy() { } { int expected_dst_grid[] = { - 3, 2, 1, 0, // - 7, 6, 5, 4, // - 11, 10, 9, 8, // + 3, 2, 1, 0, // + 7, 6, 5, 4, // + 11, 10, 9, 8, // 15, 14, 13, 12, // 19, 18, 17, 16, // @@ -187,9 +194,9 @@ void test_transform_3d_array_zxy() { 19, 18, 17, 16, // 23, 22, 21, 20, // - 3, 2, 1, 0, // - 7, 6, 5, 4, // - 11, 10, 9, 8 // + 3, 2, 1, 0, // + 7, 6, 5, 4, // + 11, 10, 9, 8 // }; const Vector3i expected_dst_size(3, 4, 2); IntBasis basis; diff --git a/tests/voxel/test_voxel_graph.cpp b/tests/voxel/test_voxel_graph.cpp index 6cc9ab6b7..e81cf0f76 100644 --- a/tests/voxel/test_voxel_graph.cpp +++ b/tests/voxel/test_voxel_graph.cpp @@ -1899,7 +1899,7 @@ void test_voxel_graph_function_execute() { } const Vector3i block_size(16, 18, 20); - const int volume = Vector3iUtil::get_volume(block_size); + const size_t volume = Vector3iUtil::get_volume_u64(block_size); StdVector x_buffer; StdVector y_buffer; @@ -1929,7 +1929,7 @@ void test_voxel_graph_function_execute() { Span outputs = to_span(sd_buffer); function->execute(Span>(inputs, 3), Span>(&outputs, 1)); - for (int i = 0; i < volume; ++i) { + for (size_t i = 0; i < volume; ++i) { const float obtained_result = sd_buffer[i]; const float expected_result = Math::sin(x_buffer[i]) + Math::cos(z_buffer[i]) + y_buffer[i]; ZN_TEST_ASSERT(Math::is_equal_approx(obtained_result, expected_result)); diff --git a/util/containers/span.h b/util/containers/span.h index 097eb8779..738d39c96 100644 --- a/util/containers/span.h +++ b/util/containers/span.h @@ -103,14 +103,16 @@ class [[nodiscard]] Span { } } - inline void copy_to(Span other) const { + // Template because T could be const and TDst should not be + template + inline void copy_to(Span other) const { ZN_ASSERT(other.size() == _size); - ZN_ASSERT(other._ptr != nullptr); + ZN_ASSERT(other.data() != nullptr); // for (size_t i = 0; i < _size; ++i) { // other._ptr[i] = _ptr[i]; // } // Should compile to memcpy if T is simple enough - std::copy(_ptr, _ptr + _size, other._ptr); + std::copy(_ptr, _ptr + _size, other.data()); } inline bool overlaps(const Span other) const { diff --git a/util/godot/classes/curve.h b/util/godot/classes/curve.h index dc08f87fd..386117c1f 100644 --- a/util/godot/classes/curve.h +++ b/util/godot/classes/curve.h @@ -8,4 +8,19 @@ using namespace godot; #endif +#include "../../math/interval.h" +#include "../core/version.h" + +namespace zylann::godot { + +inline math::Interval get_curve_domain(const Curve &curve) { +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3 + return math::Interval(0, 1); +#else + return math::Interval(curve.get_min_domain(), curve.get_max_domain()); +#endif +} + +} // namespace zylann::godot + #endif // ZN_GODOT_CURVE_H diff --git a/util/godot/classes/editor_import_plugin.cpp b/util/godot/classes/editor_import_plugin.cpp index 5b762ceb4..6dd8cc647 100644 --- a/util/godot/classes/editor_import_plugin.cpp +++ b/util/godot/classes/editor_import_plugin.cpp @@ -65,6 +65,9 @@ bool ZN_EditorImportPlugin::get_option_visibility( } Error ZN_EditorImportPlugin::import( +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 4 + ResourceUID::ID p_source_id, +#endif const String &p_source_file, const String &p_save_path, const HashMap &p_options, diff --git a/util/godot/classes/editor_import_plugin.h b/util/godot/classes/editor_import_plugin.h index 1eb0e835f..3c4b08a81 100644 --- a/util/godot/classes/editor_import_plugin.h +++ b/util/godot/classes/editor_import_plugin.h @@ -102,6 +102,9 @@ class ZN_EditorImportPlugin : public EditorImportPlugin { ) const override; Error import( +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 4 + ResourceUID::ID p_source_id, +#endif const String &p_source_file, const String &p_save_path, const HashMap &p_options, diff --git a/util/godot/direct_static_body.cpp b/util/godot/direct_static_body.cpp index 5ba8987ec..6eac899f8 100644 --- a/util/godot/direct_static_body.cpp +++ b/util/godot/direct_static_body.cpp @@ -98,7 +98,7 @@ void DirectStaticBody::set_shape_enabled(int shape_index, bool enabled) { } } -void DirectStaticBody::set_attached_object(Object *obj) { +void DirectStaticBody::set_attached_object(const Object *obj) { // Serves in high-level collision query results, `collider` will contain the attached object ERR_FAIL_COND(!_body.is_valid()); PhysicsServer3D::get_singleton()->body_attach_object_instance_id( @@ -123,7 +123,7 @@ void DirectStaticBody::set_debug(bool enabled, World3D *world) { _debug_mesh_instance.create(); _debug_mesh_instance.set_world(world); - Transform3D transform = + const Transform3D transform = PhysicsServer3D::get_singleton()->body_get_state(_body, PhysicsServer3D::BODY_STATE_TRANSFORM); _debug_mesh_instance.set_transform(transform); diff --git a/util/godot/direct_static_body.h b/util/godot/direct_static_body.h index 4848301a6..b3d286d4f 100644 --- a/util/godot/direct_static_body.h +++ b/util/godot/direct_static_body.h @@ -25,7 +25,7 @@ class DirectStaticBody : public zylann::NonCopyable { Ref get_shape(int shape_index); void set_world(World3D *world); void set_shape_enabled(int shape_index, bool disabled); - void set_attached_object(Object *obj); + void set_attached_object(const Object *obj); void set_collision_layer(int layer); void set_collision_mask(int mask); diff --git a/util/island_finder.h b/util/island_finder.h index 66ee10a58..fd3b3f1cf 100644 --- a/util/island_finder.h +++ b/util/island_finder.h @@ -23,7 +23,7 @@ class IslandFinder { template void scan_3d(Box3i box, VolumePredicate_F volume_predicate_func, Span output, unsigned int *out_count) { - const size_t volume = Vector3iUtil::get_volume(box.size); + const size_t volume = Vector3iUtil::get_volume_u64(box.size); CRASH_COND(output.size() != volume); memset(output.data(), 0, volume * sizeof(uint8_t)); diff --git a/util/math/funcs.h b/util/math/funcs.h index 04a436892..6c2dec6f2 100644 --- a/util/math/funcs.h +++ b/util/math/funcs.h @@ -497,6 +497,17 @@ inline T pow(T x, T y) { return Math::pow(x, y); } +inline uint64_t multiply_check_overflow_u64(const uint64_t a, const uint64_t b) { + const uint64_t r = a * b; +#ifdef DEV_ENABLED + if (a != 0 && r / a != b) { + ZN_PRINT_ERROR("Multiplication overflow"); + return 0; + } +#endif + return r; +} + } // namespace zylann::math #endif // VOXEL_MATH_FUNCS_H diff --git a/util/math/vector3i.h b/util/math/vector3i.h index c5191bc45..040f896ea 100644 --- a/util/math/vector3i.h +++ b/util/math/vector3i.h @@ -33,13 +33,15 @@ inline void sort_min_max(Vector3i &a, Vector3i &b) { } // Returning a 64-bit integer because volumes can quickly overflow INT_MAX (like 1300^3), -// even though dense volumes of that size will rarely be encountered in this module -inline int64_t get_volume(const Vector3i &v) { +// even though dense volumes of that size will rarely be encountered in this module. +inline uint64_t get_volume_u64(const Vector3i &v) { #ifdef DEBUG_ENABLED ZN_ASSERT_RETURN_V(v.x >= 0 && v.y >= 0 && v.z >= 0, 0); #endif - // TODO Overflow-checking multiplication in debug builds? - return static_cast(v.x) * static_cast(v.y) * static_cast(v.z); + return math::multiply_check_overflow_u64( + static_cast(v.x), + math::multiply_check_overflow_u64(static_cast(v.y), static_cast(v.z)) + ); } inline unsigned int get_zxy_index(const Vector3i &v, const Vector3i area_size) { diff --git a/util/noise/spot_noise.h b/util/noise/spot_noise.h index f4f571c3a..5493a4642 100644 --- a/util/noise/spot_noise.h +++ b/util/noise/spot_noise.h @@ -199,7 +199,7 @@ inline math::Interval spot_noise_3d_range( ivec3 min_cell_origin_norm_i = to_vec3i(min_cell_origin_norm); ivec3 max_cell_origin_norm_i = to_vec3i(max_cell_origin_norm); - if (Vector3iUtil::get_volume(max_cell_origin_norm_i - min_cell_origin_norm_i + ivec3(1, 1, 1)) > 30) { + if (Vector3iUtil::get_volume_u64(max_cell_origin_norm_i - min_cell_origin_norm_i + ivec3(1, 1, 1)) > 30) { // Don't bother checking too many cells, assume we'll intersect a spot. return math::Interval(0, 1); } diff --git a/util/voxel_raycast.h b/util/voxel_raycast.h index 6f4e42272..e0234296d 100644 --- a/util/voxel_raycast.h +++ b/util/voxel_raycast.h @@ -1,3 +1,6 @@ +#ifndef ZN_VOXEL_RAYCAST_H +#define ZN_VOXEL_RAYCAST_H + #include "../util/math/vector3i.h" // #include "../util/profiling.h" #include "errors.h" @@ -59,7 +62,9 @@ bool voxel_raycast( // Note : the grid is assumed to have 1-unit square cells. +#ifdef DEBUG_ENABLED ZN_ASSERT_RETURN_V(math::is_normalized(ray_direction), false); // Must be normalized +#endif /* Initialisation */ @@ -202,3 +207,5 @@ bool voxel_raycast( } } // namespace zylann + +#endif // ZN_VOXEL_RAYCAST_H