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/SCsub b/SCsub
index 3573cb7e5..50312e848 100644
--- a/SCsub
+++ b/SCsub
@@ -37,9 +37,9 @@ if INCLUDE_TESTS:
# SQLite
env_sqlite = env_voxel.Clone()
-# Turn off some warnings produced when compiling with warnings=extra
-if env["warnings"] == "extra":
- env_sqlite.disable_warnings()
+# Turn off warnings, we get some with Clang and with `warnings=extra`
+env_sqlite.disable_warnings()
+
# if not env_sqlite.msvc:
# env_sqlite.Append(CXXFLAGS=["-Wno-discarded-qualifiers"])
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/VoxelBuffer.xml b/doc/classes/VoxelBuffer.xml
index f47518f1a..dcde56887 100644
--- a/doc/classes/VoxelBuffer.xml
+++ b/doc/classes/VoxelBuffer.xml
@@ -164,6 +164,7 @@
+ Gets which memory allocator is used by this buffer.
@@ -408,12 +409,15 @@
How many compression modes there are.
+ Uses Godot's default memory allocator (at time of writing, it is [code]malloc[/code]). Preferred for occasional buffers with uncommon size, or very large size.
+ Uses a pool allocator. Can be faster than the default allocator buffers are created very frequently with similar size. This memory will remain allocated after use, under the assumption that other buffers will need it soon after. Does not support very large buffers (greater than 2 megabytes)
+ Maximum size a buffer can have when serialized. Buffers that contain uniform-compressed voxels can reach it, but in practice, the limit is much lower and depends on available memory.
diff --git a/doc/classes/VoxelColorPalette.xml b/doc/classes/VoxelColorPalette.xml
index ed0a67eb9..a41cb3b15 100644
--- a/doc/classes/VoxelColorPalette.xml
+++ b/doc/classes/VoxelColorPalette.xml
@@ -6,6 +6,7 @@
Contains a list of colors that can be accessed quickly by their index. This is useful to store colored voxels with small memory footprint.
It can be used with [VoxelMesherCubes].
+ Note: colors are internally stored with 8-bit components, so floats used by [Color] will be rounded to the nearest value.
@@ -26,8 +27,10 @@
+ Array of all the colors in the palette. It must have 256 elements. You may use this if you want to get or set all colors at once. Otherwise, prefer using [method get_color] and [method set_color].
+ Array used to store colors as 8-bit binary components in the resource file. It must have 256 elements. To access colors with a script, prefer using [method get_color] and [method set_color].
diff --git a/doc/classes/VoxelGeneratorFlat.xml b/doc/classes/VoxelGeneratorFlat.xml
index 414a66da3..4e9df74ea 100644
--- a/doc/classes/VoxelGeneratorFlat.xml
+++ b/doc/classes/VoxelGeneratorFlat.xml
@@ -9,13 +9,13 @@
- Channel that will be used to generate the ground.
+ Channel that will be used to generate the ground. Use [member VoxelBuffer.CHANNEL_SDF] for smooth terrain, other channels for blocky.
Altitude of the ground.
- If [member channel] is set to [constant VoxelBuffer.CHANNEL_TYPE], this value will be used to fill ground voxels.
+ If [member channel] is set to any channel other than [constant VoxelBuffer.CHANNEL_SDF], this value will be used to fill ground voxels, while air voxels will be set to 0.
diff --git a/doc/classes/VoxelGeneratorHeightmap.xml b/doc/classes/VoxelGeneratorHeightmap.xml
index dc9304f6b..0d0826b22 100644
--- a/doc/classes/VoxelGeneratorHeightmap.xml
+++ b/doc/classes/VoxelGeneratorHeightmap.xml
@@ -1,6 +1,7 @@
+ Base class for several basic height-based generators.
@@ -8,6 +9,7 @@
+ Channel where voxels will be generated. If set to [constant VoxelBuffer.CHANNEL_SDF], voxels will be a signed distance field usable by smooth meshers. Otherwise, the value 1 will be set below ground, and the value 0 will be set above ground (blocky).
Maximum distance between the lowest and highest surface points that can generate.
@@ -17,7 +19,7 @@
Minimum height where the surface will generate.
- Scale applied to the signed distance field. This is useful when smooth voxels are used, to reduce blockyness over large distances.
+ Scale applied to the signed distance field when using a smooth terrain configuration.
diff --git a/doc/classes/VoxelGeneratorImage.xml b/doc/classes/VoxelGeneratorImage.xml
index c95efce0f..feb649e8e 100644
--- a/doc/classes/VoxelGeneratorImage.xml
+++ b/doc/classes/VoxelGeneratorImage.xml
@@ -4,6 +4,8 @@
Voxel generator producing a heightmap-based shape using an image.
+ Uses the red channel of an image to generate a heightmap, such that the top left corner is centered on the world origin. The image will repeat if terrain generates beyond its size.
+ Note: values in the image are read using `get_pixel` and are assumed to be between 0 and 1 (normalized). These values will be transformed by [member VoxelGeneratorHeightmap.height_start] and [member VoxelGeneratorHeightmap.height_range].
@@ -12,6 +14,7 @@
+ Sets the image that will be used as a heightmap. Only the red channel will be used. It is preferable to use an image using the `RF` or `RH` format, which contain higher resolution heights. Common images only have 8-bit depth and will appear blocky.
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/VoxelGraphFunction.xml b/doc/classes/VoxelGraphFunction.xml
index 12b538a05..8917285ad 100644
--- a/doc/classes/VoxelGraphFunction.xml
+++ b/doc/classes/VoxelGraphFunction.xml
@@ -1,12 +1,18 @@
- Graph for generating or processing voxels.
+ Graph for generating or processing series of 3D values.
- Contains a graph that can be used to generate voxel data (when used as main function of a generator), or to be re-used into other graphs (like a sub-graph).
- Currently this class only stores a graph, it cannot run actual processing on its own. To generate voxels with it, see [VoxelGeneratorGraph].
- Note: node types are identified with the enum [enum VoxelGraphFunction.NodeTypeID]. This enum shouldn't be used in persistent contexts (such as save files) as its values may change between versions.
+ Contains a graph that can be used to process series of values, such as voxel positions (when used as main function of a generator), or to be re-used into other graphs (like a sub-graph).
+ Currently this class only stores a graph, it cannot run actual processing on its own. It is usually embedded into another resource which then makes use of the graph in a specific way.
+ To generate voxels with it, see [VoxelGeneratorGraph].
+ Nodes can be connected together from their outputs to the inputs of next nodes. Unconnected inputs can have default values or default implicit connections.
+ Nodes can also have "parameters" which are constants setup per node.
+ Nodes come in 3 main families: inputs (only have outputs), outputs (only have inputs), and others (which have both inputs and output to do some calculation).
+ Node types are identified with the enum [enum VoxelGraphFunction.NodeTypeID]. This enum shouldn't be used in persistent contexts (such as save files) as its values may change between versions.
+ Graphs can only process 32-bit floating point values.
+ Description of node types is present in the graph editor node dialog, or at [url]https://voxel-tools.readthedocs.io/en/latest/graph_nodes[/url].
@@ -68,6 +74,19 @@
+ Gets an array describing all connections between nodes.
+ The array has the following format:
+ [codeblock]
+ [
+ {
+ "src_node_id": int,
+ "src_port_index": int,
+ "dst_node_id": int,
+ "dst_port_index": int
+ },
+ ...
+ ]
+ [/codeblock]
@@ -101,6 +120,7 @@
Get a list of IDs of all the nodes in the graph.
+ Note: the order in which IDs are returned is not guaranteed to be the same after nodes are added or removed.
@@ -115,6 +135,7 @@
+ Get a parameter of a node. The parameter index corresponds to the position that parameter comes in when seen in the editor.
@@ -168,7 +189,7 @@
Copies nodes into another graph, and connections between them only.
Resources in node parameters will be duplicated if they don't have a file path.
- If [code]node_ids[/code] is provided with non-zero size, defines the IDs of copied nodes. Otherwise, they are generated.
+ If [code]node_ids[/code] is provided with non-zero size, defines the IDs copied nodes will have in the destination graph, in the same order as [method get_node_ids] from the source graph. The array must have the same size as the number of copied nodes and IDs must not already exist in the destination graph. If the array is empty, they will be generated instead.
@@ -194,7 +215,7 @@
Configures inputs for an Expression node. [code]names[/code] is the list of input names used in the expression.
- [code]value[/code] must be a [code]float[/code] for now.
+ If you create an Expression node from code, you should call this method afterwards.
@@ -203,6 +224,7 @@
+ Sets the value an input of a node will have when it is left unconnected.
@@ -211,7 +233,7 @@
Sets wether a node input with no inbound connection will automatically create a default connection when the graph is compiled.
- This is only available on specific nodes. On other nodes, it has no effect.
+ This is only available on specific nodes (for example, 2D or 3D noise defaults to XYZ inputs). On other nodes, it has no effect.
diff --git a/doc/classes/VoxelLodTerrain.xml b/doc/classes/VoxelLodTerrain.xml
index f7557c07f..889adbd38 100644
--- a/doc/classes/VoxelLodTerrain.xml
+++ b/doc/classes/VoxelLodTerrain.xml
@@ -299,6 +299,7 @@
Material used for the surface of the volume. The main usage of this node is with smooth voxels, which means if you want more than one "material" on the ground, you need to use splatmapping techniques with a shader. In addition, many features require shaders to work properly. Check the online documentation or examples for more information.
+ Note: if you use a [ShaderMaterial], it will be instanced on every chunk in order to support per-chunk/LOD features, so dynamic changes done to parameters will not apply. You can use [url=https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_language.html#global-uniforms]global uniforms[/url] to workaround this limitation.
Size of meshes used for chunks of this volume, in voxels. Can only be set to either 16 or 32. Using 32 is expected to increase rendering performance, and slightly increase the cost of edits.
diff --git a/doc/classes/VoxelMesherTransvoxel.xml b/doc/classes/VoxelMesherTransvoxel.xml
index ed3aa983c..595a7e819 100644
--- a/doc/classes/VoxelMesherTransvoxel.xml
+++ b/doc/classes/VoxelMesherTransvoxel.xml
@@ -13,6 +13,7 @@
+ Generates only the part of the mesh that Transvoxel uses to connect surfaces with different level of detail. This method is mainly for testing purposes.
@@ -35,8 +36,13 @@
+ Disables texturing information. This mode is the fastest if you can use a shader to apply textures procedurally.
+ Adds texturing information as 4 texture indices and 4 weights, encoded in [code]CUSTOM1.xy[/code] in Godot fragment shaders, where x and y contain 4 packed 8-bit values.
+ Expects voxels to have 4 4-bit indices packed in 16-bit values in [constant VoxelBuffer.CHANNEL_INDICES], and 4 4-bit weights in [constant VoxelBuffer.CHANNEL_WEIGHTS].
+ In cases where more than 4 textures cross each other in a 2x2x2 voxel area, triangles in that area will only use the 4 indices with the highest weights.
+ A custom shader is required to render this, usually with texture arrays to index textures easily.
diff --git a/doc/classes/VoxelRaycastResult.xml b/doc/classes/VoxelRaycastResult.xml
index 62b7d9306..549c62031 100644
--- a/doc/classes/VoxelRaycastResult.xml
+++ b/doc/classes/VoxelRaycastResult.xml
@@ -9,12 +9,13 @@
+ Distance between the origin of the ray and the surface of the cube representing the hit voxel.
- Integer position of the voxel that was hit.
+ Integer position of the voxel that was hit. In a blocky game, this would be the position of the voxel to interact with.
- Integer position of the previous voxel along the ray before the final hit.
+ Integer position of the previous voxel along the ray before the final hit. In a blocky game, this would be the position of the voxel to place on top of the pointed one.
diff --git a/doc/classes/VoxelSaveCompletionTracker.xml b/doc/classes/VoxelSaveCompletionTracker.xml
index 868369228..b84dd6f4d 100644
--- a/doc/classes/VoxelSaveCompletionTracker.xml
+++ b/doc/classes/VoxelSaveCompletionTracker.xml
@@ -1,6 +1,7 @@
+ Object returned by some asynchronous functions to track progress and completion.
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/VoxelStreamScript.xml b/doc/classes/VoxelStreamScript.xml
index 1539121cd..7f26321c1 100644
--- a/doc/classes/VoxelStreamScript.xml
+++ b/doc/classes/VoxelStreamScript.xml
@@ -11,6 +11,7 @@
+ Tells which channels in [VoxelBuffer] are supported to save voxel data, in case the stream only saves specific ones.
@@ -19,6 +20,7 @@
+ Called when a block of voxels needs to be loaded. Assumes [code]out_buffer[/code] always has the same size. Returns [enum VoxelStream.ResultCode].
@@ -27,6 +29,7 @@
+ Called when a block of voxels needs to be saved. Assumes [code]out_buffer[/code] always has the same size.
diff --git a/doc/classes/VoxelTool.xml b/doc/classes/VoxelTool.xml
index 3e9c22c6d..31762d5da 100644
--- a/doc/classes/VoxelTool.xml
+++ b/doc/classes/VoxelTool.xml
@@ -49,6 +49,7 @@
+ Traces a "tube" defined by a list of points, each having a corresponding radius to control the width of the tube at each point. The begin and end of the path is rounded. This is equivalent to placing/carving multiple connected capsules with varying top/bottom radius. The path is not using bezier or splines, each point is connected linearly to the next. If you need more smoothness, you may add points to areas that need them. The more points, the slower it is.
@@ -160,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.
@@ -245,6 +246,7 @@
+ Index of the texture used in smooth voxel texture painting mode. The choice of this index depends on the way you setup rendering of textured voxel meshes (for example, layer index in a texture array).
diff --git a/doc/classes/ZN_FastNoiseLiteGradient.xml b/doc/classes/ZN_FastNoiseLiteGradient.xml
index 874c34848..ddd80f460 100644
--- a/doc/classes/ZN_FastNoiseLiteGradient.xml
+++ b/doc/classes/ZN_FastNoiseLiteGradient.xml
@@ -5,6 +5,7 @@
This is an alternate implementation of [FastNoiseLite], because Godot's integration does not expose methods to access gradients directly.
+ These algorithms are specialized to generate vectors to perturb positions, and can be faster than calculating 2 or 3 times a regular noise.
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 0bbe45d6c..78660599b 100644
--- a/doc/source/changelog.md
+++ b/doc/source/changelog.md
@@ -12,8 +12,11 @@ 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)
- `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)
@@ -22,15 +25,25 @@ Primarily developped with Godot 4.3.
- `VoxelLodTerrain`:
- Fixed potential crash when when using the Clipbox streaming system with threaded update (thanks to lenesxy, issue #692)
- Fixed blocks were saved with incorrect LOD index when they get unloaded using Clipbox, leading to holes and mismatched terrain (#691)
- - `VoxelTerrain`: edits and copies across fixed bounds no longer behave as if terrain generates beyond (was causing "walls" to appear).
- - `VoxelGeneratorGraph`: fix wrong values when using `OutputWeight` with optimized execution map enabled, when weights are determined to be locally constant
+ - 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).
+ - 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
+ - Fixed shader generation error when using the `Distance3D` node (vec2 instead of vec3, thanks to scwich)
+ - Fixed crash when assigning an empty image to the `Image` node
- `VoxelMesherTransvoxel`: revert texturing logic that attempted to prevent air voxels from contributing, but was lowering quality. It is now optional as an experimental property.
- `VoxelStreamSQLite`: Fixed "empty size" errors when loading areas with edited `VoxelInstancer` data
+ - `.vox` scene importer: disabled threaded import to workaround the editor freezing when saving meshes
- Breaking changes
- `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/editor.md b/doc/source/editor.md
index fb2441988..eb4868c34 100644
--- a/doc/source/editor.md
+++ b/doc/source/editor.md
@@ -22,7 +22,7 @@ The whole terrain can be told to re-mesh or re-load by using one of the options
### Camera options
-Blocks will only load around the node's origin by default. If the volume is very big or uses LOD, it will not load further and concentrate detail at its center. You can override this by going in the `Terrain` menu and enabling `Stream follow camera`. This will make the terrain adapt its level of detail and blocks to be around the editor's camera, and will update as the camera moves. Turning off the option will freeze the terrain.
+In the editor, blocks will only load around the node's origin by default. If the volume is very big or uses LOD, it will not load further and concentrate detail at its center. You can override this by going in the `Terrain` menu and enabling `Stream follow camera`. This will make the terrain adapt its level of detail and blocks to be around the editor's camera, and will update as the camera moves. Turning off the option will freeze the terrain.
![Stream follow camera menu](images/menu_stream_follow_camera.webp)
@@ -37,9 +37,9 @@ Terrains can be very big, and sometimes Godot might prevent you from zooming out
Editing
--------
-Editing voxel volumes destructively in the Godot Editor is not supported yet. This feature may be implemented in the future.
+There are no tools to edit voxel volumes destructively in the Godot Editor yet. This feature might be implemented in the future.
-It is possible to use non-destructive [modifiers](generators.md#modifiers).
+It is possible to use non-destructive [modifiers](generators.md#modifiers), but they are limited.
-Terrains can be fully edited in-game using scripts and [VoxelTool](scripting.md).
+Terrains can be fully edited in-game using scripts and [VoxelTool](scripting.md). It is also possible to create a script editor plugin to implement edition in a similar manner.
diff --git a/doc/source/generators.md b/doc/source/generators.md
index 081739ad5..cdbb83437 100644
--- a/doc/source/generators.md
+++ b/doc/source/generators.md
@@ -72,7 +72,7 @@ Basic generators may often not be suited to make a whole game from, but you don'
Voxel graphs allow to represent a 3D density by connecting operation nodes together. It takes 3D coordinates (X, Y, Z), and computes the value of every voxel from them. For example it can do a simple 2D or 3D noise, which can be scaled, deformed, masked using other noises, curves or even images.
-A big inspiration of this approach comes again from sculpting of signed-distance-fields (every voxel stores the distance to the nearest surface), which is why the main output node may be an `SdfOutput`. A bunch of nodes are meant to work on SDF as well. However, it is not strictly necessary to respect perfect distances, as long as the result looks correct for a game, so most of the time it's easier to work with approximations.
+An inspiration of this approach comes again from sculpting of signed-distance-fields (every voxel stores the distance to the nearest surface), which is why the main output node is usually an `SdfOutput`. A bunch of nodes are meant to work on SDF as well. However, it is not strictly necessary to respect perfect distances, as long as the result looks correct for a game, so most of the time it's easier to work with approximations.
!!! note
Voxel graphs are half-way between programming 3D shaders and procedural design. It has similar speed to C++ generators but has only basic instructions, so there are some maths involved. This might get eased a bit in the future when more high-level nodes are added.
@@ -209,6 +209,14 @@ With a sphere, gradients are quite regular, which is usually best. But when usin
Noise is a lot more inconsistent. However, most of the time, this isn't a big deal. It depends on what operations you do with the terrain.
If the "speed" of gradients varies too sharply, especially near the surface, it can be the cause of precision loss or blockyness in generated meshes. It can also be a problem when approximating the surface solely from SDF voxels or using sphere tracing.
+### Scripting
+
+Graph generators can be modified from a script using the [VoxelGraphFunction](api/VoxelGraphFunction.md) API. This is useful for example if you design a base graph, and want to randomize noise seeds or adjust some constants. `VoxelGeneratorGraph` contains an instance of it as their "main" function. You can access the graph by calling `get_main_function()` on the generator.
+
+Nodes are identified by an ID, so you should give a name to nodes that you want to access so you can get their ID with `find_node_by_name`.
+
+Example in the Solar System demo: [https://github.com/Zylann/solar_system_demo/blob/1ec891db22b41a842d48ca0c0b1c4c7c9157f6bc/solar_system/solar_system_setup.gd#L306](https://github.com/Zylann/solar_system_demo/blob/1ec891db22b41a842d48ca0c0b1c4c7c9157f6bc/solar_system/solar_system_setup.gd#L306)
+
Custom generator
-----------------
diff --git a/doc/source/getting_the_module.md b/doc/source/getting_the_module.md
index b1bf3a0e0..038778fff 100644
--- a/doc/source/getting_the_module.md
+++ b/doc/source/getting_the_module.md
@@ -115,7 +115,16 @@ Exporting
-------------------
!!! note
- You will need this section if you want to export your game.
+ You will need this section if you want to export your game into an executable.
+
+### Supported platforms
+
+This module supports all platforms Godot supports, on which threads are available.
+
+Some features might not always be available:
+- SIMD noise with FastNoise2 0.10 can only benefit from an x86 CPU and falls back to scalar otherwise, which is slower
+- GPU features require support for compute shaders (Forward+ renderer)
+- Threads might not work on all browsers with the web export
### Getting a template
@@ -123,9 +132,9 @@ In Godot Engine, exporting your game as an executable for a target platform requ
If you only download the Godot Editor with the module, it will allow you to develop and test your game, but if you export without any other setup, Godot will attempt to use a vanilla template, which won't have the module. Therefore, it will fail to open some scenes.
-As mentionned in earlier sections, there are currently no "official" builds of this module, but you can get template builds at the same place as [latest development versions](#development-builds). Template builds are those with `template` in their name.
+As mentionned in earlier sections, you can get pre-built templates for some platforms and configurations.
-If there is no template available for your platform, you may build it yourself. This is the same as building Godot with the module, only with different options. See the [Godot Documentation](https://docs.godotengine.org/en/latest/development/compiling/index.html) for more details, under the "building export templates" category of the platform you target.
+If there is no pre-built template available for your platform, you may build it yourself. This is the same as building Godot with the module, only with different options. See the [Godot Documentation](https://docs.godotengine.org/en/latest/development/compiling/index.html) for more details, under the "building export templates" category of the platform you target.
### Using a template
diff --git a/doc/source/module_development.md b/doc/source/module_development.md
index 357f7c697..3211c8bef 100644
--- a/doc/source/module_development.md
+++ b/doc/source/module_development.md
@@ -2,6 +2,7 @@ Module development
=====================
This page will give some info about the module's internals.
+It may be useful if you want to contribute, or write custom C++ code for your game in order to get better performance.
The source code of the module can be found on [Github](https://github.com/Zylann/godot_voxel).
@@ -73,18 +74,18 @@ constants/ | Constants and lookup tables used throughout the engine.
doc/ | Contains documentation
edition/ | High-level utilities to access and modify voxels. May depend on voxel nodes.
editor/ | Editor-specific code. May also depend on voxel nodes.
-engine/ | Contains task management. Depends on meshers, streams, storage but not directly on nodes.
+engine/ | Contains global stuff with the VoxelEngine singleton. Depends on meshers, streams, storage but not directly on nodes.
generators/ | Procedural generators. They only depend on voxel storage and math.
meshers/ | Only depends on voxel storage, math and some Godot graphics APIs.
misc/ | Various scripts and configuration files, stored here to avoid cluttering the main folder.
modifiers/ | Files related to the modifiers feature.
shaders/ | Shaders used internally by the engine, both in text form and formatted C++ form.
storage/ | Storage and memory data structures.
-streams/ | Files handling code. Only depends on filesystem and storage.
+streams/ | File storage handling code. Only depends on filesystem and storage.
terrain/ | Contains all the nodes. Depends on the rest of the module, except editor-only parts.
-tests/ | Contains tests. These run when Godot starts if enabled in the build script.
+tests/ | Contains tests. These run when Godot starts if enabled in the build script and specified by command line.
thirdparty/ | Third-party libraries, in source code form. They are compiled statically so Godot remains a single executable.
-util/ | Generic utility functions and structures. They don't depend on voxel stuff.
+util/ | Generic utility functions and data structures. They don't depend on voxel stuff.
@@ -136,7 +137,7 @@ There is one pool of threads. This pool can be given many tasks and distributes
Some tasks are scheduled in a "serial" group, which means only one of them will run at a time (although any thread can run them). This is to avoid clogging up all the threads with waiting tasks if they all lock a shared resource. This is used for I/O such as loading and saving to disk.
-Threads are managed in [VoxelEngine](api/VoxelEngine.md).
+The thread pool is in [VoxelEngine](api/VoxelEngine.md).
Note: this task system does not account for "frames". Tasks can run at any time for less or more than one frame of the main thread.
@@ -146,7 +147,7 @@ Code guidelines
### Syntax
-For the most part, use `clang-format` and follow Godot conventions.
+For the most part, use `clang-format` and follow most of Godot conventions.
- Class and struct names `PascalCase`
- Constants, enums and macros `CAPSLOCK_CASE`
@@ -177,6 +178,7 @@ For the most part, use `clang-format` and follow Godot conventions.
- Bindings go at the bottom.
- Avoid long lines. Preferred maximum line length is 120 characters. Don't fit too many operations on the same line, use locals.
- Defining types or functions in `.cpp` may be better for compilation times than in header if they are internal.
+- When a line is too long to fit a function signature, function call or list, write elements in column.
### C++ features
@@ -186,7 +188,7 @@ For the most part, use `clang-format` and follow Godot conventions.
- STL is ok if it measurably performs better than Godot alternatives.
- Initialize variables next to declaration
- Avoid using macros to define logic or constants. Prefer `static const`, `constexpr` and `inline` functions.
-- Prefer adding `const` to variables that won't change after being initialized (function arguments are spared for now as it would make signatures very long)
+- Prefer adding `const` to variables that won't change after being initialized
- Don't exploit booleanization when an explicit alternative exists. Example: use `if (a == nullptr)` instead of `if (!a)`
- If possible, avoid plain arrays like `int a[42]`. Debuggers don't catch overruns on them. Prefer using wrappers such as `FixedArray` and `Span`.
- Use `uint32_t`, `uint16_t`, `uint8_t` in case integer size matters.
@@ -216,7 +218,7 @@ In performance-critical areas which run a lot:
- Careful about what is thread-safe and what isn't. Some major areas of this module work within threads.
- Reduce mutex locking to a minimum, and avoid locking for long periods.
- Use data structures that are fit to the most frequent use over time (will often be either array, vector or hash map).
-- Consider statistics if their impact is negligible. It helps users to monitor how well the module performs even in release builds.
+- Consider tracking debug stats if their impact is negligible. It helps users to monitor how well the module performs even in release builds.
- Profile your code, in release mode. This module is Tracy-friendly, see `util/profiling.hpp`.
- Care about alignment when making data structures. For exmaple, pack fields smaller than 4 bytes so they use space better
@@ -227,7 +229,7 @@ In performance-critical areas which run a lot:
- Use `memnew` and `memdelete` instead of `new` and `delete` on types derived from Godot `Object`
- Don't leave random prints. For verbose mode you may also use `ZN_PRINT_VERBOSE()` instead of `print_verbose()`.
- Use `int` as argument for functions exposed to scripts if they don't need to exceed 2^31, even if they are never negative, so errors are clearer if the user makes a mistake
-- If possible, keep Godot usage to a minimum, to make the code more portable, and sometimes faster for future GDExtension. Some areas use custom equivalents defined in `util/`.
+- If possible, keep Godot usage to a minimum, to make the code more portable, and sometimes faster for GDExtension builds. Some areas use custom equivalents defined in `util/`.
Compiling as a module or an extension is both supported, so it involves some restrictions:
@@ -238,9 +240,9 @@ Compiling as a module or an extension is both supported, so it involves some res
The intented namespaces are `zylann::` as main, and `zylann::voxel::` for voxel-related stuff. There may be others for different parts of the module.
-Registered classes are also namespaced to prevent conflicts. These do not appear in Godot's ClassDB, so voxel-related classes are also prefixed `Voxel`. Other more generic classes are prefixed `ZN_`.
+Registered classes are also namespaced to prevent conflicts. Namespaces do not appear in Godot's ClassDB, so voxel-related classes are also prefixed `Voxel`. Other more generic classes are prefixed `ZN_`.
-If a registered class needs the same name as an internal one, it can be placed into a `::gd` sub-namespace. On the other hand, internal classes can also be suffixed `Internal`.
+If a registered class needs the same name as an internal one, it can be placed into a `::godot` sub-namespace. On the other hand, internal classes can also be suffixed `Internal`.
### Version control
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 7f842e379..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.
@@ -295,7 +295,6 @@ bool indices_to_bitarray_u16(Span indices, DynamicBitset &bitarra
false,
format("Index {} is out of supported range 0..{}", i, max_supported_value)
);
- return false;
}
#endif
@@ -348,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;
@@ -428,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 cc9fbd4c7..35c7c901d 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/graph/voxel_graph_node_dialog.cpp b/editor/graph/voxel_graph_node_dialog.cpp
index aaaa1fd45..4bb5510a7 100644
--- a/editor/graph/voxel_graph_node_dialog.cpp
+++ b/editor/graph/voxel_graph_node_dialog.cpp
@@ -21,6 +21,12 @@
#include "../../util/godot/editor_scale.h"
#include "graph_nodes_doc_data.h"
+#ifdef ZN_GODOT
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 4
+#include
+#endif
+#endif
+
namespace zylann::voxel {
namespace {
@@ -167,11 +173,13 @@ VoxelGraphNodeDialog::VoxelGraphNodeDialog() {
// TODO Replace QuickOpen with listing of project functions directly in the dialog
// TODO GDX: EditorQuickOpen is not exposed to extensions
#ifdef ZN_GODOT
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
_function_quick_open_dialog = memnew(EditorQuickOpen);
_function_quick_open_dialog->connect(
"quick_open", callable_mp(this, &VoxelGraphNodeDialog::_on_function_quick_open_dialog_quick_open)
);
add_child(_function_quick_open_dialog);
+#endif
#endif
// In this editor, categories come from the documentation and may be unrelated to internal node categories.
@@ -373,8 +381,17 @@ void VoxelGraphNodeDialog::_on_tree_item_activated() {
} else if (id == ID_FUNCTION_QUICK_OPEN) {
#ifdef ZN_GODOT
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
// Quick open function nodes
_function_quick_open_dialog->popup_dialog(pg::VoxelGraphFunction::get_class_static());
+#else
+ Vector base_types;
+ base_types.append(pg::VoxelGraphFunction::get_class_static());
+ EditorQuickOpenDialog *quick_open_dialog = EditorNode::get_singleton()->get_quick_open_dialog();
+ quick_open_dialog->popup_dialog(
+ base_types, callable_mp(this, &VoxelGraphNodeDialog::on_function_quick_open_dialog_item_selected)
+ );
+#endif
#endif
} else {
@@ -422,9 +439,18 @@ void VoxelGraphNodeDialog::_on_function_file_dialog_file_selected(String fpath)
hide();
}
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
void VoxelGraphNodeDialog::_on_function_quick_open_dialog_quick_open() {
#ifdef ZN_GODOT
String fpath = _function_quick_open_dialog->get_selected();
+ on_function_quick_open_dialog_item_selected(fpath);
+#endif
+}
+
+#endif
+
+void VoxelGraphNodeDialog::on_function_quick_open_dialog_item_selected(String fpath) {
+#ifdef ZN_GODOT
if (fpath.is_empty()) {
return;
}
diff --git a/editor/graph/voxel_graph_node_dialog.h b/editor/graph/voxel_graph_node_dialog.h
index 8a7f6dd22..acbcbd4cb 100644
--- a/editor/graph/voxel_graph_node_dialog.h
+++ b/editor/graph/voxel_graph_node_dialog.h
@@ -4,6 +4,7 @@
#include "../../generators/graph/voxel_graph_function.h"
#include "../../util/containers/std_vector.h"
#include "../../util/godot/classes/confirmation_dialog.h"
+#include "../../util/godot/core/version.h"
#include "../../util/godot/macros.h"
ZN_GODOT_FORWARD_DECLARE(class Tree);
@@ -11,8 +12,10 @@ ZN_GODOT_FORWARD_DECLARE(class LineEdit);
ZN_GODOT_FORWARD_DECLARE(class EditorFileDialog)
ZN_GODOT_FORWARD_DECLARE(class RichTextLabel)
#ifdef ZN_GODOT
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
ZN_GODOT_FORWARD_DECLARE(class EditorQuickOpen)
#endif
+#endif
namespace zylann::voxel {
@@ -37,7 +40,10 @@ class VoxelGraphNodeDialog : public ConfirmationDialog {
void _on_tree_item_selected();
void _on_tree_nothing_selected();
void _on_function_file_dialog_file_selected(String fpath);
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
void _on_function_quick_open_dialog_quick_open();
+#endif
+ void on_function_quick_open_dialog_item_selected(String fpath);
void _on_description_label_meta_clicked(Variant meta);
void _notification(int p_what);
@@ -65,9 +71,11 @@ class VoxelGraphNodeDialog : public ConfirmationDialog {
RichTextLabel *_description_label = nullptr;
EditorFileDialog *_function_file_dialog = nullptr;
#ifdef ZN_GODOT
+#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3
// TODO GDX: EditorQuickOpen is not exposed!
EditorQuickOpen *_function_quick_open_dialog = nullptr;
#endif
+#endif
};
} // namespace zylann::voxel
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/editor/vox/vox_scene_importer.cpp b/editor/vox/vox_scene_importer.cpp
index e8fc7b69f..f6eb17a98 100644
--- a/editor/vox/vox_scene_importer.cpp
+++ b/editor/vox/vox_scene_importer.cpp
@@ -419,6 +419,10 @@ Error VoxelVoxSceneImporter::_zn_import(
for (unsigned int model_index = 0; model_index < meshes.size(); ++model_index) {
ZN_PROFILE_SCOPE();
Ref mesh = meshes[model_index].mesh;
+ // Some models might be empty, as seen earlier
+ if (mesh.is_null()) {
+ continue;
+ }
String res_save_path = String("{0}.model{1}.mesh").format(varray(p_save_path, model_index));
// `FLAG_CHANGE_PATH` did not do what I thought it did.
mesh->set_path(res_save_path);
@@ -445,4 +449,14 @@ Error VoxelVoxSceneImporter::_zn_import(
return OK;
}
+bool VoxelVoxSceneImporter::_zn_can_import_threaded() const {
+ // By default it is `true`, but `ResourceSaver::save` ended up deadlocking the editor when saving meshes.
+ // I don't know if this is a known issue or something importers should do when saving meshes.
+
+ // TODO Make a bug report? Might take a while to create an MRP :(
+ // this happens in a crowded project and might be timing-dependent...
+
+ return false;
+}
+
} // namespace zylann::voxel::magica
diff --git a/editor/vox/vox_scene_importer.h b/editor/vox/vox_scene_importer.h
index 55c2a2e09..16edde461 100644
--- a/editor/vox/vox_scene_importer.h
+++ b/editor/vox/vox_scene_importer.h
@@ -40,6 +40,8 @@ class VoxelVoxSceneImporter : public zylann::godot::ZN_EditorImportPlugin {
zylann::godot::StringListWrapper p_out_gen_files
) const override;
+ bool _zn_can_import_threaded() const override;
+
private:
// When compiling with GodotCpp, `_bind_methods` is not optional.
static void _bind_methods() {}
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.cpp b/engine/gpu/compute_shader.cpp
index d8843ed23..033fdc04f 100644
--- a/engine/gpu/compute_shader.cpp
+++ b/engine/gpu/compute_shader.cpp
@@ -1,8 +1,10 @@
#include "compute_shader.h"
#include "../../util/godot/classes/rd_shader_source.h"
+#include "../../util/godot/classes/rendering_server.h"
#include "../../util/godot/core/array.h" // for `varray` in GDExtension builds
#include "../../util/godot/core/print_string.h"
#include "../../util/profiling.h"
+#include "../../util/string/format.h"
#include "../voxel_engine.h"
namespace zylann::voxel {
@@ -61,7 +63,12 @@ void ComputeShader::load_from_glsl(String source_text, String name) {
shader_source->set_language(RenderingDevice::SHADER_LANGUAGE_GLSL);
shader_source->set_stage_source(RenderingDevice::SHADER_STAGE_COMPUTE, source_text);
- ZN_ASSERT_RETURN(VoxelEngine::get_singleton().has_rendering_device());
+ ZN_ASSERT_RETURN_MSG(
+ VoxelEngine::get_singleton().has_rendering_device(),
+ format("Can't create compute shader \"{}\". Maybe the selected renderer doesn't support it? ({})",
+ name,
+ zylann::godot::get_current_rendering_method())
+ );
RenderingDevice &rd = VoxelEngine::get_singleton().get_rendering_device();
// MutexLock mlock(VoxelEngine::get_singleton().get_rendering_device_mutex());
diff --git a/engine/gpu/compute_shader_resource.cpp b/engine/gpu/compute_shader_resource.cpp
index 11d38511f..9d0d6233f 100644
--- a/engine/gpu/compute_shader_resource.cpp
+++ b/engine/gpu/compute_shader_resource.cpp
@@ -133,12 +133,15 @@ void ComputeShaderResource::create_texture_2d(const Curve &curve) {
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])));
@@ -153,7 +156,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) {
@@ -171,7 +174,7 @@ void ComputeShaderResource::create_texture_3d_zxy(Span fdata_zxy, V
ZN_PROFILE_SCOPE();
ZN_ASSERT(Vector3iUtil::is_valid_size(size));
- ZN_ASSERT(Vector3iUtil::get_volume(size) == int64_t(fdata_zxy.size()));
+ ZN_ASSERT(Vector3iUtil::get_volume_u64(size) == fdata_zxy.size());
clear();
@@ -195,7 +198,7 @@ void ComputeShaderResource::create_texture_3d_zxy(Span fdata_zxy, V
TypedArray data_array;
{
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);
data_array.append(pba);
diff --git a/generators/generate_block_gpu_task.cpp b/generators/generate_block_gpu_task.cpp
index 81f590079..64f94bc4b 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
@@ -434,7 +434,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/image_range_grid.cpp b/generators/graph/image_range_grid.cpp
index e1a47b23e..fce53c082 100644
--- a/generators/graph/image_range_grid.cpp
+++ b/generators/graph/image_range_grid.cpp
@@ -23,6 +23,10 @@ void ImageRangeGrid::generate(const Image &im) {
clear();
+ if (im.is_empty()) {
+ return;
+ }
+
const int lod_base = 4; // Start at 16
// Compute first lod
diff --git a/generators/graph/node_type_db.cpp b/generators/graph/node_type_db.cpp
index f47426abb..3734fd86f 100644
--- a/generators/graph/node_type_db.cpp
+++ b/generators/graph/node_type_db.cpp
@@ -244,7 +244,7 @@ VoxelGraphFunction::Port make_port_from_io_node(const ProgramGraph::Node &node,
}
bool is_node_matching_port(const ProgramGraph::Node &node, const VoxelGraphFunction::Port &port) {
- if (node.type_id != port.type) {
+ if (node.type_id != static_cast(port.type)) {
return false;
}
diff --git a/generators/graph/nodes/curve.h b/generators/graph/nodes/curve.h
index 2a97de326..476c0eebd 100644
--- a/generators/graph/nodes/curve.h
+++ b/generators/graph/nodes/curve.h
@@ -76,13 +76,22 @@ void register_curve_node(Span types) {
ComputeShaderResource res;
res.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/nodes/image.h b/generators/graph/nodes/image.h
index c55571c6a..150641a24 100644
--- a/generators/graph/nodes/image.h
+++ b/generators/graph/nodes/image.h
@@ -148,6 +148,10 @@ void register_image_nodes(Span types) {
.format(varray(Image::get_class_static())));
return;
}
+ if (image->is_empty()) {
+ ctx.make_error(String(ZN_TTR("{0} is empty").format(varray(Image::get_class_static()))));
+ return;
+ }
ImageRangeGrid *im_range = ZN_NEW(ImageRangeGrid);
im_range->generate(**image);
Params p;
@@ -167,6 +171,12 @@ void register_image_nodes(Span types) {
// Cache image size to reduce API calls in GDExtension
const int w = im.get_width();
const int h = im.get_height();
+#ifdef DEBUG_ENABLED
+ if (w == 0 || h == 0) {
+ ZN_PRINT_ERROR_ONCE("Image is empty");
+ return;
+ }
+#endif
// TODO Optimized path for most used formats, `get_pixel` is kinda slow
if (p.filter == FILTER_NEAREST) {
for (uint32_t i = 0; i < out.size; ++i) {
diff --git a/generators/graph/nodes/math_vectors.h b/generators/graph/nodes/math_vectors.h
index 070844434..f0a3b5000 100644
--- a/generators/graph/nodes/math_vectors.h
+++ b/generators/graph/nodes/math_vectors.h
@@ -36,8 +36,14 @@ void register_math_vector_nodes(Span types) {
ctx.set_output(0, r);
};
t.shader_gen_func = [](ShaderGenContext &ctx) {
- ctx.add_format("{} = distance(vec2({}, {}), vec2({}, {}));\n", ctx.get_output_name(0),
- ctx.get_input_name(0), ctx.get_input_name(1), ctx.get_input_name(2), ctx.get_input_name(3));
+ ctx.add_format(
+ "{} = distance(vec2({}, {}), vec2({}, {}));\n",
+ ctx.get_output_name(0),
+ ctx.get_input_name(0),
+ ctx.get_input_name(1),
+ ctx.get_input_name(2),
+ ctx.get_input_name(3)
+ );
};
}
{
@@ -60,8 +66,10 @@ void register_math_vector_nodes(Span types) {
const Runtime::Buffer &z1 = ctx.get_input(5);
Runtime::Buffer &out = ctx.get_output(0);
for (uint32_t i = 0; i < out.size; ++i) {
- out.data[i] = Math::sqrt(squared(x1.data[i] - x0.data[i]) + squared(y1.data[i] - y0.data[i]) +
- squared(z1.data[i] - z0.data[i]));
+ out.data[i] = Math::sqrt(
+ squared(x1.data[i] - x0.data[i]) + squared(y1.data[i] - y0.data[i]) +
+ squared(z1.data[i] - z0.data[i])
+ );
}
};
t.range_analysis_func = [](Runtime::RangeAnalysisContext &ctx) {
@@ -78,9 +86,16 @@ void register_math_vector_nodes(Span types) {
ctx.set_output(0, r);
};
t.shader_gen_func = [](ShaderGenContext &ctx) {
- ctx.add_format("{} = distance(vec3({}, {}, {}), vec2({}, {}, {}));\n", ctx.get_output_name(0),
- ctx.get_input_name(0), ctx.get_input_name(1), ctx.get_input_name(2), ctx.get_input_name(3),
- ctx.get_input_name(4), ctx.get_input_name(5));
+ ctx.add_format(
+ "{} = distance(vec3({}, {}, {}), vec3({}, {}, {}));\n",
+ ctx.get_output_name(0),
+ ctx.get_input_name(0),
+ ctx.get_input_name(1),
+ ctx.get_input_name(2),
+ ctx.get_input_name(3),
+ ctx.get_input_name(4),
+ ctx.get_input_name(5)
+ );
};
}
{
@@ -132,17 +147,26 @@ void register_math_vector_nodes(Span types) {
};
t.shader_gen_func = [](ShaderGenContext &ctx) {
- ctx.require_lib_code("vg_normalize",
+ ctx.require_lib_code(
+ "vg_normalize",
"void vg_normalize(vec3 v, out float x, out float y, out float z, out float mag) {\n"
" mag = length(v);\n"
" v /= mag;\n"
" x = v.x;\n"
" y = v.y;\n"
" z = v.z;\n"
- "}\n");
- ctx.add_format("vg_normalize(vec3({}, {}, {}), {}, {}, {}, {});\n", ctx.get_input_name(0),
- ctx.get_input_name(1), ctx.get_input_name(2), ctx.get_output_name(0), ctx.get_output_name(1),
- ctx.get_output_name(2), ctx.get_output_name(3));
+ "}\n"
+ );
+ ctx.add_format(
+ "vg_normalize(vec3({}, {}, {}), {}, {}, {}, {});\n",
+ ctx.get_input_name(0),
+ ctx.get_input_name(1),
+ ctx.get_input_name(2),
+ ctx.get_output_name(0),
+ ctx.get_output_name(1),
+ ctx.get_output_name(2),
+ ctx.get_output_name(3)
+ );
};
}
}
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 ccf3bae3f..1fc954a0f 100644
--- a/generators/graph/voxel_generator_graph.cpp
+++ b/generators/graph/voxel_generator_graph.cpp
@@ -523,7 +523,7 @@ void fill_zx_integer_slice(
} // namespace
-VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::VoxelQueryData input) {
std::shared_ptr runtime_ptr;
{
RWLockRead rlock(_runtime_lock);
@@ -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);
@@ -830,7 +830,7 @@ VoxelGenerator::Result VoxelGeneratorGraph::generate_block(VoxelGenerator::Voxel
return result;
}
-bool VoxelGeneratorGraph::generate_broad_block(VoxelGenerator::VoxelQueryData &input) {
+bool VoxelGeneratorGraph::generate_broad_block(VoxelGenerator::VoxelQueryData input) {
// This is a reduced version of whan `generate_block` does already, so it can be used before scheduling GPU work.
// If range analysis and SDF clipping finds that we don't need to generate the full block, we can get away with the
// broad result. If any channel cannot be determined this way, we have to perform full generation.
@@ -1665,7 +1665,7 @@ VoxelSingleValue VoxelGeneratorGraph::generate_single(Vector3i position, unsigne
RWLockRead rlock(_runtime_lock);
runtime_ptr = _runtime;
}
- ERR_FAIL_COND_V(runtime_ptr == nullptr, v);
+ ERR_FAIL_COND_V_MSG(runtime_ptr == nullptr, v, "no compiled graph available");
if (runtime_ptr->sdf_output_buffer_index == -1) {
return v;
}
@@ -1688,7 +1688,7 @@ VoxelSingleValue VoxelGeneratorGraph::generate_single(Vector3i position, unsigne
RWLockRead rlock(_runtime_lock);
runtime_ptr = _runtime;
}
- ERR_FAIL_COND_V(runtime_ptr == nullptr, v);
+ ERR_FAIL_COND_V_MSG(runtime_ptr == nullptr, v, "no compiled graph available");
if (runtime_ptr->single_texture_output_buffer_index == -1) {
return v;
}
@@ -1716,10 +1716,24 @@ VoxelSingleValue VoxelGeneratorGraph::generate_single(Vector3i position, unsigne
}
}
+math::Interval get_range(const Span values) {
+ float minv = values[0];
+ float maxv = minv;
+ for (const float v : values) {
+ minv = math::min(v, minv);
+ maxv = math::max(v, maxv);
+ }
+ return math::Interval(minv, maxv);
+}
+
// Note, this wrapper may not be used for main generation tasks.
// It is mostly used as a debug tool.
math::Interval VoxelGeneratorGraph::debug_analyze_range(Vector3i min_pos, Vector3i max_pos, bool optimize_execution_map)
const {
+ ZN_ASSERT_RETURN_V(max_pos.x >= min_pos.x, math::Interval());
+ ZN_ASSERT_RETURN_V(max_pos.y >= min_pos.y, math::Interval());
+ ZN_ASSERT_RETURN_V(max_pos.z >= min_pos.z, math::Interval());
+
std::shared_ptr runtime_ptr;
{
RWLockRead rlock(_runtime_lock);
@@ -1743,6 +1757,105 @@ math::Interval VoxelGeneratorGraph::debug_analyze_range(Vector3i min_pos, Vector
if (optimize_execution_map) {
runtime.generate_optimized_execution_map(cache.state, cache.optimized_execution_map, true);
}
+
+#if 0
+ const bool range_validation = true;
+ if (range_validation) {
+ // Actually calculate every value and check if range analysis bounds them.
+ // If it does not, then it's a bug.
+ // This is not 100% accurate. We would have to calculate every value at every point in space in the area which
+ // is not possible, we can only sample a finite number within a grid.
+
+ // TODO Make sure the graph is compiled in debug mode, otherwise buffer lookups won't match
+ // TODO Even with debug on, it appears buffers don't really match???
+
+ const Vector3i cube_size = max_pos - min_pos;
+ if (cube_size.x > 64 || cube_size.y > 64 || cube_size.z > 64) {
+ ZN_PRINT_ERROR("Area too big for range validation");
+ } else {
+ const int64_t cube_volume = Vector3iUtil::get_volume(cube_size);
+ ZN_ASSERT_RETURN_V(cube_volume >= 0, math::Interval());
+
+ StdVector src_x;
+ StdVector src_y;
+ StdVector src_z;
+ StdVector src_sdf;
+ src_x.resize(cube_volume);
+ src_y.resize(cube_volume);
+ src_z.resize(cube_volume);
+ src_sdf.resize(cube_volume);
+ Span sx = to_span(src_x);
+ Span sy = to_span(src_y);
+ Span sz = to_span(src_z);
+ Span ssdf = to_span(src_sdf);
+
+ {
+ unsigned int i = 0;
+ for (int z = min_pos.z; z < max_pos.z; ++z) {
+ for (int y = min_pos.y; y < max_pos.y; ++y) {
+ for (int x = min_pos.x; x < max_pos.x; ++x) {
+ src_x[i] = x;
+ src_y[i] = y;
+ src_z[i] = z;
+ ++i;
+ }
+ }
+ }
+ }
+
+ QueryInputs inputs(*runtime_ptr, sx, sy, sz, ssdf);
+
+ runtime.prepare_state(cache.state, sx.size(), false);
+ runtime.generate_set(cache.state, inputs.get(), false, nullptr);
+
+ const pg::Runtime::State &state = get_last_state_from_current_thread();
+
+ _main_function->get_graph().for_each_node_const([this, &state](const ProgramGraph::Node &node) {
+ for (uint32_t output_index = 0; output_index < node.outputs.size(); ++output_index) {
+ // const ProgramGraph::Port &output_port = node.outputs[output_index];
+ const ProgramGraph::PortLocation loc{ node.id, output_index };
+
+ uint32_t address;
+ if (!try_get_output_port_address(loc, address)) {
+ continue;
+ }
+
+ const math::Interval analytic_range = state.get_range(address);
+
+ const pg::Runtime::Buffer &buffer = state.get_buffer(address);
+
+ if (buffer.data == nullptr) {
+ if (buffer.is_binding) {
+ // Not supported
+ continue;
+ }
+ ZN_PRINT_ERROR("Didn't expect nullptr in buffer data");
+ continue;
+ }
+
+ const Span values(buffer.data, buffer.size);
+ const math::Interval empiric_range = get_range(values);
+
+ if (!analytic_range.contains(empiric_range)) {
+ const String node_name = node.name;
+ const pg::NodeType &node_type = pg::NodeTypeDB::get_singleton().get_type(node.type_id);
+ ZN_PRINT_WARNING(
+ format("Empiric range not included in analytic range. A: {}, E: {}; {} "
+ "output {} instance {} \"{}\"",
+ analytic_range,
+ empiric_range,
+ node_type.name,
+ output_index,
+ node.id,
+ node_name)
+ );
+ }
+ }
+ });
+ }
+ }
+#endif
+
// TODO Change return value to allow checking other outputs
if (runtime_ptr->sdf_output_buffer_index != -1) {
return cache.state.get_range(runtime_ptr->sdf_output_buffer_index);
diff --git a/generators/graph/voxel_generator_graph.h b/generators/graph/voxel_generator_graph.h
index e092093c5..1da1d3b08 100644
--- a/generators/graph/voxel_generator_graph.h
+++ b/generators/graph/voxel_generator_graph.h
@@ -62,8 +62,8 @@ class VoxelGeneratorGraph : public VoxelGenerator {
int get_used_channels_mask() const override;
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
- bool generate_broad_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
+ bool generate_broad_block(VoxelGenerator::VoxelQueryData input) override;
// float generate_single(const Vector3i &position);
bool supports_single_generation() const override {
return true;
diff --git a/generators/graph/voxel_graph_compiler.cpp b/generators/graph/voxel_graph_compiler.cpp
index abc641f08..5da607eb7 100644
--- a/generators/graph/voxel_graph_compiler.cpp
+++ b/generators/graph/voxel_graph_compiler.cpp
@@ -255,6 +255,9 @@ uint32_t expand_node(
node_type_id = VoxelGraphFunction::NODE_POW;
break;
default:
+ // Fix uninitialized variable warning on Clang, even though it is not supposed to carry on after the
+ // switch
+ node_type_id = VoxelGraphFunction::NODE_CONSTANT;
ZN_CRASH();
break;
}
@@ -1340,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 98babdbb3..2a787230e 100644
--- a/generators/graph/voxel_graph_shader_generator.cpp
+++ b/generators/graph/voxel_graph_shader_generator.cpp
@@ -32,9 +32,14 @@ StdString ShaderGenContext::add_uniform(ComputeShaderResource &&res) {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-CompilationResult generate_shader(const ProgramGraph &p_graph, Span input_defs,
- FwdMutableStdString source_code, StdVector &shader_params, StdVector &outputs,
- Span restricted_outputs) {
+CompilationResult generate_shader(
+ const ProgramGraph &p_graph,
+ Span input_defs,
+ FwdMutableStdString source_code,
+ StdVector &shader_params,
+ StdVector &outputs,
+ Span restricted_outputs
+) {
ZN_PROFILE_SCOPE();
const NodeTypeDB &type_db = NodeTypeDB::get_singleton();
@@ -75,7 +80,7 @@ CompilationResult generate_shader(const ProgramGraph &p_graph, Span 0) {
// Not supported
return { false };
@@ -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/multipass/voxel_generator_multipass_cb.h b/generators/multipass/voxel_generator_multipass_cb.h
index cd8361c60..0d9d3bcd4 100644
--- a/generators/multipass/voxel_generator_multipass_cb.h
+++ b/generators/multipass/voxel_generator_multipass_cb.h
@@ -54,7 +54,7 @@ class VoxelGeneratorMultipassCB : public VoxelGenerator {
return false;
}
- Result generate_block(VoxelQueryData &input) override;
+ Result generate_block(VoxelQueryData input) override;
int get_used_channels_mask() const override;
IThreadedTask *create_block_task(const VoxelGenerator::BlockTaskParams ¶ms) const override;
diff --git a/generators/simple/voxel_generator_flat.cpp b/generators/simple/voxel_generator_flat.cpp
index d9cc55098..dc72e6724 100644
--- a/generators/simple/voxel_generator_flat.cpp
+++ b/generators/simple/voxel_generator_flat.cpp
@@ -51,7 +51,7 @@ float VoxelGeneratorFlat::get_height() const {
return _parameters.height;
}
-VoxelGenerator::Result VoxelGeneratorFlat::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorFlat::generate_block(VoxelGenerator::VoxelQueryData input) {
Result result;
Parameters params;
diff --git a/generators/simple/voxel_generator_flat.h b/generators/simple/voxel_generator_flat.h
index 39dfc5523..b995a4de6 100644
--- a/generators/simple/voxel_generator_flat.h
+++ b/generators/simple/voxel_generator_flat.h
@@ -20,7 +20,7 @@ class VoxelGeneratorFlat : public VoxelGenerator {
VoxelBuffer::ChannelId get_channel() const;
int get_used_channels_mask() const override;
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
void set_voxel_type(int t);
int get_voxel_type() const;
diff --git a/generators/simple/voxel_generator_image.cpp b/generators/simple/voxel_generator_image.cpp
index 5563a769c..8004de71b 100644
--- a/generators/simple/voxel_generator_image.cpp
+++ b/generators/simple/voxel_generator_image.cpp
@@ -56,7 +56,7 @@ bool VoxelGeneratorImage::is_blur_enabled() const {
return _parameters.blur_enabled;
}
-VoxelGenerator::Result VoxelGeneratorImage::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorImage::generate_block(VoxelGenerator::VoxelQueryData input) {
VoxelBuffer &out_buffer = input.voxel_buffer;
Parameters params;
diff --git a/generators/simple/voxel_generator_image.h b/generators/simple/voxel_generator_image.h
index 686c40a1f..540494cb0 100644
--- a/generators/simple/voxel_generator_image.h
+++ b/generators/simple/voxel_generator_image.h
@@ -23,7 +23,7 @@ class VoxelGeneratorImage : public VoxelGeneratorHeightmap {
void set_blur_enabled(bool enable);
bool is_blur_enabled() const;
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
private:
static void _bind_methods();
diff --git a/generators/simple/voxel_generator_noise.cpp b/generators/simple/voxel_generator_noise.cpp
index 81714c2b8..31c9a8cde 100644
--- a/generators/simple/voxel_generator_noise.cpp
+++ b/generators/simple/voxel_generator_noise.cpp
@@ -125,7 +125,7 @@ static inline float get_shaped_noise(OpenSimplexNoise &noise, float x, float y,
}
*/
-VoxelGenerator::Result VoxelGeneratorNoise::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorNoise::generate_block(VoxelGenerator::VoxelQueryData input) {
Parameters params;
{
RWLockRead rlock(_parameters_lock);
diff --git a/generators/simple/voxel_generator_noise.h b/generators/simple/voxel_generator_noise.h
index 64492310a..2c9417fe4 100644
--- a/generators/simple/voxel_generator_noise.h
+++ b/generators/simple/voxel_generator_noise.h
@@ -32,7 +32,7 @@ class VoxelGeneratorNoise : public VoxelGenerator {
void set_height_range(real_t hrange);
real_t get_height_range() const;
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
private:
void _on_noise_changed();
@@ -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/generators/simple/voxel_generator_noise_2d.cpp b/generators/simple/voxel_generator_noise_2d.cpp
index 8800cab9e..be845ae59 100644
--- a/generators/simple/voxel_generator_noise_2d.cpp
+++ b/generators/simple/voxel_generator_noise_2d.cpp
@@ -63,7 +63,7 @@ Ref VoxelGeneratorNoise2D::get_curve() const {
return _curve;
}
-VoxelGenerator::Result VoxelGeneratorNoise2D::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorNoise2D::generate_block(VoxelGenerator::VoxelQueryData input) {
Parameters params;
{
RWLockRead rlock(_parameters_lock);
diff --git a/generators/simple/voxel_generator_noise_2d.h b/generators/simple/voxel_generator_noise_2d.h
index cc8b8daea..19e28b21b 100644
--- a/generators/simple/voxel_generator_noise_2d.h
+++ b/generators/simple/voxel_generator_noise_2d.h
@@ -25,14 +25,21 @@ class VoxelGeneratorNoise2D : public VoxelGeneratorHeightmap {
void set_curve(Ref curve);
Ref get_curve() const;
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
bool supports_series_generation() const override {
return true;
}
- void generate_series(Span positions_x, Span positions_y, Span positions_z,
- unsigned int channel, Span out_values, Vector3f min_pos, Vector3f max_pos) override;
+ void generate_series(
+ Span positions_x,
+ Span positions_y,
+ Span positions_z,
+ unsigned int channel,
+ Span out_values,
+ Vector3f min_pos,
+ Vector3f max_pos
+ ) override;
private:
void _on_noise_changed();
diff --git a/generators/simple/voxel_generator_waves.cpp b/generators/simple/voxel_generator_waves.cpp
index 2b0c84fad..634d77c89 100644
--- a/generators/simple/voxel_generator_waves.cpp
+++ b/generators/simple/voxel_generator_waves.cpp
@@ -12,7 +12,7 @@ VoxelGeneratorWaves::VoxelGeneratorWaves() {
VoxelGeneratorWaves::~VoxelGeneratorWaves() {}
-VoxelGenerator::Result VoxelGeneratorWaves::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorWaves::generate_block(VoxelGenerator::VoxelQueryData input) {
Parameters params;
{
RWLockRead rlock(_parameters_lock);
diff --git a/generators/simple/voxel_generator_waves.h b/generators/simple/voxel_generator_waves.h
index be7cbf58b..63c255675 100644
--- a/generators/simple/voxel_generator_waves.h
+++ b/generators/simple/voxel_generator_waves.h
@@ -14,7 +14,7 @@ class VoxelGeneratorWaves : public VoxelGeneratorHeightmap {
VoxelGeneratorWaves();
~VoxelGeneratorWaves();
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
Vector2 get_pattern_size() const;
void set_pattern_size(Vector2 size);
diff --git a/generators/voxel_generator.cpp b/generators/voxel_generator.cpp
index cfd14a923..1b3e0c8b8 100644
--- a/generators/voxel_generator.cpp
+++ b/generators/voxel_generator.cpp
@@ -12,7 +12,7 @@ namespace zylann::voxel {
VoxelGenerator::VoxelGenerator() {}
-VoxelGenerator::Result VoxelGenerator::generate_block(VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGenerator::generate_block(VoxelQueryData input) {
return Result();
}
@@ -294,7 +294,7 @@ void VoxelGenerator::invalidate_shaders() {
}
}
-bool VoxelGenerator::generate_broad_block(VoxelQueryData &input) {
+bool VoxelGenerator::generate_broad_block(VoxelQueryData input) {
// By default, generators don't support this separately and just do it inside `generate_block`.
// However if a generator supports GPU, it is recommended to implement it.
return false;
diff --git a/generators/voxel_generator.h b/generators/voxel_generator.h
index f89b05c84..7d51b30d7 100644
--- a/generators/voxel_generator.h
+++ b/generators/voxel_generator.h
@@ -59,7 +59,7 @@ class VoxelGenerator : public Resource {
uint32_t lod;
};
- virtual Result generate_block(VoxelQueryData &input);
+ virtual Result generate_block(VoxelQueryData input);
struct BlockTaskParams {
Vector3i block_position;
@@ -159,7 +159,7 @@ class VoxelGenerator : public Resource {
// If it returns false, no block is returned and full generation should be used.
// Usually, `generate_block` can do this anyways internally, but in some cases like GPU generation it may be used
// to avoid sending work to the graphics card.
- virtual bool generate_broad_block(VoxelQueryData &input);
+ virtual bool generate_broad_block(VoxelQueryData input);
// Caching API
//
diff --git a/generators/voxel_generator_script.cpp b/generators/voxel_generator_script.cpp
index 4809bb2d8..a21208de2 100644
--- a/generators/voxel_generator_script.cpp
+++ b/generators/voxel_generator_script.cpp
@@ -7,7 +7,7 @@ namespace zylann::voxel {
VoxelGeneratorScript::VoxelGeneratorScript() {}
-VoxelGenerator::Result VoxelGeneratorScript::generate_block(VoxelGenerator::VoxelQueryData &input) {
+VoxelGenerator::Result VoxelGeneratorScript::generate_block(VoxelGenerator::VoxelQueryData input) {
Result result;
// Create a temporary wrapper so Godot can pass it to scripts
diff --git a/generators/voxel_generator_script.h b/generators/voxel_generator_script.h
index 26115bb8b..dfb6f94cb 100644
--- a/generators/voxel_generator_script.h
+++ b/generators/voxel_generator_script.h
@@ -18,7 +18,7 @@ class VoxelGeneratorScript : public VoxelGenerator {
public:
VoxelGeneratorScript();
- Result generate_block(VoxelGenerator::VoxelQueryData &input) override;
+ Result generate_block(VoxelGenerator::VoxelQueryData input) override;
int get_used_channels_mask() const override;
protected:
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 125d38f02..482a3d265 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(VoxelMesherOutput &output, const VoxelMesherInput
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(VoxelMesherOutput &output, const VoxelMesherInput
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 037b3ca92..ba3e45738 100644
--- a/meshers/transvoxel/transvoxel.cpp
+++ b/meshers/transvoxel/transvoxel.cpp
@@ -5,6 +5,7 @@
#include "../../util/math/conv.h"
#include "../../util/math/funcs.h"
#include "../../util/profiling.h"
+#include "transvoxel_materials_mixel4.h"
#include "transvoxel_tables.cpp"
// #define VOXEL_TRANSVOXEL_REUSE_VERTEX_ON_COINCIDENT_CASES
@@ -155,162 +156,6 @@ inline Vector3f get_corner_gradient(unsigned int data_index, Span s
return Vector3f(nx - px, ny - py, nz - pz);
}
-inline uint32_t pack_bytes(const FixedArray &a) {
- return (a[0] | (a[1] << 8) | (a[2] << 16) | (a[3] << 24));
-}
-
-void add_texture_data(
- StdVector &uv,
- unsigned int packed_indices,
- FixedArray weights
-) {
- struct IntUV {
- uint32_t x;
- uint32_t y;
- };
- static_assert(sizeof(IntUV) == sizeof(Vector2f), "Expected same binary size");
- uv.push_back(Vector2f());
- IntUV &iuv = *(reinterpret_cast(&uv.back()));
- // print_line(String("{0}, {1}, {2}, {3}").format(varray(weights[0], weights[1], weights[2], weights[3])));
- iuv.x = packed_indices;
- iuv.y = pack_bytes(weights);
-}
-
-template
-struct CellTextureDatas {
- uint32_t packed_indices = 0;
- FixedArray indices;
- FixedArray, NVoxels> weights;
-};
-
-template
-CellTextureDatas select_textures_4_per_voxel(
- const FixedArray &voxel_indices,
- const Span indices_data,
- const WeightSampler_T &weights_sampler,
- const unsigned int case_code
-) {
- // TODO Optimization: this function takes almost half of the time when polygonizing non-empty cells.
- // I wonder how it can be optimized further?
-
- struct IndexAndWeight {
- unsigned int index;
- unsigned int weight;
- };
-
- FixedArray, NVoxels> cell_texture_weights_temp;
- FixedArray indexed_weight_sums;
-
- // Find 4 most-used indices in voxels
- for (unsigned int i = 0; i < indexed_weight_sums.size(); ++i) {
- indexed_weight_sums[i] = IndexAndWeight{ i, 0 };
- }
- for (unsigned int ci = 0; ci < voxel_indices.size(); ++ci) {
- // ZN_PROFILE_SCOPE();
-
- FixedArray &weights_temp = cell_texture_weights_temp[ci];
- fill(weights_temp, uint8_t(0));
-
- // Air voxels should not contribute
- if ((case_code & (1 << ci)) != 0) {
- continue;
- }
-
- const unsigned int data_index = voxel_indices[ci];
-
- const FixedArray indices = decode_indices_from_packed_u16(indices_data[data_index]);
- const FixedArray weights = weights_sampler.get_weights(data_index);
-
- for (unsigned int j = 0; j < indices.size(); ++j) {
- const unsigned int ti = indices[j];
- indexed_weight_sums[ti].weight += weights[j];
- weights_temp[ti] = weights[j];
- }
- }
- struct IndexAndWeightComparator {
- inline bool operator()(const IndexAndWeight &a, const IndexAndWeight &b) const {
- return a.weight > b.weight;
- }
- };
- SortArray sorter;
- sorter.sort(indexed_weight_sums.data(), indexed_weight_sums.size());
-
- CellTextureDatas cell_textures;
-
- // Assign indices
- for (unsigned int i = 0; i < cell_textures.indices.size(); ++i) {
- cell_textures.indices[i] = indexed_weight_sums[i].index;
- }
-
- // Sort indices to avoid cases that are ambiguous for blending, like 1,2,3,4 and 2,1,3,4
- // TODO maybe we could require this sorting to be done up front?
- // Or maybe could be done after meshing so we do it less times?
- math::sort(cell_textures.indices[0], cell_textures.indices[1], cell_textures.indices[2], cell_textures.indices[3]);
-
- cell_textures.packed_indices = pack_bytes(cell_textures.indices);
-
- // Remap weights to follow the indices we selected
- for (unsigned int ci = 0; ci < cell_texture_weights_temp.size(); ++ci) {
- // ZN_PROFILE_SCOPE();
-
- FixedArray &dst_weights = cell_textures.weights[ci];
-
- // Skip air voxels
- if ((case_code & (1 << ci)) != 0) {
- fill(dst_weights, uint8_t(0));
- continue;
- }
-
- const FixedArray &src_weights = cell_texture_weights_temp[ci];
-
- for (unsigned int i = 0; i < cell_textures.indices.size(); ++i) {
- const unsigned int ti = cell_textures.indices[i];
- dst_weights[i] = src_weights[ti];
- }
- }
-
- return cell_textures;
-}
-
-struct TextureIndicesData {
- Span buffer;
- FixedArray default_indices;
- uint32_t packed_default_indices;
-};
-
-template
-inline void get_cell_texture_data(
- CellTextureDatas &cell_textures,
- const TextureIndicesData &texture_indices_data,
- const FixedArray &voxel_indices,
- const WeightSampler_T &weights_data,
- // Used for rejecting air voxels. Can be set to 0 so all corners are always used.
- const unsigned int case_code
-) {
- if (texture_indices_data.buffer.size() == 0) {
- // Indices are known for the whole block, just read weights directly
- cell_textures.indices = texture_indices_data.default_indices;
- cell_textures.packed_indices = texture_indices_data.packed_default_indices;
- for (unsigned int ci = 0; ci < voxel_indices.size(); ++ci) {
- if ((case_code & (1 << ci)) != 0) {
- // Force air voxels to not contribute
- // TODO This is not great, because every Transvoxel vertex interpolates between a matter and air corner.
- // This approach means we would always interpolate towards 0 as a result.
- // Maybe we'll have to use a different approach and remove this option in the future.
- fill(cell_textures.weights[ci], uint8_t(0));
- } else {
- const unsigned int wi = voxel_indices[ci];
- cell_textures.weights[ci] = weights_data.get_weights(wi);
- }
- }
-
- } else {
- // There can be more than 4 indices or they are not known, so we have to select them
- cell_textures =
- select_textures_4_per_voxel(voxel_indices, texture_indices_data.buffer, weights_data, case_code);
- }
-}
-
template
inline Sdf_T get_isolevel() = delete;
@@ -329,20 +174,36 @@ inline float get_isolevel() {
return 0.f;
}
+struct MaterialProcessorNull {
+ // Called for every 2x2x2 cell containing triangles.
+ // The returned value is used to determine if the next cell can re-use vertices from previous cells, when equal.
+ inline uint32_t on_cell(const FixedArray &corner_voxel_indices, const uint8_t case_code) const {
+ return 0;
+ }
+ // Called for every 2x3x3 transition cell containing triangles.
+ // Such cells are actually in 2D data-wise, so corners are the same value, so only 9 are passed in.
+ // The returned value is used to determine if the next cell can re-use vertices from previous cells, when equal.
+ inline uint32_t on_transition_cell(const FixedArray &corner_voxel_indices, const uint8_t case_code)
+ const {
+ return 0;
+ }
+ // Called one or more times after each `on_cell` for every new vertex, to interpolate and add material data
+ inline void on_vertex(const unsigned int v0, const unsigned int v1, const float alpha) const {
+ return;
+ }
+};
+
// This function is template so we avoid branches and checks when sampling voxels
-template
+template
void build_regular_mesh(
- Span sdf_data,
- TextureIndicesData texture_indices_data,
- const WeightSampler_T &weights_sampler,
+ Span sdf_data,
+ TMaterialProcessor material_processor,
const Vector3i block_size_with_padding,
uint32_t lod_index,
- TexturingMode texturing_mode,
Cache &cache,
MeshArrays &output,
StdVector *cell_info,
- const float edge_clamp_margin,
- const bool textures_skip_air_voxels
+ const float edge_clamp_margin
) {
ZN_PROFILE_SCOPE();
@@ -371,7 +232,7 @@ void build_regular_mesh(
const unsigned int n111 = n100 + n010 + n001;
// Get direct representation of the isolevel (not always zero since we are not using signed integers yet)
- const Sdf_T isolevel = get_isolevel();
+ const TSdf isolevel = get_isolevel();
// Iterate all cells with padding (expected to be neighbors)
Vector3i pos;
@@ -406,8 +267,6 @@ void build_regular_mesh(
}
}
- // ZN_PROFILE_SCOPE();
-
// 6-------7
// /| /|
// / | / | Corners
@@ -465,17 +324,7 @@ void build_regular_mesh(
padded_corner_positions[6] = Vector3i(pos.x, pos.y + 1, pos.z + 1);
padded_corner_positions[7] = Vector3i(pos.x + 1, pos.y + 1, pos.z + 1);
- CellTextureDatas<8> cell_textures;
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- get_cell_texture_data(
- cell_textures,
- texture_indices_data,
- corner_data_indices,
- weights_sampler,
- textures_skip_air_voxels ? case_code : 0
- );
- current_reuse_cell.packed_texture_indices = cell_textures.packed_indices;
- }
+ current_reuse_cell.packed_texture_indices = material_processor.on_cell(corner_data_indices, case_code);
FixedArray corner_positions;
for (unsigned int i = 0; i < padded_corner_positions.size(); ++i) {
@@ -501,6 +350,7 @@ void build_regular_mesh(
FixedArray cell_vertex_indices;
fill(cell_vertex_indices, -1);
+ // TODO When not using LOD, this is not necessary
const uint8_t cell_border_mask = get_border_mask(pos - min_pos, block_size - Vector3i(1, 1, 1));
// For each vertex in the case
@@ -562,7 +412,7 @@ void build_regular_mesh(
if (present) {
const Vector3i cache_pos = pos + dir_to_prev_vec(reuse_dir);
const ReuseCell &prev_cell = cache.get_reuse_cell(cache_pos);
- if (prev_cell.packed_texture_indices == cell_textures.packed_indices) {
+ if (prev_cell.packed_texture_indices == current_reuse_cell.packed_texture_indices) {
// Will reuse a previous vertice
cell_vertex_indices[vertex_index] = prev_cell.vertices[reuse_vertex_index];
}
@@ -582,10 +432,10 @@ void build_regular_mesh(
// I'm not sure how to overcome this because if we sample low-detail normals, we get a
// "blocky" result due to SDF clipping. If we sample high-detail gradients, we get details,
// but if details are bumpy, we also get noisy results.
- const Vector3f cg0 = get_corner_gradient(
+ const Vector3f cg0 = get_corner_gradient(
corner_data_indices[v0], sdf_data, block_size_with_padding
);
- const Vector3f cg1 = get_corner_gradient(
+ const Vector3f cg1 = get_corner_gradient(
corner_data_indices[v1], sdf_data, block_size_with_padding
);
const Vector3f normal = normalized_not_null(cg0 * t0 + cg1 * t1);
@@ -604,17 +454,7 @@ void build_regular_mesh(
primaryf, normal, cell_border_mask, vertex_border_mask, 0, secondary
);
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- const FixedArray weights0 = cell_textures.weights[v0];
- const FixedArray weights1 = cell_textures.weights[v1];
- FixedArray weights;
- for (unsigned int i = 0; i < MAX_TEXTURE_BLENDS; ++i) {
- weights[i] = static_cast(
- math::clamp(Math::lerp(weights0[i], weights1[i], t1), 0.f, 255.f)
- );
- }
- add_texture_data(output.texturing_data, cell_textures.packed_indices, weights);
- }
+ material_processor.on_vertex(v0, v1, t1);
if (reuse_dir & 8) {
// Store the generated vertex so that other cells can reuse it.
@@ -630,7 +470,7 @@ void build_regular_mesh(
const Vector3i primary = p1;
const Vector3f primaryf = to_vec3f(primary);
const Vector3f cg1 =
- get_corner_gradient(corner_data_indices[v1], sdf_data, block_size_with_padding);
+ get_corner_gradient(corner_data_indices[v1], sdf_data, block_size_with_padding);
const Vector3f normal = normalized_not_null(cg1);
Vector3f secondary;
@@ -644,10 +484,7 @@ void build_regular_mesh(
cell_vertex_indices[vertex_index] =
output.add_vertex(primaryf, normal, cell_border_mask, vertex_border_mask, 0, secondary);
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- const FixedArray weights1 = cell_textures.weights[v1];
- add_texture_data(output.texturing_data, cell_textures.packed_indices, weights1);
- }
+ material_processor.on_vertex(v0, v1, 1.f);
current_reuse_cell.vertices[0] = cell_vertex_indices[vertex_index];
@@ -684,12 +521,11 @@ void build_regular_mesh(
{
// Create new vertex
- // TODO Earlier we associated t==0 to p0, why are we doing p1 here?
const unsigned int vi = t == 0 ? v1 : v0;
const Vector3i primary = t == 0 ? p1 : p0;
const Vector3f primaryf = to_vec3f(primary);
- const Vector3f cg = get_corner_gradient(
+ const Vector3f cg = get_corner_gradient(
corner_data_indices[vi], sdf_data, block_size_with_padding
);
const Vector3f normal = normalized_not_null(cg);
@@ -707,10 +543,7 @@ void build_regular_mesh(
primaryf, normal, cell_border_mask, vertex_border_mask, 0, secondary
);
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- const FixedArray weights = cell_textures.weights[vi];
- add_texture_data(output.texturing_data, cell_textures.packed_indices, weights);
- }
+ material_processor.on_vertex(v0, v1, 1.f - t);
}
}
@@ -771,7 +604,7 @@ void build_regular_mesh(
}
if (cell_info != nullptr) {
- cell_info->push_back(CellInfo{ pos - min_pos, effective_triangle_count });
+ cell_info->push_back(CellInfo{ pos - min_pos, static_cast(effective_triangle_count) });
}
} // x
@@ -881,19 +714,16 @@ inline uint8_t get_face_index(int cube_dir) {
}
}
-template
+template
void build_transition_mesh(
- Span sdf_data,
- TextureIndicesData texture_indices_data,
- const WeightSampler_T &weights_sampler,
+ Span sdf_data,
+ TMaterialProcessor material_processor,
const Vector3i block_size_with_padding,
const int direction,
const int lod_index,
- TexturingMode texturing_mode,
Cache &cache,
MeshArrays &output,
- const float edge_clamp_margin,
- const bool textures_skip_air_voxels
+ const float edge_clamp_margin
) {
// From this point, we expect the buffer to contain allocated data.
// This function has some comments as quotes from the Transvoxel paper.
@@ -960,7 +790,7 @@ void build_transition_mesh(
FixedArray cell_positions;
const int fz = MIN_PADDING;
- const Sdf_T isolevel = get_isolevel();
+ const TSdf isolevel = get_isolevel();
const uint8_t transition_hint_mask = 1 << get_face_index(direction);
@@ -1028,11 +858,8 @@ void build_transition_mesh(
cell_samples[0xC] = cell_samples[8];
uint16_t case_code = sign_f(cell_samples[0]);
- // The order chosen here is dependent on how the Transvoxel tables are laid out (see figures 4.16 and 4.17
- // of the paper), which unfortunately is different from the sampling pattern. That prevents from re-using it
- // in texture selection.
- // The reason for that choice seems to stem from convenience when creating the lookup tables. Does that mean
- // we could just re-order them?
+ // Note, the case code here is not ordered the same as the sampling order (as per Transvoxel paper) due to
+ // how lookup tables were made
case_code |= (sign_f(cell_samples[1]) << 1);
case_code |= (sign_f(cell_samples[2]) << 2);
case_code |= (sign_f(cell_samples[5]) << 3);
@@ -1047,49 +874,9 @@ void build_transition_mesh(
continue;
}
- CellTextureDatas<13> cell_textures;
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- // Re-making an ordered case code...
- // |436785210|
- const uint16_t alt_case_code = textures_skip_air_voxels ? 0 //
- | ((case_code & 0b000000111)) // 210
- | ((case_code & 0b110000000) >> 4) // 43
- | ((case_code & 0b000001000) << 2) // 5
- | ((case_code & 0b001000000)) // 6
- | ((case_code & 0b000100000) << 2) // 7
- | ((case_code & 0b000010000) << 4) // 8
- : 0;
-
- // uint16_t alt_case_code = sign_f(cell_samples[0]);
- // alt_case_code |= (sign_f(cell_samples[1]) << 1);
- // alt_case_code |= (sign_f(cell_samples[2]) << 2);
- // alt_case_code |= (sign_f(cell_samples[3]) << 3);
- // alt_case_code |= (sign_f(cell_samples[4]) << 4);
- // alt_case_code |= (sign_f(cell_samples[5]) << 5);
- // alt_case_code |= (sign_f(cell_samples[6]) << 6);
- // alt_case_code |= (sign_f(cell_samples[7]) << 7);
- // alt_case_code |= (sign_f(cell_samples[8]) << 8);
-
- CellTextureDatas<9> cell_textures_partial;
- get_cell_texture_data(
- cell_textures_partial, texture_indices_data, cell_data_indices, weights_sampler, alt_case_code
- );
-
- cell_textures.indices = cell_textures_partial.indices;
- cell_textures.packed_indices = cell_textures_partial.packed_indices;
-
- for (unsigned int i = 0; i < cell_textures_partial.weights.size(); ++i) {
- cell_textures.weights[i] = cell_textures_partial.weights[i];
- }
-
- cell_textures.weights[0x9] = cell_textures_partial.weights[0];
- cell_textures.weights[0xA] = cell_textures_partial.weights[2];
- cell_textures.weights[0xB] = cell_textures_partial.weights[6];
- cell_textures.weights[0xC] = cell_textures_partial.weights[8];
-
- ReuseTransitionCell ¤t_reuse_cell = cache.get_reuse_cell_2d(fx, fy);
- current_reuse_cell.packed_texture_indices = cell_textures.packed_indices;
- }
+ ReuseTransitionCell ¤t_reuse_cell = cache.get_reuse_cell_2d(fx, fy);
+ current_reuse_cell.packed_texture_indices =
+ material_processor.on_transition_cell(cell_data_indices, case_code);
ZN_ASSERT(case_code <= 511);
@@ -1192,7 +979,7 @@ void build_transition_mesh(
// from which to retrieve the reused vertex index from.
const ReuseTransitionCell &prev =
cache.get_reuse_cell_2d(fx - (reuse_direction & 1), fy - ((reuse_direction >> 1) & 1));
- if (prev.packed_texture_indices == cell_textures.packed_indices) {
+ if (prev.packed_texture_indices == current_reuse_cell.packed_texture_indices) {
// Reuse the vertex index from the previous cell.
cell_vertex_indices[vertex_index] = prev.vertices[vertex_index_to_reuse_or_create];
}
@@ -1232,19 +1019,7 @@ void build_transition_mesh(
primaryf, normal, cell_border_mask2, vertex_border_mask, transition_hint_mask, secondary
);
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- const FixedArray weights0 =
- cell_textures.weights[index_vertex_a];
- const FixedArray weights1 =
- cell_textures.weights[index_vertex_b];
- FixedArray weights;
- for (unsigned int i = 0; i < cell_textures.indices.size(); ++i) {
- weights[i] = static_cast(
- math::clamp(Math::lerp(weights0[i], weights1[i], t1), 0.f, 255.f)
- );
- }
- add_texture_data(output.texturing_data, cell_textures.packed_indices, weights);
- }
+ material_processor.on_vertex(index_vertex_a, index_vertex_b, t1);
if (reuse_direction & 0x8) {
// The vertex can be re-used later
@@ -1298,10 +1073,7 @@ void build_transition_mesh(
primaryf, normal, cell_border_mask2, vertex_border_mask, transition_hint_mask, secondary
);
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- const FixedArray weights = cell_textures.weights[cell_index];
- add_texture_data(output.texturing_data, cell_textures.packed_indices, weights);
- }
+ material_processor.on_vertex(index_vertex_a, index_vertex_b, 1.f - t);
// We are on a corner so the vertex will be re-usable later
ReuseTransitionCell &r = cache.get_reuse_cell_2d(fx, fy);
@@ -1337,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) {
@@ -1352,72 +1124,6 @@ Span get_or_decompress_channel(const VoxelBuffer &voxels, StdVector
}
}
-TextureIndicesData get_texture_indices_data(
- const VoxelBuffer &voxels,
- unsigned int channel,
- DefaultTextureIndicesData &out_default_texture_indices_data
-) {
- ZN_ASSERT_RETURN_V(voxels.get_channel_depth(channel) == VoxelBuffer::DEPTH_16_BIT, TextureIndicesData());
-
- TextureIndicesData data;
-
- if (voxels.is_uniform(channel)) {
- const uint16_t encoded_indices = voxels.get_voxel(Vector3i(), channel);
- data.default_indices = decode_indices_from_packed_u16(encoded_indices);
- data.packed_default_indices = pack_bytes(data.default_indices);
-
- out_default_texture_indices_data.indices = data.default_indices;
- out_default_texture_indices_data.packed_indices = data.packed_default_indices;
- out_default_texture_indices_data.use = true;
-
- } else {
- Span data_bytes;
- ZN_ASSERT(voxels.get_channel_as_bytes_read_only(channel, data_bytes) == true);
- data.buffer = data_bytes.reinterpret_cast_to();
-
- out_default_texture_indices_data.use = false;
- }
-
- return data;
-}
-
-// I'm not really decided if doing this is better or not yet?
-// #define USE_TRICHANNEL
-
-#ifdef USE_TRICHANNEL
-// TODO Is this a faster/equivalent option with better precision?
-struct WeightSampler3U8 {
- Span u8_data0;
- Span u8_data1;
- Span u8_data2;
- inline FixedArray get_weights(int i) const {
- FixedArray w;
- w[0] = u8_data0[i];
- w[1] = u8_data1[i];
- w[2] = u8_data2[i];
- w[3] = 255 - (w[0] + w[1] + w[2]);
- return w;
- }
-};
-
-thread_local StdVector s_weights_backing_buffer_u8_0;
-thread_local StdVector s_weights_backing_buffer_u8_1;
-thread_local StdVector s_weights_backing_buffer_u8_2;
-
-#else
-struct WeightSamplerPackedU16 {
- Span u16_data;
- inline FixedArray get_weights(int i) const {
- return decode_weights_from_packed_u16(u16_data[i]);
- }
-};
-
-StdVector &get_tls_weights_backing_buffer_u16() {
- thread_local StdVector tls_weights_backing_buffer_u16;
- return tls_weights_backing_buffer_u16;
-}
-#endif
-
// Presence of zeroes in samples occurs more often when precision is scarce
// (8-bit, scaled SDF, or slow gradients).
// This causes two symptoms:
@@ -1458,56 +1164,25 @@ Span apply_zero_sdf_fix(Span p_sdf_data) {
return to_span_const(sdf_data);
}*/
-DefaultTextureIndicesData build_regular_mesh(
+StdVector &get_tls_weights_backing_buffer_u16() {
+ thread_local StdVector tls_weights_backing_buffer_u16;
+ return tls_weights_backing_buffer_u16;
+}
+
+template
+inline void build_regular_mesh_dispatch_sd(
const VoxelBuffer &voxels,
const unsigned int sdf_channel,
+ TMaterialProcessor material_processor,
const uint32_t lod_index,
- const TexturingMode texturing_mode,
Cache &cache,
MeshArrays &output,
StdVector *cell_infos,
- const float edge_clamp_margin,
- const bool textures_ignore_air_voxels
+ const float edge_clamp_margin
) {
- ZN_PROFILE_SCOPE();
- // From this point, we expect the buffer to contain allocated data in the relevant channels.
-
Span sdf_data_raw;
ZN_ASSERT(voxels.get_channel_as_bytes_read_only(sdf_channel, sdf_data_raw) == true);
- const unsigned int voxels_count = Vector3iUtil::get_volume(voxels.get_size());
-
- DefaultTextureIndicesData default_texture_indices_data;
- default_texture_indices_data.use = false;
- TextureIndicesData indices_data;
-#ifdef USE_TRICHANNEL
- WeightSampler3U8 weights_data;
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- // From this point we know SDF is not uniform so it has an allocated buffer,
- // but it might have uniform indices or weights so we need to ensure there is a backing buffer.
- indices_data = get_texture_indices_data(voxels, VoxelBuffer::CHANNEL_INDICES, default_texture_indices_data);
- weights_data.u8_data0 =
- get_or_decompress_channel(voxels, s_weights_backing_buffer_u8_0, VoxelBuffer::CHANNEL_WEIGHTS);
- weights_data.u8_data1 =
- get_or_decompress_channel(voxels, s_weights_backing_buffer_u8_1, VoxelBuffer::CHANNEL_DATA5);
- weights_data.u8_data2 =
- get_or_decompress_channel(voxels, s_weights_backing_buffer_u8_2, VoxelBuffer::CHANNEL_DATA6);
- ERR_FAIL_COND_V(weights_data.u8_data0.size() != voxels_count, default_texture_indices_data);
- ERR_FAIL_COND_V(weights_data.u8_data1.size() != voxels_count, default_texture_indices_data);
- ERR_FAIL_COND_V(weights_data.u8_data2.size() != voxels_count, default_texture_indices_data);
- }
-#else
- WeightSamplerPackedU16 weights_data;
- if (texturing_mode == TEXTURES_BLEND_4_OVER_16) {
- // From this point we know SDF is not uniform so it has an allocated buffer,
- // but it might have uniform indices or weights so we need to ensure there is a backing buffer.
- indices_data = get_texture_indices_data(voxels, VoxelBuffer::CHANNEL_INDICES, default_texture_indices_data);
- weights_data.u16_data =
- get_or_decompress_channel(voxels, get_tls_weights_backing_buffer_u16(), VoxelBuffer::CHANNEL_WEIGHTS);
- ZN_ASSERT_RETURN_V(weights_data.u16_data.size() == voxels_count, default_texture_indices_data);
- }
-#endif
-
// We settle data types up-front so we can get rid of abstraction layers and conditionals,
// which would otherwise harm performance in tight iterations
switch (voxels.get_channel_depth(sdf_channel)) {
@@ -1515,16 +1190,13 @@ DefaultTextureIndicesData build_regular_mesh(
Span