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 sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), lod_index, - texturing_mode, cache, output, cell_infos, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; @@ -1532,16 +1204,13 @@ DefaultTextureIndicesData build_regular_mesh( Span sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), lod_index, - texturing_mode, cache, output, cell_infos, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; @@ -1552,109 +1221,132 @@ DefaultTextureIndicesData build_regular_mesh( Span sdf_data = sdf_data_raw.reinterpret_cast_to(); build_regular_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), lod_index, - texturing_mode, cache, output, cell_infos, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; - case VoxelBuffer::DEPTH_64_BIT: - ZN_PRINT_ERROR("Double-precision SDF channel is not supported"); - // Not worth growing executable size for relatively pointless double-precision sdf - break; + case VoxelBuffer::DEPTH_64_BIT: { + static bool s_once = false; + if (s_once == false) { + s_once = true; + ZN_PRINT_ERROR("Double-precision SDF channel is not supported"); + // Not worth growing executable size for relatively pointless double-precision sdf + } + } break; default: ZN_PRINT_ERROR("Invalid channel"); break; } - - return default_texture_indices_data; } -void build_transition_mesh( +DefaultTextureIndicesData build_regular_mesh( const VoxelBuffer &voxels, const unsigned int sdf_channel, - const int direction, const uint32_t lod_index, const TexturingMode texturing_mode, Cache &cache, MeshArrays &output, - DefaultTextureIndicesData default_texture_indices_data, + StdVector *cell_infos, const float edge_clamp_margin, const bool textures_ignore_air_voxels ) { 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_u64(voxels.get_size()); - const unsigned int voxels_count = Vector3iUtil::get_volume(voxels.get_size()); - - // TODO Support more texturing data configurations - TextureIndicesData indices_data; -#ifdef USE_TRICHANNEL - WeightSampler3U8 weights_data; - if (texturing_mode == TEXTURES_BLEND_4_OVER_16) { - if (default_texture_indices_data.use) { - indices_data.default_indices = default_texture_indices_data.indices; - indices_data.packed_default_indices = default_texture_indices_data.packed_indices; - } else { - // 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. - // TODO Is it worth doing conditionnals instead during meshing? - 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(weights_data.u8_data0.size() != voxels_count); - ERR_FAIL_COND(weights_data.u8_data1.size() != voxels_count); - ERR_FAIL_COND(weights_data.u8_data2.size() != voxels_count); - } -#else - WeightSamplerPackedU16 weights_data; - if (texturing_mode == TEXTURES_BLEND_4_OVER_16) { - if (default_texture_indices_data.use) { - indices_data.default_indices = default_texture_indices_data.indices; - indices_data.packed_default_indices = default_texture_indices_data.packed_indices; - } else { - // 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. - // TODO Is it worth doing conditionnals instead during meshing? - 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(weights_data.u16_data.size() == voxels_count); + output.clear(); + + DefaultTextureIndicesData default_texture_indices; + default_texture_indices.use = false; + + switch (texturing_mode) { + case TEXTURES_NONE: + build_regular_mesh_dispatch_sd( + voxels, + sdf_channel, + MaterialProcessorNull{}, + lod_index, + cache, + output, + cell_infos, + edge_clamp_margin + ); + break; + + case TEXTURES_BLEND_4_OVER_16: { + TextureIndicesData voxel_material_indices; + WeightSamplerPackedU16 voxel_material_weights; + if (texturing_mode == TEXTURES_BLEND_4_OVER_16) { + ZN_PROFILE_SCOPE_NAMED("Prepare material info"); + + // 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. + voxel_material_indices = + get_texture_indices_data(voxels, VoxelBuffer::CHANNEL_INDICES, default_texture_indices); + voxel_material_weights.u16_data = get_or_decompress_channel( + voxels, get_tls_weights_backing_buffer_u16(), VoxelBuffer::CHANNEL_WEIGHTS + ); + ZN_ASSERT_RETURN_V(voxel_material_weights.u16_data.size() == voxels_count, default_texture_indices); + } + build_regular_mesh_dispatch_sd( + voxels, + sdf_channel, + MaterialProcessorMixel4<8>( + voxel_material_indices, + voxel_material_weights, + output.texturing_data, + textures_ignore_air_voxels + ), + lod_index, + cache, + output, + cell_infos, + edge_clamp_margin + ); + } break; + + default: + ZN_PRINT_ERROR("Invalid material mode"); + break; } -#endif + + return default_texture_indices; +} + +template +inline void build_transition_mesh_dispatch_sd( + const VoxelBuffer &voxels, + const unsigned int sdf_channel, + const TMaterialProcessor material_processor, + const int direction, + const uint32_t lod_index, + Cache &cache, + MeshArrays &output, + const float edge_clamp_margin +) { + Span sdf_data_raw; + ZN_ASSERT(voxels.get_channel_as_bytes_read_only(sdf_channel, sdf_data_raw) == true); switch (voxels.get_channel_depth(sdf_channel)) { case VoxelBuffer::DEPTH_8_BIT: { Span sdf_data = sdf_data_raw.reinterpret_cast_to(); build_transition_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), direction, lod_index, - texturing_mode, cache, output, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; @@ -1662,16 +1354,13 @@ void build_transition_mesh( Span sdf_data = sdf_data_raw.reinterpret_cast_to(); build_transition_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), direction, lod_index, - texturing_mode, cache, output, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; @@ -1679,16 +1368,13 @@ void build_transition_mesh( Span sdf_data = sdf_data_raw.reinterpret_cast_to(); build_transition_mesh( sdf_data, - indices_data, - weights_data, + material_processor, voxels.get_size(), direction, lod_index, - texturing_mode, cache, output, - edge_clamp_margin, - textures_ignore_air_voxels + edge_clamp_margin ); } break; @@ -1703,4 +1389,68 @@ void build_transition_mesh( } } +void build_transition_mesh( + const VoxelBuffer &voxels, + const unsigned int sdf_channel, + const int direction, + const uint32_t lod_index, + const TexturingMode texturing_mode, + Cache &cache, + MeshArrays &output, + DefaultTextureIndicesData default_texture_indices_data, + const float edge_clamp_margin, + const bool textures_ignore_air_voxels +) { + ZN_PROFILE_SCOPE(); + // From this point, we expect the buffer to contain allocated data in the relevant channels. + + const unsigned int voxels_count = Vector3iUtil::get_volume_u64(voxels.get_size()); + + switch (texturing_mode) { + case TEXTURES_NONE: + build_transition_mesh_dispatch_sd( + voxels, sdf_channel, MaterialProcessorNull{}, direction, lod_index, cache, output, edge_clamp_margin + ); + break; + + case TEXTURES_BLEND_4_OVER_16: { + // TODO Support more texturing data configurations + TextureIndicesData indices_data; + WeightSamplerPackedU16 weights_data; + + if (default_texture_indices_data.use) { + indices_data.default_indices = default_texture_indices_data.indices; + indices_data.packed_default_indices = default_texture_indices_data.packed_indices; + } else { + // 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. + // TODO Is it worth doing conditionnals instead during meshing? + 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(weights_data.u16_data.size() == voxels_count); + + build_transition_mesh_dispatch_sd( + voxels, + sdf_channel, + MaterialProcessorMixel4<13>( + indices_data, weights_data, output.texturing_data, textures_ignore_air_voxels + ), + direction, + lod_index, + cache, + output, + edge_clamp_margin + ); + } break; + + default: + ZN_PRINT_ERROR("Invalid material mode"); + break; + } +} + } // namespace zylann::voxel::transvoxel diff --git a/meshers/transvoxel/transvoxel.h b/meshers/transvoxel/transvoxel.h index c5e9c3b8e..ec34727da 100644 --- a/meshers/transvoxel/transvoxel.h +++ b/meshers/transvoxel/transvoxel.h @@ -77,12 +77,12 @@ struct MeshArrays { } int add_vertex( - Vector3f primary, - Vector3f normal, - uint8_t cell_border_mask, - uint8_t vertex_border_mask, - uint8_t transition, - Vector3f secondary + const Vector3f primary, + const Vector3f normal, + const uint8_t cell_border_mask, + const uint8_t vertex_border_mask, + const uint8_t transition, + const Vector3f secondary ) { int vi = vertices.size(); vertices.push_back(primary); @@ -94,12 +94,12 @@ struct MeshArrays { struct ReuseCell { FixedArray vertices; - unsigned int packed_texture_indices = 0; + uint32_t packed_texture_indices = 0; }; struct ReuseTransitionCell { FixedArray vertices; - unsigned int packed_texture_indices = 0; + uint32_t packed_texture_indices = 0; }; class Cache { @@ -149,8 +149,9 @@ class Cache { // This is only to re-use some data computed for regular mesh into transition meshes struct DefaultTextureIndicesData { FixedArray indices; - uint32_t packed_indices; - bool use; + uint32_t packed_indices = 0; + // TODO Use an optional? + bool use = false; }; struct CellInfo { diff --git a/meshers/transvoxel/transvoxel_materials_mixel4.h b/meshers/transvoxel/transvoxel_materials_mixel4.h new file mode 100644 index 000000000..99f6e0b95 --- /dev/null +++ b/meshers/transvoxel/transvoxel_materials_mixel4.h @@ -0,0 +1,308 @@ +#ifndef VOXEL_TRANSVOXEL_MATERIALS_MIXEL4_H +#define VOXEL_TRANSVOXEL_MATERIALS_MIXEL4_H + +#include "../../storage/materials_4i4w.h" +#include "../../util/containers/fixed_array.h" +#include "../../util/containers/std_vector.h" +#include "transvoxel.h" + +namespace zylann::voxel::transvoxel { + +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 { + // Texture indices for each voxel + Span buffer; + // Used if the buffer is empty + 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); + } +} + +struct WeightSamplerPackedU16 { + Span u16_data; + inline FixedArray get_weights(unsigned int i) const { + return decode_weights_from_packed_u16(u16_data[i]); + } +}; + +inline uint16_t reorder_transition_case_code(const uint16_t case_code) { + // Reorders the case code from a transition cell so bits corresponds to an XYZ iteration order through cell corners. + // + // The order of cell corners chosen in transition cells are 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 order in which we sample voxels. + // That prevents from re-using it in texture selection. The reason for that choice seems to stem from convenience + // when creating the lookup tables. + // |436785210| + const uint16_t alt_case_code = 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 + ; + // 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); + + return alt_case_code; +} + +template +struct MaterialProcessorMixel4 { + const TextureIndicesData voxel_material_indices; + const WeightSamplerPackedU16 voxel_material_weights; + const bool textures_skip_air_voxels; + CellTextureDatas cell_textures; + StdVector &output_mesh_material_data; + + MaterialProcessorMixel4( + const TextureIndicesData p_voxel_material_indices, + const WeightSamplerPackedU16 p_voxel_material_weights, + StdVector &p_output_mesh_material_data, + const bool p_textures_skip_air_voxels + ) : + voxel_material_indices(p_voxel_material_indices), + voxel_material_weights(p_voxel_material_weights), + textures_skip_air_voxels(p_textures_skip_air_voxels), + output_mesh_material_data(p_output_mesh_material_data) {} + + inline uint32_t on_cell(const FixedArray &corner_voxel_indices, const uint8_t case_code) { + get_cell_texture_data( + cell_textures, + voxel_material_indices, + corner_voxel_indices, + voxel_material_weights, + textures_skip_air_voxels ? case_code : 0 + ); + + return cell_textures.packed_indices; + } + + inline uint32_t on_transition_cell(const FixedArray &corner_voxel_indices, const uint8_t case_code) { + const uint16_t alt_case_code = textures_skip_air_voxels ? reorder_transition_case_code(case_code) : 0; + + // Get values from 9 significant corners + CellTextureDatas<9> cell_textures_partial; + get_cell_texture_data( + cell_textures_partial, + voxel_material_indices, + corner_voxel_indices, + voxel_material_weights, + alt_case_code + ); + + // Fill in slots that are just repeating others + + 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]; + + return cell_textures.packed_indices; + } + + inline void on_vertex(const unsigned int v0, const unsigned int v1, const float alpha) { + 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], alpha), 0.f, 255.f)); + } + add_texture_data(output_mesh_material_data, cell_textures.packed_indices, weights); + } +}; + +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; +} + +} // namespace zylann::voxel::transvoxel + +#endif // VOXEL_TRANSVOXEL_MATERIALS_MIXEL4_H diff --git a/modifiers/voxel_modifier_stack.cpp b/modifiers/voxel_modifier_stack.cpp index 6ec455215..84ccaa5e8 100644 --- a/modifiers/voxel_modifier_stack.cpp +++ b/modifiers/voxel_modifier_stack.cpp @@ -18,7 +18,7 @@ StdVector &get_tls_positions() { } void get_positions_buffer(Vector3i buffer_size, Vector3f origin, Vector3f size, StdVector &positions) { - positions.resize(Vector3iUtil::get_volume(buffer_size)); + positions.resize(Vector3iUtil::get_volume_u64(buffer_size)); const Vector3f end = origin + size; const Vector3f inv_bsf = Vector3f(1.0, 1.0, 1.0) / to_vec3f(buffer_size); @@ -64,7 +64,7 @@ Span get_positions_temporary( void decompress_sdf_to_buffer(VoxelBuffer &voxels, StdVector &sdf) { ZN_DSTACK(); - sdf.resize(Vector3iUtil::get_volume(voxels.get_size())); + sdf.resize(Vector3iUtil::get_volume_u64(voxels.get_size())); const VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; voxels.decompress_channel(channel); @@ -221,7 +221,7 @@ void VoxelModifierStack::apply(VoxelBuffer &voxels, AABB aabb) const { modifier_box.clip(Box3i(origin_voxels, voxels.get_size())); const Vector3i local_origin_in_voxels = modifier_box.position - origin_voxels; - const int64_t volume = Vector3iUtil::get_volume(modifier_box.size); + const size_t volume = Vector3iUtil::get_volume_u64(modifier_box.size); area_sdf.resize(volume); copy_3d_region_zxy( to_span(area_sdf), diff --git a/storage/funcs.cpp b/storage/funcs.cpp index 2652f0732..6c6d439bb 100644 --- a/storage/funcs.cpp +++ b/storage/funcs.cpp @@ -33,8 +33,8 @@ void copy_3d_region_zxy( ZN_PRINT_ERROR("Different overlapping spans are not allowed"); return; } - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) * item_size <= dst.size()); - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) * item_size <= src.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) * item_size <= dst.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) * item_size <= src.size()); #endif if (area_size == src_size && area_size == dst_size) { diff --git a/storage/funcs.h b/storage/funcs.h index 43a1d75e3..81dd418fe 100644 --- a/storage/funcs.h +++ b/storage/funcs.h @@ -96,7 +96,7 @@ void fill_3d_region_zxy(Span dst, Vector3i dst_size, Vector3i dst_min, Vector } #ifdef DEBUG_ENABLED - ZN_ASSERT_RETURN(Vector3iUtil::get_volume(area_size) <= dst.size()); + ZN_ASSERT_RETURN(Vector3iUtil::get_volume_u64(area_size) <= dst.size()); #endif if (area_size == dst_size) { @@ -200,8 +200,8 @@ Vector3i transform_3d_array_zxy(Span src_grid, Span dst_grid, Vector ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.x), src_size); ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.y), src_size); ZN_ASSERT_RETURN_V(Vector3iUtil::is_unit_vector(basis.z), src_size); - ZN_ASSERT_RETURN_V(src_grid.size() == static_cast(Vector3iUtil::get_volume(src_size)), src_size); - ZN_ASSERT_RETURN_V(dst_grid.size() == static_cast(Vector3iUtil::get_volume(src_size)), src_size); + ZN_ASSERT_RETURN_V(src_grid.size() == Vector3iUtil::get_volume_u64(src_size), src_size); + ZN_ASSERT_RETURN_V(dst_grid.size() == Vector3iUtil::get_volume_u64(src_size), src_size); const int xa = basis.x.x != 0 ? 0 : basis.x.y != 0 ? 1 : 2; const int ya = basis.y.x != 0 ? 0 : basis.y.y != 0 ? 1 : 2; diff --git a/storage/metadata/voxel_metadata.h b/storage/metadata/voxel_metadata.h index 80d2f7059..a2db89a2a 100644 --- a/storage/metadata/voxel_metadata.h +++ b/storage/metadata/voxel_metadata.h @@ -2,7 +2,7 @@ #define VOXEL_METADATA_H #include "../../util/memory/memory.h" -//#include "../../util/non_copyable.h" +// #include "../../util/non_copyable.h" #include "../../util/containers/span.h" #include @@ -54,8 +54,8 @@ class VoxelMetadata { void clear(); - inline Type get_type() const { - return Type(_type); + inline uint8_t get_type() const { + return _type; } void set_u64(const uint64_t &v); diff --git a/storage/voxel_buffer.cpp b/storage/voxel_buffer.cpp index 9d92b6683..b8718c06d 100644 --- a/storage/voxel_buffer.cpp +++ b/storage/voxel_buffer.cpp @@ -1088,7 +1088,7 @@ void VoxelBuffer::copy_voxel_metadata(const VoxelBuffer &src_buffer) { void get_unscaled_sdf(const VoxelBuffer &voxels, Span sdf) { ZN_PROFILE_SCOPE(); ZN_DSTACK(); - const uint64_t volume = Vector3iUtil::get_volume(voxels.get_size()); + const uint64_t volume = Vector3iUtil::get_volume_u64(voxels.get_size()); ZN_ASSERT_RETURN(volume == sdf.size()); const VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; diff --git a/storage/voxel_buffer.h b/storage/voxel_buffer.h index aba76c8ec..03be47fc0 100644 --- a/storage/voxel_buffer.h +++ b/storage/voxel_buffer.h @@ -432,7 +432,7 @@ class VoxelBuffer { } inline uint64_t get_volume() const { - return Vector3iUtil::get_volume(_size); + return Vector3iUtil::get_volume_u64(_size); } bool get_channel_as_bytes(unsigned int channel_index, Span &slice); diff --git a/storage/voxel_data.cpp b/storage/voxel_data.cpp index e7ca68b5f..823eed3cd 100644 --- a/storage/voxel_data.cpp +++ b/storage/voxel_data.cpp @@ -1026,7 +1026,7 @@ void VoxelData::get_blocks_with_voxel_data( Span> out_blocks ) const { ZN_PROFILE_SCOPE(); - ZN_ASSERT(int64_t(out_blocks.size()) >= Vector3iUtil::get_volume(p_blocks_box.size)); + ZN_ASSERT(out_blocks.size() >= Vector3iUtil::get_volume_u64(p_blocks_box.size)); const Lod &data_lod = _lods[lod_index]; diff --git a/storage/voxel_data_grid.h b/storage/voxel_data_grid.h index 5c35d832c..c3b1397a0 100644 --- a/storage/voxel_data_grid.h +++ b/storage/voxel_data_grid.h @@ -250,7 +250,7 @@ class VoxelDataGrid { inline void create(Vector3i size, unsigned int block_size) { ZN_PROFILE_SCOPE(); _blocks.clear(); - _blocks.resize(Vector3iUtil::get_volume(size)); + _blocks.resize(Vector3iUtil::get_volume_u64(size)); _size_in_blocks = size; _block_size = block_size; } diff --git a/storage/voxel_data_map.cpp b/storage/voxel_data_map.cpp index 507dd8878..5e4886b22 100644 --- a/storage/voxel_data_map.cpp +++ b/storage/voxel_data_map.cpp @@ -196,7 +196,7 @@ void VoxelDataMap::copy( ) const { // TODO Reimplement using `copy_from_chunked_storage`? - ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume(dst_buffer.get_size()) > 0, "The area to copy is empty"); + ZN_ASSERT_RETURN_MSG(Vector3iUtil::get_volume_u64(dst_buffer.get_size()) > 0, "The area to copy is empty"); const Vector3i max_pos = min_pos + dst_buffer.get_size(); const Vector3i min_block_pos = voxel_to_block(min_pos); diff --git a/streams/region/region_file.cpp b/streams/region/region_file.cpp index 3d67c8cde..e79ebaabd 100644 --- a/streams/region/region_file.cpp +++ b/streams/region/region_file.cpp @@ -38,10 +38,10 @@ bool RegionFormat::validate() const { for (unsigned int i = 0; i < channel_depths.size(); ++i) { bytes_per_block += VoxelBuffer::get_depth_bit_count(channel_depths[i]) / 8; } - bytes_per_block *= Vector3iUtil::get_volume(Vector3iUtil::create(1 << block_size_po2)); + bytes_per_block *= Vector3iUtil::get_volume_u64(Vector3iUtil::create(1 << block_size_po2)); const size_t sectors_per_block = (bytes_per_block - 1) / sector_size + 1; ERR_FAIL_COND_V(sectors_per_block > RegionBlockInfo::MAX_SECTOR_COUNT, false); - const size_t max_potential_sectors = Vector3iUtil::get_volume(region_size) * sectors_per_block; + const size_t max_potential_sectors = Vector3iUtil::get_volume_u64(region_size) * sectors_per_block; ERR_FAIL_COND_V(max_potential_sectors > RegionBlockInfo::MAX_SECTOR_INDEX, false); return true; @@ -61,11 +61,15 @@ uint32_t get_header_size_v3(const RegionFormat &format) { // Which file offset blocks data is starting // magic + version + blockinfos return MAGIC_AND_VERSION_SIZE + FIXED_HEADER_DATA_SIZE + (format.has_palette ? PALETTE_SIZE_IN_BYTES : 0) + - Vector3iUtil::get_volume(format.region_size) * sizeof(RegionBlockInfo); + Vector3iUtil::get_volume_u64(format.region_size) * sizeof(RegionBlockInfo); } bool save_header( - FileAccess &f, uint8_t version, const RegionFormat &format, const StdVector &block_infos) { + FileAccess &f, + uint8_t version, + const RegionFormat &format, + const StdVector &block_infos +) { // `f` could be anywhere in the file, we seek to ensure we start at the beginning f.seek(0); @@ -98,9 +102,12 @@ bool save_header( } // TODO Deal with endianness, this should be little-endian - zylann::godot::store_buffer(f, - Span(reinterpret_cast(block_infos.data()), - block_infos.size() * sizeof(RegionBlockInfo))); + zylann::godot::store_buffer( + f, + Span( + reinterpret_cast(block_infos.data()), block_infos.size() * sizeof(RegionBlockInfo) + ) + ); #ifdef DEBUG_ENABLED const size_t blocks_begin_offset = f.get_position(); @@ -111,14 +118,19 @@ bool save_header( } bool load_header( - FileAccess &f, uint8_t &out_version, RegionFormat &out_format, StdVector &out_block_infos) { + FileAccess &f, + uint8_t &out_version, + RegionFormat &out_format, + StdVector &out_block_infos +) { ERR_FAIL_COND_V(f.get_position() != 0, false); ERR_FAIL_COND_V(f.get_length() < MAGIC_AND_VERSION_SIZE, false); FixedArray magic; fill(magic, '\0'); ERR_FAIL_COND_V( - zylann::godot::get_buffer(f, Span(reinterpret_cast(magic.data()), 4)) != 4, false); + zylann::godot::get_buffer(f, Span(reinterpret_cast(magic.data()), 4)) != 4, false + ); ERR_FAIL_COND_V(strcmp(magic.data(), FORMAT_REGION_MAGIC) != 0, false); const uint8_t version = f.get_8(); @@ -160,7 +172,7 @@ bool load_header( } out_version = version; - out_block_infos.resize(Vector3iUtil::get_volume(out_format.region_size)); + out_block_infos.resize(Vector3iUtil::get_volume_u64(out_format.region_size)); // TODO Deal with endianness const size_t blocks_len = out_block_infos.size() * sizeof(RegionBlockInfo); @@ -250,10 +262,13 @@ Error RegionFile::open(const String &fpath, bool create_if_not_found) { } } - std::sort(blocks_sorted_by_offset.begin(), blocks_sorted_by_offset.end(), + std::sort( + blocks_sorted_by_offset.begin(), + blocks_sorted_by_offset.end(), [](const BlockInfoAndIndex &a, const BlockInfoAndIndex &b) { return a.b.get_sector_index() < b.b.get_sector_index(); - }); + } + ); CRASH_COND(_sectors.size() != 0); for (unsigned int i = 0; i < blocks_sorted_by_offset.size(); ++i) { @@ -308,7 +323,7 @@ bool RegionFile::set_format(const RegionFormat &format) { // This will be the format used to create the next file if not found on open() _header.format = format; - _header.blocks.resize(Vector3iUtil::get_volume(format.region_size)); + _header.blocks.resize(Vector3iUtil::get_volume_u64(format.region_size)); return true; } @@ -353,8 +368,11 @@ Error RegionFile::load_block(Vector3i position, VoxelBuffer &out_block) { unsigned int block_data_size = f.get_32(); CRASH_COND(f.eof_reached()); - ERR_FAIL_COND_V_MSG(!BlockSerializer::decompress_and_deserialize(f, block_data_size, out_block), ERR_PARSE_ERROR, - String("Failed to read block {0}").format(varray(position))); + ERR_FAIL_COND_V_MSG( + !BlockSerializer::decompress_and_deserialize(f, block_data_size, out_block), + ERR_PARSE_ERROR, + String("Failed to read block {0}").format(varray(position)) + ); return OK; } @@ -391,9 +409,11 @@ Error RegionFile::save_block(Vector3i position, VoxelBuffer &block) { zylann::godot::store_buffer(f, to_span(res.data)); const unsigned int end_pos = f.get_position(); - CRASH_COND_MSG(written_size != (end_pos - block_offset), + CRASH_COND_MSG( + written_size != (end_pos - block_offset), String("written_size: {0}, block_offset: {1}, end_pos: {2}") - .format(varray(written_size, block_offset, end_pos))); + .format(varray(written_size, block_offset, end_pos)) + ); pad_to_sector_size(f); block_info.set_sector_index((block_offset - _blocks_begin_offset) / _header.format.sector_size); @@ -538,8 +558,10 @@ void RegionFile::remove_sectors_from_block(Vector3i block_pos, unsigned int p_se // but FileAccess doesn't have any function to do that... so can't rely on EOF either // Erase sectors from cache - _sectors.erase(_sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count() - p_sector_count), - _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count())); + _sectors.erase( + _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count() - p_sector_count), + _sectors.begin() + (block_info.get_sector_index() + block_info.get_sector_count()) + ); const unsigned int old_sector_index = block_info.get_sector_index(); @@ -581,7 +603,7 @@ bool RegionFile::migrate_from_v2_to_v3(FileAccess &f, RegionFormat &format) { // Which file offset blocks data is starting // magic + version + blockinfos - const unsigned int old_header_size = Vector3iUtil::get_volume(format.region_size) * sizeof(uint32_t); + const unsigned int old_header_size = Vector3iUtil::get_volume_u64(format.region_size) * sizeof(uint32_t); const unsigned int new_header_size = get_header_size_v3(format) - MAGIC_AND_VERSION_SIZE; ERR_FAIL_COND_V_MSG(new_header_size < old_header_size, false, "New version is supposed to have larger header"); @@ -676,7 +698,8 @@ void RegionFile::debug_check() { const unsigned int block_begin = _blocks_begin_offset + sector_index * _header.format.sector_size; if (block_begin >= file_len) { ZN_PRINT_ERROR(format( - "LUT {} {}: offset {} is larger than file size {}", lut_index, position, block_begin, file_len)); + "LUT {} {}: offset {} is larger than file size {}", lut_index, position, block_begin, file_len + )); continue; } f.seek(block_begin); @@ -684,8 +707,14 @@ void RegionFile::debug_check() { const size_t pos = f.get_position(); const size_t remaining_size = file_len - pos; if (block_data_size > remaining_size) { - ZN_PRINT_ERROR(format("LUT {} {}: block size {} at offset {} is larger than remaining size {}", lut_index, - position, block_data_size, block_begin, remaining_size)); + ZN_PRINT_ERROR( + format("LUT {} {}: block size {} at offset {} is larger than remaining size {}", + lut_index, + position, + block_data_size, + block_begin, + remaining_size) + ); } } } diff --git a/streams/sqlite/connection.cpp b/streams/sqlite/connection.cpp index 36fa6efbe..7b36ae7be 100644 --- a/streams/sqlite/connection.cpp +++ b/streams/sqlite/connection.cpp @@ -417,6 +417,7 @@ bool Connection::save_block(const BlockLocation loc, const Span b update_block_statement = _update_instance_block_statement; break; default: + update_block_statement = nullptr; CRASH_NOW(); } @@ -471,6 +472,7 @@ VoxelStream::ResultCode Connection::load_block( get_block_statement = _get_instance_block_statement; break; default: + get_block_statement = nullptr; CRASH_NOW(); } diff --git a/streams/sqlite/voxel_stream_sqlite.cpp b/streams/sqlite/voxel_stream_sqlite.cpp index cbf1e1d5d..3bd6000f9 100644 --- a/streams/sqlite/voxel_stream_sqlite.cpp +++ b/streams/sqlite/voxel_stream_sqlite.cpp @@ -111,6 +111,7 @@ void VoxelStreamSQLite::load_voxel_blocks(Span p_bl // This should be quick after the first call because the connection is cached. sqlite::Connection *con = get_connection(); ERR_FAIL_COND(con == nullptr); + const ScopeRecycle con_scope(this, con); // Check the cache first StdVector blocks_to_load; @@ -133,7 +134,6 @@ void VoxelStreamSQLite::load_voxel_blocks(Span p_bl if (blocks_to_load.size() == 0) { // Everything was cached, no need to query the database - recycle_connection(con); return; } @@ -161,16 +161,16 @@ void VoxelStreamSQLite::load_voxel_blocks(Span p_bl } ERR_FAIL_COND(con->end_transaction() == false); - - recycle_connection(con); } void VoxelStreamSQLite::save_voxel_blocks(Span p_blocks) { sqlite::Connection *con = get_connection(); + ZN_ASSERT_RETURN(con != nullptr); const BlockLocation::CoordinateFormat coordinate_format = con->get_meta().coordinate_format; + recycle_connection(con); + const Box3i coordinate_range = BlockLocation::get_coordinate_range(coordinate_format); const unsigned int lod_count = BlockLocation::get_lod_count(coordinate_format); - recycle_connection(con); // First put in cache for (unsigned int i = 0; i < p_blocks.size(); ++i) { @@ -223,9 +223,9 @@ void VoxelStreamSQLite::load_instance_blocks(Spanbegin_transaction() == false); for (unsigned int i = 0; i < blocks_to_load.size(); ++i) { @@ -260,16 +260,16 @@ void VoxelStreamSQLite::load_instance_blocks(Spanend_transaction() == false); - - recycle_connection(con); } void VoxelStreamSQLite::save_instance_blocks(Span p_blocks) { sqlite::Connection *con = get_connection(); + ZN_ASSERT_RETURN(con != nullptr); const BlockLocation::CoordinateFormat coordinate_format = con->get_meta().coordinate_format; + recycle_connection(con); + const Box3i coordinate_range = BlockLocation::get_coordinate_range(coordinate_format); const unsigned int lod_count = BlockLocation::get_lod_count(coordinate_format); - recycle_connection(con); // First put in cache for (size_t i = 0; i < p_blocks.size(); ++i) { @@ -296,6 +296,7 @@ void VoxelStreamSQLite::load_all_blocks(FullLoadingResult &result) { sqlite::Connection *con = get_connection(); ERR_FAIL_COND(con == nullptr); + const ScopeRecycle con_scope(this, con); struct Context { FullLoadingResult &result; @@ -362,8 +363,8 @@ int VoxelStreamSQLite::get_used_channels_mask() const { void VoxelStreamSQLite::flush_cache() { sqlite::Connection *con = get_connection(); ERR_FAIL_COND(con == nullptr); + const ScopeRecycle con_scope(this, con); flush_cache_to_connection(con); - recycle_connection(con); } void VoxelStreamSQLite::flush() { @@ -509,10 +510,11 @@ VoxelStreamSQLite::CoordinateFormat VoxelStreamSQLite::get_preferred_coordinate_ } VoxelStreamSQLite::CoordinateFormat VoxelStreamSQLite::get_current_coordinate_format() { - const sqlite::Connection *con = get_connection(); + sqlite::Connection *con = get_connection(); if (con == nullptr) { return get_preferred_coordinate_format(); } + const ScopeRecycle con_scope(this, con); return to_exposed_coordinate_format(con->get_meta().coordinate_format); } @@ -524,9 +526,6 @@ bool VoxelStreamSQLite::copy_blocks_to_other_sqlite_stream(Refget_database_path() != get_database_path(), false); ZN_ASSERT_RETURN_V_MSG( @@ -535,6 +534,10 @@ bool VoxelStreamSQLite::copy_blocks_to_other_sqlite_stream(Refget_connection(); + ZN_ASSERT_RETURN_V(context.dst_con != nullptr, false); + const ScopeRecycle dst_con_scope(dst_stream.ptr(), context.dst_con); + + const bool success = src_con->load_all_blocks(&context, Context::save); - return src_con->load_all_blocks(&context, Context::save); + return success; } void VoxelStreamSQLite::_bind_methods() { diff --git a/streams/sqlite/voxel_stream_sqlite.h b/streams/sqlite/voxel_stream_sqlite.h index 3b56a7aa7..d67810bc1 100644 --- a/streams/sqlite/voxel_stream_sqlite.h +++ b/streams/sqlite/voxel_stream_sqlite.h @@ -128,6 +128,24 @@ class VoxelStreamSQLite : public VoxelStream { sqlite::Connection *get_connection(); void recycle_connection(sqlite::Connection *con); + + struct ScopeRecycle { + VoxelStreamSQLite *stream; + sqlite::Connection *connection; + + ScopeRecycle(VoxelStreamSQLite *p_stream, sqlite::Connection *p_connection) : + stream(p_stream), connection(p_connection) { +#ifdef DEV_ENABLED + ZN_ASSERT(stream != nullptr); + ZN_ASSERT(connection != nullptr); +#endif + } + + ~ScopeRecycle() { + stream->recycle_connection(connection); + } + }; + void flush_cache_to_connection(sqlite::Connection *p_connection); static void _bind_methods(); diff --git a/streams/voxel_block_serializer.cpp b/streams/voxel_block_serializer.cpp index 472cccc79..8876b7a3d 100644 --- a/streams/voxel_block_serializer.cpp +++ b/streams/voxel_block_serializer.cpp @@ -210,7 +210,6 @@ bool deserialize_metadata(VoxelMetadata &meta, MemoryReader &mr) { return false; } } - return false; } bool deserialize_metadata(Span p_src, VoxelBuffer &buffer) { @@ -296,7 +295,7 @@ SerializeResult serialize(const VoxelBuffer &voxel_buffer) { metadata_tmp.clear(); // Cannot serialize an empty block - ERR_FAIL_COND_V(Vector3iUtil::get_volume(voxel_buffer.get_size()) == 0, SerializeResult(dst_data, false)); + ERR_FAIL_COND_V(Vector3iUtil::get_volume_u64(voxel_buffer.get_size()) == 0, SerializeResult(dst_data, false)); size_t expected_metadata_size = 0; const size_t expected_data_size = get_size_in_bytes(voxel_buffer, expected_metadata_size); @@ -668,6 +667,9 @@ bool deserialize(Span p_data, VoxelBuffer &out_voxel_buffer) { v = f.get_64(); break; default: + // Fix uninitialized variable warning on Clang, even though it is not supposed to carry on after + // the switch + v = 0; CRASH_NOW(); } out_voxel_buffer.clear_channel(channel_index, v); diff --git a/streams/voxel_stream.cpp b/streams/voxel_stream.cpp index 6e5a66bce..7d1a25f0d 100644 --- a/streams/voxel_stream.cpp +++ b/streams/voxel_stream.cpp @@ -88,22 +88,22 @@ void VoxelStream::flush() { VoxelStream::ResultCode VoxelStream::_b_load_voxel_block( Ref out_buffer, - Vector3i origin_in_voxels, + Vector3i block_position, int lod_index ) { ERR_FAIL_COND_V(lod_index < 0, RESULT_ERROR); ERR_FAIL_COND_V(lod_index >= static_cast(constants::MAX_LOD), RESULT_ERROR); ERR_FAIL_COND_V(out_buffer.is_null(), RESULT_ERROR); - VoxelQueryData q{ out_buffer->get_buffer(), origin_in_voxels, static_cast(lod_index), RESULT_ERROR }; + VoxelQueryData q{ out_buffer->get_buffer(), block_position, static_cast(lod_index), RESULT_ERROR }; load_voxel_block(q); return q.result; } -void VoxelStream::_b_save_voxel_block(Ref buffer, Vector3i origin_in_voxels, int lod_index) { +void VoxelStream::_b_save_voxel_block(Ref buffer, Vector3i block_position, int lod_index) { ERR_FAIL_COND(lod_index < 0); ERR_FAIL_COND(lod_index >= static_cast(constants::MAX_LOD)); ERR_FAIL_COND(buffer.is_null()); - VoxelQueryData q{ buffer->get_buffer(), origin_in_voxels, static_cast(lod_index), RESULT_ERROR }; + VoxelQueryData q{ buffer->get_buffer(), block_position, static_cast(lod_index), RESULT_ERROR }; save_voxel_block(q); } @@ -117,11 +117,10 @@ Vector3 VoxelStream::_b_get_block_size() const { void VoxelStream::_bind_methods() { ClassDB::bind_method( - D_METHOD("load_voxel_block", "out_buffer", "origin_in_voxels", "lod_index"), - &VoxelStream::_b_load_voxel_block + D_METHOD("load_voxel_block", "out_buffer", "block_position", "lod_index"), &VoxelStream::_b_load_voxel_block ); ClassDB::bind_method( - D_METHOD("save_voxel_block", "buffer", "origin_in_voxels", "lod_index"), &VoxelStream::_b_save_voxel_block + D_METHOD("save_voxel_block", "buffer", "block_position", "lod_index"), &VoxelStream::_b_save_voxel_block ); ClassDB::bind_method(D_METHOD("get_used_channels_mask"), &VoxelStream::_b_get_used_channels_mask); diff --git a/streams/voxel_stream.h b/streams/voxel_stream.h index cb30fddb1..933041da3 100644 --- a/streams/voxel_stream.h +++ b/streams/voxel_stream.h @@ -137,8 +137,8 @@ class VoxelStream : public Resource { private: static void _bind_methods(); - ResultCode _b_load_voxel_block(Ref out_buffer, Vector3i origin_in_voxels, int lod_index); - void _b_save_voxel_block(Ref buffer, Vector3i origin_in_voxels, int lod_index); + ResultCode _b_load_voxel_block(Ref out_buffer, Vector3i block_position, int lod_index); + void _b_save_voxel_block(Ref buffer, Vector3i block_position, int lod_index); int _b_get_used_channels_mask() const; Vector3 _b_get_block_size() const; diff --git a/terrain/fixed_lod/voxel_terrain.cpp b/terrain/fixed_lod/voxel_terrain.cpp index 62ec55593..0adf1d748 100644 --- a/terrain/fixed_lod/voxel_terrain.cpp +++ b/terrain/fixed_lod/voxel_terrain.cpp @@ -484,6 +484,7 @@ void VoxelTerrain::unview_mesh_block(Vector3i bpos, bool mesh_flag, bool collisi if (block->mesh_viewers.get() == 0) { // Mesh no longer required block->drop_mesh(); + block->set_visible(false); } } @@ -492,6 +493,7 @@ void VoxelTerrain::unview_mesh_block(Vector3i bpos, bool mesh_flag, bool collisi if (block->collision_viewers.get() == 0) { // Collision no longer required block->drop_collision(); + block->set_collision_enabled(false); } } @@ -1475,36 +1477,42 @@ void VoxelTerrain::process_viewer_data_box_change( _unloaded_saving_blocks[bts.position] = bts.voxels; } - // Remove loading blocks (those were loaded and had their refcount reach zero) - for (const Vector3i bpos : tls_found_blocks_positions) { - emit_data_block_unloaded(bpos); - // TODO If they were loaded, why would they be in loading blocks? - // Probably in case we move so fast that blocks haven't even finished loading - _loading_blocks.erase(bpos); + { + ZN_PROFILE_SCOPE_NAMED("Unload signals"); + // Remove loading blocks (those were loaded and had their refcount reach zero) + for (const Vector3i bpos : tls_found_blocks_positions) { + emit_data_block_unloaded(bpos); + // TODO If they were loaded, why would they be in loading blocks? + // Probably in case we move so fast that blocks haven't even finished loading + _loading_blocks.erase(bpos); + } } // Remove refcount from loading blocks, and cancel loading if it reaches zero - for (const Vector3i bpos : tls_missing_blocks) { - auto loading_block_it = _loading_blocks.find(bpos); - if (loading_block_it == _loading_blocks.end()) { - ZN_PRINT_VERBOSE("Request to unview a loading block that was never requested"); - // Not expected, but fine I guess - return; - } - - LoadingBlock &loading_block = loading_block_it->second; - loading_block.viewers.remove(); - - if (loading_block.viewers.get() == 0) { - // No longer want to load it - _loading_blocks.erase(loading_block_it); + { + ZN_PROFILE_SCOPE_NAMED("Cancel missing blocks"); + for (const Vector3i bpos : tls_missing_blocks) { + auto loading_block_it = _loading_blocks.find(bpos); + if (loading_block_it == _loading_blocks.end()) { + ZN_PRINT_VERBOSE("Request to unview a loading block that was never requested"); + // Not expected, but fine I guess + return; + } - // TODO Do we really need that vector after all? - for (size_t i = 0; i < _blocks_pending_load.size(); ++i) { - if (_blocks_pending_load[i] == bpos) { - _blocks_pending_load[i] = _blocks_pending_load.back(); - _blocks_pending_load.pop_back(); - break; + LoadingBlock &loading_block = loading_block_it->second; + loading_block.viewers.remove(); + + if (loading_block.viewers.get() == 0) { + // No longer want to load it + _loading_blocks.erase(loading_block_it); + + // TODO Do we really need that vector after all? + for (size_t i = 0; i < _blocks_pending_load.size(); ++i) { + if (_blocks_pending_load[i] == bpos) { + _blocks_pending_load[i] = _blocks_pending_load.back(); + _blocks_pending_load.pop_back(); + break; + } } } } @@ -1531,33 +1539,37 @@ void VoxelTerrain::process_viewer_data_box_change( }); // Schedule loading of missing blocks - for (const Vector3i missing_bpos : tls_missing_blocks) { - auto loading_block_it = _loading_blocks.find(missing_bpos); + { + ZN_PROFILE_SCOPE_NAMED("Gather missing blocks"); + for (const Vector3i missing_bpos : tls_missing_blocks) { + auto loading_block_it = _loading_blocks.find(missing_bpos); - if (loading_block_it == _loading_blocks.end()) { - // First viewer to request it - LoadingBlock new_loading_block; - new_loading_block.viewers.add(); + if (loading_block_it == _loading_blocks.end()) { + // First viewer to request it + LoadingBlock new_loading_block; + new_loading_block.viewers.add(); - if (require_notifications) { - new_loading_block.viewers_to_notify.push_back(viewer_id); - } + if (require_notifications) { + new_loading_block.viewers_to_notify.push_back(viewer_id); + } - _loading_blocks.insert({ missing_bpos, new_loading_block }); - _blocks_pending_load.push_back(missing_bpos); + _loading_blocks.insert({ missing_bpos, new_loading_block }); + _blocks_pending_load.push_back(missing_bpos); - } else { - // More viewers - LoadingBlock &loading_block = loading_block_it->second; - loading_block.viewers.add(); + } else { + // More viewers + LoadingBlock &loading_block = loading_block_it->second; + loading_block.viewers.add(); - if (require_notifications) { - loading_block.viewers_to_notify.push_back(viewer_id); + if (require_notifications) { + loading_block.viewers_to_notify.push_back(viewer_id); + } } } } if (require_notifications) { + ZN_PROFILE_SCOPE_NAMED("Enter notifications"); // Notifications for blocks that were already loaded for (unsigned int i = 0; i < tls_found_blocks.size(); ++i) { const Vector3i bpos = tls_found_blocks_positions[i]; @@ -1806,12 +1818,13 @@ void VoxelTerrain::process_meshing() { task->mesh_block_position = mesh_block_pos; task->lod_index = 0; task->meshing_dependency = _meshing_dependency; - task->collision_hint = _generate_collisions; + task->require_visual = mesh_block->mesh_viewers.get() > 0; + task->collision_hint = _generate_collisions && mesh_block->collision_viewers.get() > 0; task->data = _data; // This iteration order is specifically chosen to match VoxelEngine and threaded access _data->get_blocks_with_voxel_data(data_box, 0, to_span(task->blocks)); - task->blocks_count = Vector3iUtil::get_volume(data_box.size); + task->blocks_count = Vector3iUtil::get_volume_u64(data_box.size); #ifdef DEBUG_ENABLED { @@ -1882,22 +1895,24 @@ void VoxelTerrain::apply_mesh_update(const VoxelEngine::BlockMeshOutput &ob) { Ref mesh; Ref shadow_occluder_mesh; StdVector material_indices; - if (ob.has_mesh_resource) { - // The mesh was already built as part of the threaded task - mesh = ob.mesh; - shadow_occluder_mesh = ob.shadow_occluder_mesh; - // It can be empty - material_indices = std::move(ob.mesh_material_indices); - } else { - // Can't build meshes in threads, do it here - material_indices.clear(); - mesh = build_mesh( - to_span_const(ob.surfaces.surfaces), - ob.surfaces.primitive_type, - ob.surfaces.mesh_flags, - material_indices - ); - shadow_occluder_mesh = build_mesh(ob.surfaces.shadow_occluder); + if (ob.visual_was_required) { + if (ob.has_mesh_resource) { + // The mesh was already built as part of the threaded task + mesh = ob.mesh; + shadow_occluder_mesh = ob.shadow_occluder_mesh; + // It can be empty + material_indices = std::move(ob.mesh_material_indices); + } else { + // Can't build meshes in threads, do it here + material_indices.clear(); + mesh = build_mesh( + to_span_const(ob.surfaces.surfaces), + ob.surfaces.primitive_type, + ob.surfaces.mesh_flags, + material_indices + ); + shadow_occluder_mesh = build_mesh(ob.surfaces.shadow_occluder); + } } if (mesh.is_valid()) { const unsigned int surface_count = mesh->get_surface_count(); @@ -1948,15 +1963,27 @@ void VoxelTerrain::apply_mesh_update(const VoxelEngine::BlockMeshOutput &ob) { const bool gen_collisions = _generate_collisions && block->collision_viewers.get() > 0; if (gen_collisions) { Ref collision_shape = make_collision_shape_from_mesher_output(ob.surfaces, **_mesher); - const bool debug_collisions = is_inside_tree() ? get_tree()->is_debugging_collisions_hint() : false; + + bool debug_collisions = false; + if (is_inside_tree()) { + const SceneTree *scene_tree = get_tree(); +#if DEBUG_ENABLED + if (collision_shape.is_valid()) { + const Color debug_color = scene_tree->get_debug_collisions_color(); + collision_shape->set_debug_color(debug_color); + } +#endif + debug_collisions = scene_tree->is_debugging_collisions_hint(); + } + block->set_collision_shape(collision_shape, debug_collisions, this, _collision_margin); block->set_collision_layer(_collision_layer); block->set_collision_mask(_collision_mask); } - block->set_visible(true); - block->set_collision_enabled(true); + block->set_visible(block->mesh_viewers.get() > 0); + block->set_collision_enabled(gen_collisions); block->set_parent_visible(is_visible()); block->set_parent_transform(get_global_transform()); // TODO We don't set MESH_UP_TO_DATE anywhere, but it seems to work? @@ -2165,6 +2192,30 @@ void VoxelTerrain::process_debug_draw() { } } + if (debug_get_draw_flag(DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS)) { + const int mesh_block_size = get_mesh_block_size(); + _mesh_map.for_each_block([&parent_transform, &dr, mesh_block_size](const VoxelMeshBlockVT &block) { + Color8 color; + const bool visual = block.is_visible(); + const bool collision = block.is_collision_enabled(); + if (visual && collision) { + color = Color8(255, 255, 0, 255); + } else if (visual) { + color = Color8(0, 255, 0, 255); + } else if (collision) { + color = Color8(255, 0, 0, 255); + } else { + return; + } + const Vector3i voxel_pos = block.position * mesh_block_size; + const Transform3D local_transform( + Basis().scaled(Vector3(mesh_block_size, mesh_block_size, mesh_block_size)), voxel_pos + ); + const Transform3D t = parent_transform * local_transform; + dr.draw_box(t, color); + }); + } + dr.end(); } @@ -2426,6 +2477,7 @@ void VoxelTerrain::_bind_methods() { ); ADD_DEBUG_DRAW_FLAG("debug_draw_volume_bounds", DEBUG_DRAW_VOLUME_BOUNDS); + ADD_DEBUG_DRAW_FLAG("debug_draw_visual_and_collision_blocks", DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS); ADD_PROPERTY( PropertyInfo(Variant::BOOL, "debug_draw_shadow_occluders", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), diff --git a/terrain/fixed_lod/voxel_terrain.h b/terrain/fixed_lod/voxel_terrain.h index 13c63ca69..8e504288a 100644 --- a/terrain/fixed_lod/voxel_terrain.h +++ b/terrain/fixed_lod/voxel_terrain.h @@ -151,8 +151,9 @@ class VoxelTerrain : public VoxelNode { enum DebugDrawFlag { DEBUG_DRAW_VOLUME_BOUNDS = 0, + DEBUG_DRAW_VISUAL_AND_COLLISION_BLOCKS = 1, - DEBUG_DRAW_FLAGS_COUNT = 1 + DEBUG_DRAW_FLAGS_COUNT = 2 }; void debug_set_draw_enabled(bool enabled); diff --git a/terrain/instancing/voxel_instance_generator.cpp b/terrain/instancing/voxel_instance_generator.cpp index 0be433515..19b38b69b 100644 --- a/terrain/instancing/voxel_instance_generator.cpp +++ b/terrain/instancing/voxel_instance_generator.cpp @@ -4,11 +4,16 @@ #include "../../util/godot/classes/array_mesh.h" #include "../../util/godot/classes/engine.h" #include "../../util/godot/core/array.h" +#include "../../util/godot/core/packed_arrays.h" #include "../../util/godot/core/random_pcg.h" #include "../../util/math/conv.h" #include "../../util/math/triangle.h" #include "../../util/profiling.h" +#if defined(_MSC_VER) +#pragma warning(disable : 4701) // Potentially uninitialized local variable used. +#endif + namespace zylann::voxel { namespace { @@ -51,9 +56,10 @@ void VoxelInstanceGenerator::generate_transforms( PackedVector3Array normals = surface_arrays[ArrayMesh::ARRAY_NORMAL]; ERR_FAIL_COND(normals.size() == 0); - PackedInt32Array indices = surface_arrays[ArrayMesh::ARRAY_INDEX]; - ERR_FAIL_COND(indices.size() == 0); - ERR_FAIL_COND(indices.size() % 3 != 0); + PackedInt32Array mesh_indices_pba = surface_arrays[ArrayMesh::ARRAY_INDEX]; + ERR_FAIL_COND(mesh_indices_pba.size() == 0); + ERR_FAIL_COND(mesh_indices_pba.size() % 3 != 0); + Span mesh_indices = to_span(mesh_indices_pba); const uint32_t block_pos_hash = Vector3iHasher::hash(grid_position); @@ -71,6 +77,7 @@ void VoxelInstanceGenerator::generate_transforms( // TODO Candidates for temp allocator static thread_local StdVector g_vertex_cache; static thread_local StdVector g_normal_cache; + static thread_local StdVector g_index_cache; static thread_local StdVector g_noise_cache; // static thread_local StdVector g_noise_graph_output_cache; static thread_local StdVector g_noise_graph_x_cache; @@ -79,9 +86,16 @@ void VoxelInstanceGenerator::generate_transforms( StdVector &vertex_cache = g_vertex_cache; StdVector &normal_cache = g_normal_cache; + StdVector &index_cache = g_index_cache; vertex_cache.clear(); normal_cache.clear(); + index_cache.clear(); + + const bool voxel_material_filter_enabled = _voxel_material_filter_enabled; + const uint32_t voxel_material_filter_mask = _voxel_material_filter_mask; + + const bool index_cache_used = voxel_material_filter_enabled; // Pick random points { @@ -117,13 +131,16 @@ void VoxelInstanceGenerator::generate_transforms( } vertex_cache.push_back(pos); normal_cache.push_back(to_vec3f(normals[i])); + if (index_cache_used) { + index_cache.push_back(i); + } } } break; case EMIT_FROM_FACES_FAST: { // PoolIntArray::Read indices_r = indices.read(); - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; // Assumes triangles are all roughly under the same size, and Transvoxel ones do (when not simplified), // so we can use number of triangles as a metric proportional to the number of instances @@ -136,9 +153,9 @@ void VoxelInstanceGenerator::generate_transforms( // Pick a random triangle const uint32_t ii = (pcg0.rand() % triangle_count) * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3 &pa = vertices[ia]; const Vector3 &pb = vertices[ib]; @@ -160,6 +177,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache[instance_index] = to_vec3f(p); normal_cache[instance_index] = to_vec3f(n); + + if (index_cache_used) { + index_cache.push_back(ii); + } } } break; @@ -167,7 +188,7 @@ void VoxelInstanceGenerator::generate_transforms( case EMIT_FROM_FACES: { // PackedInt32Array::Read indices_r = indices.read(); - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; // static thread_local StdVector g_area_cache; // StdVector &area_cache = g_area_cache; @@ -188,9 +209,9 @@ void VoxelInstanceGenerator::generate_transforms( for (int triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { const uint32_t ii = triangle_index * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3f &pa = to_vec3f(vertices[ia]); const Vector3f &pb = to_vec3f(vertices[ib]); @@ -222,6 +243,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache.push_back(rp); normal_cache.push_back(rn); + + if (index_cache_used) { + index_cache.push_back(ii); + } } area_accumulator -= count_in_triangle * inv_density; @@ -232,7 +257,7 @@ void VoxelInstanceGenerator::generate_transforms( case EMIT_ONE_PER_TRIANGLE: { // Density has no effect here. - const int triangle_count = indices.size() / 3; + const int triangle_count = mesh_indices.size() / 3; const float one_third = 1.f / 3.f; const float triangle_area_threshold = math::squared(1 << lod_index) * _triangle_area_threshold_lod0; @@ -242,9 +267,9 @@ void VoxelInstanceGenerator::generate_transforms( for (int triangle_index = 0; triangle_index < triangle_count; ++triangle_index) { const uint32_t ii = triangle_index * 3; - const int ia = indices[ii]; - const int ib = indices[ii + 1]; - const int ic = indices[ii + 2]; + const int ia = mesh_indices[ii]; + const int ib = mesh_indices[ii + 1]; + const int ic = mesh_indices[ii + 2]; const Vector3f &pa = to_vec3f(vertices[ia]); const Vector3f &pb = to_vec3f(vertices[ib]); @@ -284,6 +309,10 @@ void VoxelInstanceGenerator::generate_transforms( vertex_cache.push_back(p); normal_cache.push_back(n); } + + if (index_cache_used) { + index_cache.push_back(ii); + } } } break; @@ -304,11 +333,108 @@ void VoxelInstanceGenerator::generate_transforms( if ((octant_mask & (1 << octant_index)) == 0) { unordered_remove(vertex_cache, i); unordered_remove(normal_cache, i); + if (index_cache_used) { + unordered_remove(index_cache, i); + } --i; } } } + // Filter out by voxel materials + // Assuming 4x8-bit weights and 4x8-bit indices as used in VoxelMesherTransvoxel for now, but might have other + // formats in the future + if (voxel_material_filter_enabled && surface_arrays.size() >= Mesh::ARRAY_CUSTOM1) { + ZN_PROFILE_SCOPE(); + + struct Attrib { + uint32_t packed_indices; + uint32_t packed_weights; + }; + const PackedFloat32Array src_vertex_data = surface_arrays[Mesh::ARRAY_CUSTOM1]; + const Span attrib_array = to_span(src_vertex_data).reinterpret_cast_to(); + + const unsigned int weight_threshold = 128; + + struct L { + static inline bool vertex_contains_enough_material( + const Attrib attrib, + const unsigned int threshold, + const uint32_t material_mask + ) { + for (unsigned int i = 0; i < 4; ++i) { + const unsigned int vmat_weight = (attrib.packed_weights >> (i * 8)) & 0xff; + if (vmat_weight > threshold) { + const unsigned int vmat_index = (attrib.packed_indices >> (i * 8)) & 0xff; + if (((1 << vmat_index) & material_mask) != 0) { + return true; + } + } + } + return false; + } + + static inline bool triangle_contains_enough_material( + const Span attrib_array, + const Span mesh_indices, + const unsigned int ii0, + const unsigned int threshold, + const uint32_t material_mask + ) { + const uint32_t vi0 = mesh_indices[ii0 + 0]; + const uint32_t vi1 = mesh_indices[ii0 + 1]; + const uint32_t vi2 = mesh_indices[ii0 + 2]; + + return vertex_contains_enough_material(attrib_array[vi0], threshold, material_mask) || + vertex_contains_enough_material(attrib_array[vi1], threshold, material_mask) || + vertex_contains_enough_material(attrib_array[vi2], threshold, material_mask); + } + }; + + switch (_emit_mode) { + case EMIT_FROM_VERTICES: { + // Indices are vertices + for (unsigned int instance_index = 0; instance_index < vertex_cache.size();) { + const unsigned int vi = index_cache[instance_index]; + const Attrib attrib = attrib_array[vi]; + if (L::vertex_contains_enough_material(attrib, weight_threshold, voxel_material_filter_mask)) { + instance_index += 1; + } else { + // Remove instance + unordered_remove(vertex_cache, instance_index); + unordered_remove(normal_cache, instance_index); + unordered_remove(index_cache, instance_index); + } + } + } break; + case EMIT_FROM_FACES: + case EMIT_FROM_FACES_FAST: + case EMIT_ONE_PER_TRIANGLE: { + // Indices are the index in the index buffer of the first vertex of the triangle in which the instance + // was spawned in + for (unsigned int instance_index = 0; instance_index < vertex_cache.size();) { + const uint32_t ii0 = index_cache[instance_index]; + if (L::triangle_contains_enough_material( + attrib_array, mesh_indices, ii0, weight_threshold, voxel_material_filter_mask + )) { + instance_index += 1; + } else { + // Remove instance + unordered_remove(vertex_cache, instance_index); + unordered_remove(normal_cache, instance_index); + unordered_remove(index_cache, instance_index); + } + } + } break; + default: + ZN_PRINT_ERROR_ONCE("Unhandled emit mode"); + break; + } + + // Index cache has no use yet after this. To detect future mistakes if any, make it obvious by clearing it + index_cache.clear(); + } + // Position of the block relative to the instancer node. // Use full-precision here because we deal with potentially large coordinates const Vector3 mesh_block_origin_d = grid_position * block_size; @@ -471,6 +597,10 @@ void VoxelInstanceGenerator::generate_transforms( unordered_remove(vertex_cache, i); unordered_remove(normal_cache, i); unordered_remove(noise_cache, i); + // We don't use the index cache after this... for now + // if (index_cache_used) { + // unordered_remove(index_cache, i); + // } --i; } } @@ -940,6 +1070,63 @@ float VoxelInstanceGenerator::get_noise_on_scale() const { return _noise_on_scale; } +void VoxelInstanceGenerator::set_voxel_material_filter_enabled(bool enabled) { + if (enabled == _voxel_material_filter_enabled) { + return; + } + _voxel_material_filter_enabled = enabled; + emit_changed(); +} + +bool VoxelInstanceGenerator::is_voxel_material_filter_enabled() const { + return _voxel_material_filter_enabled; +} + +void VoxelInstanceGenerator::set_voxel_material_filter_mask(const uint32_t mask) { + if (mask == _voxel_material_filter_mask) { + return; + } + _voxel_material_filter_mask = mask; + emit_changed(); +} + +uint32_t VoxelInstanceGenerator::get_voxel_material_filter_mask() const { + return _voxel_material_filter_mask; +} + +PackedInt32Array VoxelInstanceGenerator::_b_get_voxel_material_filter_array() const { + const unsigned int bit_count = sizeof(_voxel_material_filter_mask) * 8; + PackedInt32Array array; + for (unsigned int i = 0; i < bit_count; ++i) { + if ((_voxel_material_filter_mask & (1 << i)) != 0) { + array.append(i); + } + } + return array; +} + +void VoxelInstanceGenerator::_b_set_voxel_material_filter_array(PackedInt32Array material_indices) { + const unsigned int bit_count = sizeof(_voxel_material_filter_mask) * 8; + uint32_t mask = 0; + Span indices = to_span(material_indices); + for (const int32_t si : indices) { + ZN_ASSERT_CONTINUE(si >= 0); + const unsigned int i = static_cast(si); + ZN_ASSERT_CONTINUE(i < bit_count); + mask |= (1 << i); + } +#if TOOLS_ENABLED + // Only warn when running the game, because when users add new items to the array in the editor, + // it is likely to have duplicates temporarily, until they set the desired values. + if (!Engine::get_singleton()->is_editor_hint()) { + if (has_duplicate(indices)) { + ZN_PRINT_WARNING("The array of indices contains a duplicate."); + } + } +#endif + set_voxel_material_filter_mask(mask); +} + void VoxelInstanceGenerator::_on_noise_changed() { emit_changed(); } @@ -1075,6 +1262,19 @@ void VoxelInstanceGenerator::_bind_methods() { ClassDB::bind_method(D_METHOD("set_noise_on_scale", "amount"), &Self::set_noise_on_scale); ClassDB::bind_method(D_METHOD("get_noise_on_scale"), &Self::get_noise_on_scale); + ClassDB::bind_method( + D_METHOD("set_voxel_texture_filter_enabled", "enabled"), &Self::set_voxel_material_filter_enabled + ); + ClassDB::bind_method(D_METHOD("is_voxel_texture_filter_enabled"), &Self::is_voxel_material_filter_enabled); + + ClassDB::bind_method(D_METHOD("set_voxel_texture_filter_mask", "mask"), &Self::set_voxel_material_filter_mask); + ClassDB::bind_method(D_METHOD("get_voxel_texture_filter_mask"), &Self::get_voxel_material_filter_mask); + + ClassDB::bind_method( + D_METHOD("set_voxel_texture_filter_array", "texture_indices"), &Self::_b_set_voxel_material_filter_array + ); + ClassDB::bind_method(D_METHOD("get_voxel_texture_filter_array"), &Self::_b_get_voxel_material_filter_array); + ADD_GROUP("Emission", ""); ADD_PROPERTY( @@ -1176,6 +1376,24 @@ void VoxelInstanceGenerator::_bind_methods() { "get_noise_on_scale" ); + ADD_GROUP("Filtering", ""); + + ADD_PROPERTY( + PropertyInfo(Variant::BOOL, "voxel_texture_filter_enabled"), + "set_voxel_texture_filter_enabled", + "is_voxel_texture_filter_enabled" + ); + // ADD_PROPERTY( + // PropertyInfo(Variant::INT, "voxel_texture_filter_mask"), + // "set_voxel_texture_filter_mask", + // "get_voxel_texture_filter_mask" + // ); + ADD_PROPERTY( + PropertyInfo(Variant::PACKED_INT32_ARRAY, "voxel_texture_filter_array"), + "set_voxel_texture_filter_array", + "get_voxel_texture_filter_array" + ); + BIND_ENUM_CONSTANT(EMIT_FROM_VERTICES); BIND_ENUM_CONSTANT(EMIT_FROM_FACES_FAST); BIND_ENUM_CONSTANT(EMIT_FROM_FACES); diff --git a/terrain/instancing/voxel_instance_generator.h b/terrain/instancing/voxel_instance_generator.h index 316b44f6f..8a385eb52 100644 --- a/terrain/instancing/voxel_instance_generator.h +++ b/terrain/instancing/voxel_instance_generator.h @@ -124,6 +124,12 @@ class VoxelInstanceGenerator : public Resource { void set_noise_on_scale(float amount); float get_noise_on_scale() const; + void set_voxel_material_filter_enabled(bool enabled); + bool is_voxel_material_filter_enabled() const; + + void set_voxel_material_filter_mask(const uint32_t mask); + uint32_t get_voxel_material_filter_mask() const; + static inline int get_octant_index(const Vector3f pos, float half_block_size) { return get_octant_index(pos.x > half_block_size, pos.y > half_block_size, pos.z > half_block_size); } @@ -141,6 +147,9 @@ class VoxelInstanceGenerator : public Resource { void _on_noise_changed(); void _on_noise_graph_changed(); + PackedInt32Array _b_get_voxel_material_filter_array() const; + void _b_set_voxel_material_filter_array(PackedInt32Array material_indices); + static void _bind_methods(); float _density = 0.1f; @@ -161,6 +170,8 @@ class VoxelInstanceGenerator : public Resource { Ref _noise; Dimension _noise_dimension = DIMENSION_3D; float _noise_on_scale = 0.f; + bool _voxel_material_filter_enabled = false; + uint32_t _voxel_material_filter_mask = 1; // TODO Protect noise and noise graph members from multithreaded access diff --git a/terrain/variable_lod/voxel_lod_terrain.cpp b/terrain/variable_lod/voxel_lod_terrain.cpp index 5644598e6..b06af6d9a 100644 --- a/terrain/variable_lod/voxel_lod_terrain.cpp +++ b/terrain/variable_lod/voxel_lod_terrain.cpp @@ -1737,6 +1737,32 @@ void VoxelLodTerrain::apply_data_block_response(VoxelEngine::BlockDataOutput &ob // } } +inline void set_block_collision_shape( + const VoxelLodTerrain &terrain, + VoxelMeshBlockVLT &block, + Ref shape, + const uint64_t now +) { + bool debug_collisions = false; + if (terrain.is_inside_tree()) { + const SceneTree *scene_tree = terrain.get_tree(); +#if DEBUG_ENABLED + if (shape.is_valid()) { + const Color debug_color = scene_tree->get_debug_collisions_color(); + shape->set_debug_color(debug_color); + } +#endif + debug_collisions = scene_tree->is_debugging_collisions_hint(); + } + + block.set_collision_shape(shape, debug_collisions, &terrain, terrain.get_collision_margin()); + + block.set_collision_layer(terrain.get_collision_layer()); + block.set_collision_mask(terrain.get_collision_mask()); + block.last_collider_update_time = now; + block.deferred_collider_data.reset(); +} + void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) { // The following is done on the main thread because Godot doesn't really support everything done here. // Building meshes can be done in the threaded task when using Vulkan, but not OpenGL. @@ -2017,14 +2043,8 @@ void VoxelLodTerrain::apply_mesh_update(VoxelEngine::BlockMeshOutput &ob) { static_cast(now - block->last_collider_update_time) > _collision_update_delay) { ZN_ASSERT(_mesher.is_valid()); Ref collision_shape = make_collision_shape_from_mesher_output(ob.surfaces, **_mesher); - const bool debug_collisions = is_inside_tree() ? get_tree()->is_debugging_collisions_hint() : false; - block->set_collision_shape(collision_shape, debug_collisions, this, _collision_margin); - - block->set_collision_layer(_collision_layer); - block->set_collision_mask(_collision_mask); + set_block_collision_shape(*this, *block, collision_shape, now); block->set_collision_enabled(collision_active); - block->last_collider_update_time = now; - block->deferred_collider_data.reset(); } else { if (block->deferred_collider_data == nullptr) { @@ -2270,13 +2290,7 @@ void VoxelLodTerrain::process_deferred_collision_updates(uint32_t timeout_msec) make_collision_shape_from_mesher_output(*block->deferred_collider_data, **_mesher); } - block->set_collision_shape( - collision_shape, get_tree()->is_debugging_collisions_hint(), this, _collision_margin - ); - block->set_collision_layer(_collision_layer); - block->set_collision_mask(_collision_mask); - block->last_collider_update_time = now; - block->deferred_collider_data.reset(); + set_block_collision_shape(*this, *block, collision_shape, now); unordered_remove(deferred_collision_updates, i); --i; @@ -2789,6 +2803,11 @@ void VoxelLodTerrain::get_configuration_warnings(PackedStringArray &warnings) co warnings.append(String("`use_gpu_generation` is enabled, but {0} does not support running on the GPU.") .format(varray(generator->get_class()))); } + if (!VoxelEngine::get_singleton().has_rendering_device()) { + warnings.append(String("`use_gpu_generation` is enabled, but the selected renderer does not support the " + "RenderingDevice API ({0}).") + .format(varray(ZN_CLASS_NAME_C(VoxelLodTerrain), get_current_rendering_method()))); + } } if (mesher.is_valid()) { @@ -3465,7 +3484,7 @@ Array VoxelLodTerrain::_b_debug_print_sdf_top_down(Vector3i center, Vector3i ext for (unsigned int lod_index = 0; lod_index < lod_count; ++lod_index) { const Box3i world_box = Box3i::from_center_extents(center >> lod_index, extents >> lod_index); - if (Vector3iUtil::get_volume(world_box.size) == 0) { + if (Vector3iUtil::get_volume_u64(world_box.size) == 0) { continue; } diff --git a/terrain/variable_lod/voxel_lod_terrain_update_clipbox_streaming.cpp b/terrain/variable_lod/voxel_lod_terrain_update_clipbox_streaming.cpp index 422dcfdeb..4569e7305 100644 --- a/terrain/variable_lod/voxel_lod_terrain_update_clipbox_streaming.cpp +++ b/terrain/variable_lod/voxel_lod_terrain_update_clipbox_streaming.cpp @@ -266,8 +266,6 @@ void process_viewers( const int lod_mesh_block_size_po2 = volume_settings.mesh_block_size_po2 + lod_index; const int lod_mesh_block_size = 1 << lod_mesh_block_size_po2; - const Box3i volume_bounds_in_mesh_blocks = volume_bounds_in_voxels.downscaled(lod_mesh_block_size); - const Vector3i ld = get_relative_lod_distance_in_chunks( lod_index, lod_count, @@ -301,12 +299,19 @@ void process_viewers( new_mesh_box = enforce_neighboring_rule(new_mesh_box, child_box, even_coordinates_required); } - // Clip last - new_mesh_box.clip(volume_bounds_in_mesh_blocks); - paired_viewer.state.mesh_box_per_lod[lod_index] = new_mesh_box; } + // Clip all mesh boxes in a second pass, because `enforce_neighboring_rule` depends on the child LOD box + for (unsigned int lod_index = 0; lod_index < lod_count; ++lod_index) { + const int lod_mesh_block_size_po2 = volume_settings.mesh_block_size_po2 + lod_index; + const int lod_mesh_block_size = 1 << lod_mesh_block_size_po2; + const Box3i volume_bounds_in_mesh_blocks = volume_bounds_in_voxels.downscaled(lod_mesh_block_size); + + Box3i &box = paired_viewer.state.mesh_box_per_lod[lod_index]; + box.clip(volume_bounds_in_mesh_blocks); + } + // TODO We should have a flag server side to force data boxes to be based on mesh boxes, even though the // server might not actually need meshes. That would help the server to provide data chunks to clients, // which need them for visual meshes diff --git a/terrain/variable_lod/voxel_lod_terrain_update_task.cpp b/terrain/variable_lod/voxel_lod_terrain_update_task.cpp index 401c8b763..378f52977 100644 --- a/terrain/variable_lod/voxel_lod_terrain_update_task.cpp +++ b/terrain/variable_lod/voxel_lod_terrain_update_task.cpp @@ -46,8 +46,10 @@ void init_sparse_octree_priority_dependency( // // Distance beyond which it is safe to drop a block without risking to block LOD subdivision. // This does not depend on viewer's view distance, but on LOD precision instead. // TODO Should `data_block_size` be used here? Should it be mesh_block_size instead? - dep.drop_distance_squared = math::squared(2.f * transformed_block_radius * - VoxelEngine::get_octree_lod_block_region_extent(octree_lod_distance, data_block_size)); + dep.drop_distance_squared = math::squared( + 2.f * transformed_block_radius * + VoxelEngine::get_octree_lod_block_region_extent(octree_lod_distance, data_block_size) + ); } // This is only if we want to cache voxel data @@ -85,8 +87,15 @@ void request_block_generate( // params.use_gpu = settings.generator_use_gpu; params.cancellation_token = cancellation_token; - init_sparse_octree_priority_dependency(params.priority_dependency, block_pos, lod_index, data_block_size, - shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + params.priority_dependency, + block_pos, + lod_index, + data_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); IThreadedTask *task = stream_dependency->generator->create_block_task(params); @@ -130,20 +139,50 @@ void request_block_load( // if (stream_dependency->stream.is_valid()) { PriorityDependency priority_dependency; - init_sparse_octree_priority_dependency(priority_dependency, block_pos, lod_index, data_block_size, - shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + priority_dependency, + block_pos, + lod_index, + data_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); const bool request_instances = false; - LoadBlockDataTask *task = ZN_NEW(LoadBlockDataTask(volume_id, block_pos, lod_index, data_block_size, - request_instances, stream_dependency, priority_dependency, settings.cache_generated_blocks, - settings.generator_use_gpu, data, cancellation_token)); + LoadBlockDataTask *task = ZN_NEW(LoadBlockDataTask( + volume_id, + block_pos, + lod_index, + data_block_size, + request_instances, + stream_dependency, + priority_dependency, + settings.cache_generated_blocks, + settings.generator_use_gpu, + data, + cancellation_token + )); task_scheduler.push_io_task(task); } else if (settings.cache_generated_blocks) { // Directly generate the block without checking the stream. - request_block_generate(volume_id, data_block_size, stream_dependency, data, block_pos, lod_index, - shared_viewers_data, volume_transform, settings, nullptr, true, task_scheduler, cancellation_token); + request_block_generate( + volume_id, + data_block_size, + stream_dependency, + data, + block_pos, + lod_index, + shared_viewers_data, + volume_transform, + settings, + nullptr, + true, + task_scheduler, + cancellation_token + ); } else { ZN_PRINT_WARNING("Requesting a block load when it should not have been necessary"); @@ -308,8 +347,8 @@ void send_mesh_requests( // // Don't update a detail texture if one update is already processing if (settings.detail_texture_settings.enabled && - lod_index >= settings.detail_texture_settings.begin_lod_index && - mesh_block.detail_texture_state != VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING) { + lod_index >= settings.detail_texture_settings.begin_lod_index && + mesh_block.detail_texture_state != VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING) { mesh_block.detail_texture_state = VoxelLodTerrainUpdateData::DETAIL_TEXTURE_PENDING; task->require_detail_texture = true; } @@ -322,13 +361,20 @@ void send_mesh_requests( // // The array also implicitly encodes block position due to the convention being used, // so there is no need to also include positions in the request data.get_blocks_with_voxel_data(data_box, lod_index, to_span(task->blocks)); - task->blocks_count = Vector3iUtil::get_volume(data_box.size); + task->blocks_count = Vector3iUtil::get_volume_u64(data_box.size); // TODO There is inconsistency with coordinates sent to this function. // Sometimes we send data block coordinates, sometimes we send mesh block coordinates. They aren't always // the same, it might cause issues in priority sorting? - init_sparse_octree_priority_dependency(task->priority_dependency, task->mesh_block_position, - task->lod_index, mesh_block_size, shared_viewers_data, volume_transform, settings.lod_distance); + init_sparse_octree_priority_dependency( + task->priority_dependency, + task->mesh_block_position, + task->lod_index, + mesh_block_size, + shared_viewers_data, + volume_transform, + settings.lod_distance + ); task_scheduler.push_main_task(task); @@ -362,7 +408,8 @@ std::shared_ptr preload_boxes_async( // VoxelData &data = *data_ptr; ZN_ASSERT_RETURN_V_MSG( - data.is_streaming_enabled() == false, nullptr, "This function can only be used in full load mode"); + data.is_streaming_enabled() == false, nullptr, "This function can only be used in full load mode" + ); struct TaskArguments { Vector3i block_pos; @@ -417,9 +464,12 @@ std::shared_ptr preload_boxes_async( // // This may first run the generation tasks, and then the edits tracker = make_shared_instance( - todo.size(), next_tasks, [](Span p_next_tasks) { + todo.size(), + next_tasks, + [](Span p_next_tasks) { // VoxelEngine::get_singleton().push_async_tasks(p_next_tasks); - }); + } + ); for (unsigned int i = 0; i < todo.size(); ++i) { const TaskArguments args = todo[i]; @@ -480,8 +530,9 @@ void process_async_edits( // boxes_to_preload.push_back(edit.box); tasks_to_schedule.push_back(edit.task); - state.running_async_edits.push_back( - VoxelLodTerrainUpdateData::RunningAsyncEdit{ edit.task_tracker, edit.box }); + state.running_async_edits.push_back( // + VoxelLodTerrainUpdateData::RunningAsyncEdit{ edit.task_tracker, edit.box } + ); } if (boxes_to_preload.size() > 0) { @@ -519,7 +570,7 @@ void process_changed_generated_areas( // VoxelLodTerrainUpdateData::Lod &lod = state.lods[lod_index]; for (auto box_it = state.changed_generated_areas.begin(); box_it != state.changed_generated_areas.end(); - ++box_it) { + ++box_it) { const Box3i &voxel_box = *box_it; const Box3i bbox = voxel_box.padded(1).downscaled(mesh_block_size << lod_index); @@ -530,8 +581,12 @@ void process_changed_generated_areas( // bbox.for_each_cell_zxy([&lod](const Vector3i bpos) { auto block_it = lod.mesh_map_state.map.find(bpos); if (block_it != lod.mesh_map_state.map.end()) { - VoxelLodTerrainUpdateTask::schedule_mesh_update(block_it->second, bpos, - lod.mesh_blocks_pending_update, block_it->second.mesh_viewers.get() > 0); + VoxelLodTerrainUpdateTask::schedule_mesh_update( + block_it->second, + bpos, + lod.mesh_blocks_pending_update, + block_it->second.mesh_viewers.get() > 0 + ); } }); } @@ -703,7 +758,7 @@ uint8_t VoxelLodTerrainUpdateTask::get_transition_mask( // auto lower_neighbor_block_it = lower_lod.mesh_map_state.map.find(lower_neighbor_pos); if (lower_neighbor_block_it != lower_lod.mesh_map_state.map.end() && - lower_neighbor_block_it->second.visual_active) { + lower_neighbor_block_it->second.visual_active) { // The block has a visible neighbor of lower LOD transition_mask |= dir_mask; continue; @@ -727,7 +782,7 @@ uint8_t VoxelLodTerrainUpdateTask::get_transition_mask( // auto upper_neighbor_block_it = upper_lod.mesh_map_state.map.find(upper_neighbor_pos); if (upper_neighbor_block_it == upper_lod.mesh_map_state.map.end() || - upper_neighbor_block_it->second.visual_active == false) { + upper_neighbor_block_it->second.visual_active == false) { // The block has no visible neighbor yet. World border? Assume lower LOD. transition_mask |= dir_mask; } @@ -779,8 +834,9 @@ void update_transition_masks( // if (recomputed_mask != it->second.transition_mask) { mesh_block.transition_mask = recomputed_mask; - lod.mesh_blocks_to_update_transitions.push_back( - VoxelLodTerrainUpdateData::TransitionUpdate{ it->first, recomputed_mask }); + lod.mesh_blocks_to_update_transitions.push_back( // + VoxelLodTerrainUpdateData::TransitionUpdate{ it->first, recomputed_mask } + ); } } } diff --git a/terrain/voxel_a_star_grid_3d.cpp b/terrain/voxel_a_star_grid_3d.cpp index fb4844e6e..f5996bb7a 100644 --- a/terrain/voxel_a_star_grid_3d.cpp +++ b/terrain/voxel_a_star_grid_3d.cpp @@ -3,6 +3,7 @@ // #include "../util/string/format.h" #include "../constants/voxel_string_names.h" #include "../util/math/conv.h" +#include "../util/string/format.h" namespace zylann::voxel { @@ -122,10 +123,18 @@ void VoxelAStarGrid3D::check_params(Vector3i from_position, Vector3i to_position ZN_PRINT_WARNING("The region is empty or not defined, no path will be found"); } if (!get_region().contains(from_position)) { - ZN_PRINT_WARNING("The current region does not contain the source position, no path will be found"); + ZN_PRINT_WARNING( + format("The current region {} does not contain the source position {}, no path will be found", + get_region(), + from_position) + ); } - if (!get_region().contains(from_position)) { - ZN_PRINT_WARNING("The current region does not contain the destination, no path will be found"); + if (!get_region().contains(to_position)) { + ZN_PRINT_WARNING( + format("The current region {} does not contain the destination {}, no path will be found", + get_region(), + to_position) + ); } } #endif @@ -230,17 +239,20 @@ void VoxelAStarGrid3D::_bind_methods() { ClassDB::bind_method(D_METHOD("find_path", "from_position", "to_position"), &VoxelAStarGrid3D::find_path); ClassDB::bind_method( - D_METHOD("find_path_async", "from_position", "to_position"), &VoxelAStarGrid3D::find_path_async); + D_METHOD("find_path_async", "from_position", "to_position"), &VoxelAStarGrid3D::find_path_async + ); ClassDB::bind_method(D_METHOD("is_running_async"), &VoxelAStarGrid3D::is_running_async); ClassDB::bind_method(D_METHOD("debug_get_visited_positions"), &VoxelAStarGrid3D::debug_get_visited_positions); // Internal ClassDB::bind_method( - D_METHOD("_on_async_search_completed", "path"), &VoxelAStarGrid3D::_b_on_async_search_completed); + D_METHOD("_on_async_search_completed", "path"), &VoxelAStarGrid3D::_b_on_async_search_completed + ); ADD_SIGNAL(MethodInfo( - "async_search_completed", PropertyInfo(Variant::ARRAY, "path", PROPERTY_HINT_ARRAY_TYPE, "Vector3i"))); + "async_search_completed", PropertyInfo(Variant::ARRAY, "path", PROPERTY_HINT_ARRAY_TYPE, "Vector3i") + )); } } // namespace zylann::voxel diff --git a/terrain/voxel_mesh_block.cpp b/terrain/voxel_mesh_block.cpp index 0607a1260..dfe0e8330 100644 --- a/terrain/voxel_mesh_block.cpp +++ b/terrain/voxel_mesh_block.cpp @@ -48,8 +48,12 @@ void VoxelMeshBlock::set_render_layers_mask(int mask) { } } -void VoxelMeshBlock::set_mesh(Ref mesh, GeometryInstance3D::GIMode gi_mode, - RenderingServer::ShadowCastingSetting shadow_setting, int render_layers_mask) { +void VoxelMeshBlock::set_mesh( + Ref mesh, + GeometryInstance3D::GIMode gi_mode, + RenderingServer::ShadowCastingSetting shadow_setting, + int render_layers_mask +) { // TODO Don't add mesh instance to the world if it's not visible. // I suspect Godot is trying to include invisible mesh instances into the culling process, // which is killing performance when LOD is used (i.e many meshes are in pool but hidden) @@ -139,7 +143,7 @@ void VoxelMeshBlock::set_parent_transform(const Transform3D &parent_transform) { } } -void VoxelMeshBlock::set_collision_shape(Ref shape, bool debug_collision, Node3D *node, float margin) { +void VoxelMeshBlock::set_collision_shape(Ref shape, bool debug_collision, const Node3D *node, float margin) { ERR_FAIL_COND(node == nullptr); ERR_FAIL_COND_MSG(node->get_world_3d() != _world, "Physics body and attached node must be from the same world"); @@ -211,7 +215,9 @@ bool VoxelMeshBlock::is_collision_enabled() const { } Ref make_collision_shape_from_mesher_output( - const VoxelMesher::Output &mesher_output, const VoxelMesher &mesher) { + const VoxelMesher::Output &mesher_output, + const VoxelMesher &mesher +) { using namespace zylann::godot; Ref shape; @@ -221,13 +227,15 @@ Ref make_collision_shape_from_mesher_output( // Use a sub-region of the render mesh if (mesher_output.surfaces.size() > 0) { shape = create_concave_polygon_shape( - mesher_output.surfaces[0].arrays, mesher_output.collision_surface.submesh_index_end); + mesher_output.surfaces[0].arrays, mesher_output.collision_surface.submesh_index_end + ); } } else { // Use specialized collision mesh - shape = create_concave_polygon_shape(to_span(mesher_output.collision_surface.positions), - to_span(mesher_output.collision_surface.indices)); + shape = create_concave_polygon_shape( + to_span(mesher_output.collision_surface.positions), to_span(mesher_output.collision_surface.indices) + ); } } else { diff --git a/terrain/voxel_mesh_block.h b/terrain/voxel_mesh_block.h index c7c1c5d8e..9dd860d70 100644 --- a/terrain/voxel_mesh_block.h +++ b/terrain/voxel_mesh_block.h @@ -35,8 +35,12 @@ class VoxelMeshBlock : public NonCopyable { // Visuals - void set_mesh(Ref mesh, GeometryInstance3D::GIMode gi_mode, - RenderingServer::ShadowCastingSetting shadow_setting, int render_layers_mask); + void set_mesh( + Ref mesh, + GeometryInstance3D::GIMode gi_mode, + RenderingServer::ShadowCastingSetting shadow_setting, + int render_layers_mask + ); Ref get_mesh() const; bool has_mesh() const; void drop_mesh(); @@ -61,7 +65,7 @@ class VoxelMeshBlock : public NonCopyable { // Collisions - void set_collision_shape(Ref shape, bool debug_collision, Node3D *node, float margin); + void set_collision_shape(Ref shape, bool debug_collision, const Node3D *node, float margin); bool has_collision_shape() const; void set_collision_layer(int layer); void set_collision_mask(int mask); @@ -97,7 +101,9 @@ class VoxelMeshBlock : public NonCopyable { }; Ref make_collision_shape_from_mesher_output( - const VoxelMesher::Output &mesher_output, const VoxelMesher &mesher); + const VoxelMesher::Output &mesher_output, + const VoxelMesher &mesher +); } // namespace zylann::voxel diff --git a/tests/testing.cpp b/tests/testing.cpp index 38c9335f9..d6d91de4b 100644 --- a/tests/testing.cpp +++ b/tests/testing.cpp @@ -5,7 +5,7 @@ namespace zylann::testing { -constexpr char *DEFAULT_TEST_DATA_DIRECTORY = "zylann_testing_dir"; +const char *DEFAULT_TEST_DATA_DIRECTORY = "zylann_testing_dir"; bool create_empty_file(String fpath) { if (!FileAccess::exists(fpath)) { @@ -79,4 +79,4 @@ String TestDirectory::get_path() const { return DEFAULT_TEST_DATA_DIRECTORY; } -} //namespace zylann::testing +} // namespace zylann::testing diff --git a/tests/tests.cpp b/tests/tests.cpp index 8e530420a..fdd6fe744 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -8,6 +8,7 @@ #include "util/test_flat_map.h" #include "util/test_island_finder.h" #include "util/test_math_funcs.h" +#include "util/test_noise.h" #include "util/test_slot_map.h" #include "util/test_spatial_lock.h" #include "util/test_string_funcs.h" @@ -89,6 +90,7 @@ void run_voxel_tests() { VOXEL_TEST(test_voxel_graph_many_weight_outputs); VOXEL_TEST(test_voxel_graph_many_subdivisions); VOXEL_TEST(test_voxel_graph_non_square_image); + VOXEL_TEST(test_voxel_graph_empty_image); VOXEL_TEST(test_voxel_graph_4_default_weights); VOXEL_TEST(test_island_finder); VOXEL_TEST(test_unordered_remove_if); @@ -129,6 +131,7 @@ void run_voxel_tests() { VOXEL_TEST(test_voxel_stream_sqlite_basic); VOXEL_TEST(test_voxel_stream_sqlite_coordinate_format); VOXEL_TEST(test_sdf_hemisphere); + VOXEL_TEST(test_fnl_range); print_line("------------ Voxel tests end -------------"); } diff --git a/tests/util/test_island_finder.cpp b/tests/util/test_island_finder.cpp index a2b768538..5c8bccb76 100644 --- a/tests/util/test_island_finder.cpp +++ b/tests/util/test_island_finder.cpp @@ -39,10 +39,10 @@ void test_island_finder() { ; const Vector3i grid_size(5, 5, 5); - ZN_TEST_ASSERT(Vector3iUtil::get_volume(grid_size) == strlen(cdata) / 2); + ZN_TEST_ASSERT(Vector3iUtil::get_volume_u64(grid_size) == (strlen(cdata) / 2)); StdVector grid; - grid.resize(Vector3iUtil::get_volume(grid_size)); + grid.resize(Vector3iUtil::get_volume_u64(grid_size)); for (unsigned int i = 0; i < grid.size(); ++i) { const char c = cdata[i * 2]; if (c == 'X') { @@ -55,7 +55,7 @@ void test_island_finder() { } StdVector output; - output.resize(Vector3iUtil::get_volume(grid_size)); + output.resize(Vector3iUtil::get_volume_u64(grid_size)); unsigned int label_count; IslandFinder island_finder; @@ -66,7 +66,9 @@ void test_island_finder() { CRASH_COND(i >= grid.size()); return grid[i] == 1; }, - to_span(output), &label_count); + to_span(output), + &label_count + ); // unsigned int i = 0; // for (int z = 0; z < grid_size.z; ++z) { diff --git a/tests/util/test_noise.cpp b/tests/util/test_noise.cpp new file mode 100644 index 000000000..7e15872bc --- /dev/null +++ b/tests/util/test_noise.cpp @@ -0,0 +1,51 @@ +#include "test_noise.h" +#include "../../util/noise/fast_noise_lite/fast_noise_lite.h" +#include "../../util/noise/fast_noise_lite/fast_noise_lite_range.h" +#include "../testing.h" + +namespace zylann::tests { + +void test_fnl_range() { + Ref noise; + noise.instantiate(); + noise->set_noise_type(ZN_FastNoiseLite::TYPE_OPEN_SIMPLEX_2S); + noise->set_fractal_type(ZN_FastNoiseLite::FRACTAL_NONE); + // noise->set_fractal_type(ZN_FastNoiseLite::FRACTAL_FBM); + noise->set_fractal_octaves(1); + noise->set_fractal_lacunarity(2.0); + noise->set_fractal_gain(0.5); + noise->set_period(512); + noise->set_seed(0); + + const Vector3i min_pos(-1074, 1838, 5587); + // const Vector3i max_pos(-1073, 1839, 5588); + const Vector3i max_pos(-1058, 1854, 5603); + + const math::Interval x_range(min_pos.x, max_pos.x - 1); + const math::Interval y_range(min_pos.y, max_pos.y - 1); + const math::Interval z_range(min_pos.z, max_pos.z - 1); + + const math::Interval analytic_range = get_fnl_range_3d(**noise, x_range, y_range, z_range); + + math::Interval empiric_range; + bool first_value = true; + 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) { + const float n = noise->get_noise_3d(x, y, z); + if (first_value) { + empiric_range.min = n; + empiric_range.max = n; + first_value = false; + } else { + empiric_range.min = math::min(empiric_range.min, n); + empiric_range.max = math::max(empiric_range.max, n); + } + } + } + } + + ZN_TEST_ASSERT(analytic_range.contains(empiric_range)); +} + +} // namespace zylann::tests diff --git a/tests/util/test_noise.h b/tests/util/test_noise.h new file mode 100644 index 000000000..e30fdd25e --- /dev/null +++ b/tests/util/test_noise.h @@ -0,0 +1,10 @@ +#ifndef ZN_TESTS_NOISE_H +#define ZN_TESTS_NOISE_H + +namespace zylann::tests { + +void test_fnl_range(); + +} // namespace zylann::tests + +#endif // ZN_TESTS_NOISE_H diff --git a/tests/util/test_spatial_lock.cpp b/tests/util/test_spatial_lock.cpp index d40039567..92adde0b7 100644 --- a/tests/util/test_spatial_lock.cpp +++ b/tests/util/test_spatial_lock.cpp @@ -51,7 +51,8 @@ void test_spatial_lock_misc() { spatial_lock.unlock_write(box4); }, - &spatial_lock); + &spatial_lock + ); thread.wait_to_finish(); @@ -81,7 +82,7 @@ void test_spatial_lock_spam() { public: Map(Vector3i p_size) { _size = p_size; - _cells.resize(Vector3iUtil::get_volume(_size), 0); + _cells.resize(Vector3iUtil::get_volume_u64(_size), 0); } inline Vector3i get_size() const { @@ -145,14 +146,14 @@ void test_spatial_lock_spam() { StdVector &expected_values = reusable_vector; expected_values.clear(); - expected_values.reserve(Vector3iUtil::get_volume(box.size)); + expected_values.reserve(Vector3iUtil::get_volume_u64(box.size)); box.for_each_cell([&map, &expected_values](Vector3i pos) { // expected_values.push_back(map.at(pos)); }); - for (int i = 0; /* keep looping at least once */; ++i) { + while (true) { int j = 0; - box.for_each_cell([&map, &expected_values, &j](Vector3i pos) { // + box.for_each_cell([&map, &expected_values, &j](Vector3i pos) { // Cells must not change while we read them. ZN_TEST_ASSERT(expected_values[j] == map.at(pos)); // Note, iteration order is the same as when we cached expected values @@ -168,7 +169,7 @@ void test_spatial_lock_spam() { static Box3i make_random_box(const Vector3i area_size, RandomPCG &rng) { ZN_ASSERT(area_size.x > 0 && area_size.y > 0 && area_size.z > 0); return Box3i(Vector3i(rng.rand(area_size.x), rng.rand(area_size.y), rng.rand(area_size.z)), - Vector3i(1 + rng.rand(4), 1 + rng.rand(4), 1 + rng.rand(4))) + Vector3i(1 + rng.rand(4), 1 + rng.rand(4), 1 + rng.rand(4))) .clipped(Box3i(Vector3i(), area_size)); } @@ -294,14 +295,15 @@ void test_spatial_lock_dependent_map_chunks() { EventList &events; Task1(int p_sleep_amount_usec, Map &p_map, Vector2i p_column_pos, EventList &p_events) : - sleep_amount_usec(p_sleep_amount_usec), map(p_map), column_pos(p_column_pos), events(p_events) {} + sleep_amount_usec(p_sleep_amount_usec), column_pos(p_column_pos), map(p_map), events(p_events) {} void run(ThreadedTaskContext &ctx) override { ZN_PROFILE_SCOPE(); const BoxBounds3i box( // Vector3i(column_pos.x - 1, 0, column_pos.y - 1), // - Vector3i(column_pos.x + 1, 24, column_pos.y + 1) + Vector3i(1, 1, 1)); + Vector3i(column_pos.x + 1, 24, column_pos.y + 1) + Vector3i(1, 1, 1) + ); if (!map.spatial_lock.try_lock_write(box)) { ctx.status = ThreadedTaskContext::STATUS_POSTPONED; @@ -341,7 +343,7 @@ void test_spatial_lock_dependent_map_chunks() { EventList &events; Task2(int p_sleep_amount_usec, Map &p_map, Vector3i p_bpos, EventList &p_events) : - sleep_amount_usec(p_sleep_amount_usec), map(p_map), bpos0(p_bpos), events(p_events) {} + sleep_amount_usec(p_sleep_amount_usec), bpos0(p_bpos), map(p_map), events(p_events) {} void run(ThreadedTaskContext &ctx) override { ZN_PROFILE_SCOPE(); @@ -381,7 +383,8 @@ void test_spatial_lock_dependent_map_chunks() { const unsigned int hw_concurrency = Thread::get_hardware_concurrency(); if (hw_concurrency < test_thread_count) { ZN_PRINT_WARNING(format( - "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency)); + "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency + )); } ThreadedTaskRunner runner; @@ -399,14 +402,20 @@ void test_spatial_lock_dependent_map_chunks() { Vector2i column_pos; for (column_pos.y = 0; column_pos.y < MAP_SIZE; ++column_pos.y) { for (column_pos.x = 0; column_pos.x < MAP_SIZE; ++column_pos.x) { - Task1 *task = ZN_NEW(Task1(1000 + rng.rand(2000), map, column_pos, events)); - runner.enqueue(task, false); - ++in_flight_count; + { + Task1 *task = ZN_NEW(Task1(1000 + rng.rand(2000), map, column_pos, events)); + runner.enqueue(task, false); + ++in_flight_count; + } // Add some reading requests for (int i = 0; i < 2; ++i) { - Task2 *task = ZN_NEW(Task2(1000 + rng.rand(2000), map, - Vector3i(rng.rand(MAP_SIZE), rng.rand(MAP_SIZE), rng.rand(MAP_SIZE)), events)); + Task2 *task = ZN_NEW( + Task2(1000 + rng.rand(2000), + map, + Vector3i(rng.rand(MAP_SIZE), rng.rand(MAP_SIZE), rng.rand(MAP_SIZE)), + events) + ); runner.enqueue(task, false); ++in_flight_count; } diff --git a/tests/util/test_string_funcs.cpp b/tests/util/test_string_funcs.cpp index 05f698070..b82ed64dd 100644 --- a/tests/util/test_string_funcs.cpp +++ b/tests/util/test_string_funcs.cpp @@ -8,7 +8,6 @@ void test_int32_to_string_base10(const int32_t x, std::string_view expected) { FixedArray buffer; const unsigned int nchars = int32_to_string_base10(x, to_span(buffer)); - unsigned int expected_length = 0; const unsigned int expected_nchars = expected.size(); ZN_ASSERT(nchars == expected_nchars); diff --git a/tests/util/test_threaded_task_runner.cpp b/tests/util/test_threaded_task_runner.cpp index d7fa649b1..d42d21de9 100644 --- a/tests/util/test_threaded_task_runner.cpp +++ b/tests/util/test_threaded_task_runner.cpp @@ -12,7 +12,7 @@ #include "../../util/tasks/threaded_task_runner.h" #include "../testing.h" -//#define VOXEL_TEST_TASK_POSTPONING_DUMP_EVENTS +// #define VOXEL_TEST_TASK_POSTPONING_DUMP_EVENTS #ifdef VOXEL_TEST_TASK_POSTPONING_DUMP_EVENTS #include #endif @@ -81,7 +81,8 @@ void test_threaded_task_runner_misc() { const unsigned int hw_concurrency = Thread::get_hardware_concurrency(); if (hw_concurrency < test_thread_count) { ZN_PRINT_WARNING(format( - "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency)); + "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency + )); } std::shared_ptr parallel_counter = make_unique_instance(); @@ -191,7 +192,8 @@ void test_threaded_task_runner_debug_names() { const unsigned int hw_concurrency = Thread::get_hardware_concurrency(); if (hw_concurrency < test_thread_count) { ZN_PRINT_WARNING(format( - "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency)); + "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency + )); } ThreadedTaskRunner runner; @@ -315,7 +317,7 @@ void test_threaded_task_postponing() { EventList &events; Task1(int p_sleep_amount_usec, Map &p_map, Vector3i p_bpos, EventList &p_events) : - sleep_amount_usec(p_sleep_amount_usec), map(p_map), bpos0(p_bpos), events(p_events) {} + sleep_amount_usec(p_sleep_amount_usec), bpos0(p_bpos), map(p_map), events(p_events) {} bool try_lock_area(StdVector &locked_blocks) { Vector3i delta; @@ -396,7 +398,8 @@ void test_threaded_task_postponing() { const unsigned int hw_concurrency = Thread::get_hardware_concurrency(); if (hw_concurrency < test_thread_count) { ZN_PRINT_WARNING(format( - "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency)); + "Hardware concurrency is {}, smaller than test requirement {}", test_thread_count, hw_concurrency + )); } ThreadedTaskRunner runner; diff --git a/tests/voxel/test_detail_rendering_gpu.cpp b/tests/voxel/test_detail_rendering_gpu.cpp index 631bbc0be..838061f68 100644 --- a/tests/voxel/test_detail_rendering_gpu.cpp +++ b/tests/voxel/test_detail_rendering_gpu.cpp @@ -100,7 +100,8 @@ void test_normalmap_render_gpu() { nm_task.mesh_block_position = Vector3i(); nm_task.output_textures = detail_textures; nm_task.detail_texture_settings = detail_texture_settings; - nm_task.priority_dependency; + // We don't use priority here because we call the task directly instead of scheduling it into a runner + // nm_task.priority_dependency; nm_task.use_gpu = true; RenderDetailTextureGPUTask *gpu_task = nm_task.make_gpu_task(); diff --git a/tests/voxel/test_edition_funcs.cpp b/tests/voxel/test_edition_funcs.cpp index 7be97f45c..b3cfdb80f 100644 --- a/tests/voxel/test_edition_funcs.cpp +++ b/tests/voxel/test_edition_funcs.cpp @@ -232,7 +232,7 @@ void test_discord_soakil_copypaste() { const Box3i terrain_blocks_box(Vector3i(-2, -2, -2), Vector3i(4, 4, 4)); - terrain_blocks_box.for_each_cell([&voxel_data, &generator](Vector3i bpos) { + terrain_blocks_box.for_each_cell([&voxel_data](Vector3i bpos) { // std::shared_ptr vb = make_shared_instance(); // vb->create(Vector3iUtil::create(1 << constants::DEFAULT_BLOCK_SIZE_PO2)); // VoxelGenerator::VoxelQueryData q{ *vb, bpos << constants::DEFAULT_BLOCK_SIZE_PO2, 0 }; diff --git a/tests/voxel/test_octree.cpp b/tests/voxel/test_octree.cpp index 87cf8f539..f8418ed7b 100644 --- a/tests/voxel/test_octree.cpp +++ b/tests/voxel/test_octree.cpp @@ -58,12 +58,14 @@ void test_octree_update() { bool can_split(Vector3i node_pos, int lod_index, LodOctree::NodeData &data) { return LodOctree::is_below_split_distance( - node_pos, lod_index, viewer_pos_octree_space, lod_distance_octree_space); + node_pos, lod_index, viewer_pos_octree_space, lod_distance_octree_space + ); } bool can_join(Vector3i node_pos, int parent_lod_index) { return !LodOctree::is_below_split_distance( - node_pos, parent_lod_index, viewer_pos_octree_space, lod_distance_octree_space); + node_pos, parent_lod_index, viewer_pos_octree_space, lod_distance_octree_space + ); } }; @@ -79,7 +81,7 @@ void test_octree_update() { const Vector3 relative_viewer_pos = viewer_pos - block_size_v * Vector3(block_offset_lod0); OctreeActions actions; - actions.viewer_pos_octree_space = viewer_pos / block_size; + actions.viewer_pos_octree_space = relative_viewer_pos / block_size; actions.lod_distance_octree_space = lod_distance / block_size; octree.update(actions); @@ -114,7 +116,7 @@ void test_octree_update() { const Vector3 relative_viewer_pos = viewer_pos - block_size_v * Vector3(block_offset_lod0); OctreeActions actions; - actions.viewer_pos_octree_space = viewer_pos / block_size; + actions.viewer_pos_octree_space = relative_viewer_pos / block_size; actions.lod_distance_octree_space = lod_distance / block_size; octree.update(actions); @@ -153,7 +155,7 @@ void test_octree_update() { void test_octree_find_in_box() { const int blocks_across = 32; - const int block_size = 16; + // const int block_size = 16; int lods = 0; { int diameter = blocks_across; @@ -204,16 +206,19 @@ void test_octree_find_in_box() { ZN_TEST_ASSERT(it != expected_positions.end()); const StdUnorderedSet &expected_area_positions = it->second; StdUnorderedSet found_positions; - octree.for_leaves_in_box(area_box, - [&found_positions, &expected_area_positions, &checksum]( - Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { + octree.for_leaves_in_box( + area_box, + [&found_positions, + &expected_area_positions, + &checksum](Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { auto insert_result = found_positions.insert(node_pos); // Must be one of the expected positions ZN_TEST_ASSERT(expected_area_positions.find(node_pos) != expected_area_positions.end()); // Must not be a duplicate ZN_TEST_ASSERT(insert_result.second == true); checksum += node_data.state; - }); + } + ); }); // Doing it again just to measure time @@ -223,13 +228,15 @@ void test_octree_find_in_box() { full_box.for_each_cell([&octree, &checksum2](Vector3i pos) { const Box3i area_box(pos - Vector3i(1, 1, 1), Vector3i(3, 3, 3)); octree.for_leaves_in_box( - area_box, [&checksum2](Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { + area_box, + [&checksum2](Vector3i node_pos, int lod, const LodOctree::NodeData &node_data) { checksum2 += node_data.state; - }); + } + ); }); ZN_TEST_ASSERT(checksum2 == checksum); const int for_each_cell_time = profiling_clock.restart(); - const float single_query_time = float(for_each_cell_time) / Vector3iUtil::get_volume(full_box.size); + const float single_query_time = float(for_each_cell_time) / Vector3iUtil::get_volume_u64(full_box.size); print_line(String("for_each_cell time with {0} lods: total {1} us, single query {2} us, checksum: {3}") .format(varray(lods, for_each_cell_time, single_query_time, checksum2))); } diff --git a/tests/voxel/test_storage_funcs.cpp b/tests/voxel/test_storage_funcs.cpp index 47be1d338..960ab9334 100644 --- a/tests/voxel/test_storage_funcs.cpp +++ b/tests/voxel/test_storage_funcs.cpp @@ -22,8 +22,15 @@ void test_encode_weights_packed_u16() { void test_copy_3d_region_zxy() { struct L { - static void compare(Span srcs, Vector3i src_size, Vector3i src_min, Vector3i src_max, - Span dsts, Vector3i dst_size, Vector3i dst_min) { + static void compare( + Span srcs, + Vector3i src_size, + Vector3i src_min, + Vector3i src_max, + Span dsts, + Vector3i dst_size, + Vector3i dst_min + ) { Vector3i pos; for (pos.z = src_min.z; pos.z < src_max.z; ++pos.z) { for (pos.x = src_min.x; pos.x < src_max.x; ++pos.x) { @@ -42,8 +49,8 @@ void test_copy_3d_region_zxy() { StdVector dst; const Vector3i src_size(8, 8, 8); const Vector3i dst_size(3, 4, 5); - src.resize(Vector3iUtil::get_volume(src_size), 0); - dst.resize(Vector3iUtil::get_volume(dst_size), 0); + src.resize(Vector3iUtil::get_volume_u64(src_size), 0); + dst.resize(Vector3iUtil::get_volume_u64(dst_size), 0); for (unsigned int i = 0; i < src.size(); ++i) { src[i] = i; } @@ -95,8 +102,8 @@ void test_copy_3d_region_zxy() { StdVector dst; const Vector3i src_size(3, 4, 5); const Vector3i dst_size(3, 4, 5); - src.resize(Vector3iUtil::get_volume(src_size), 0); - dst.resize(Vector3iUtil::get_volume(dst_size), 0); + src.resize(Vector3iUtil::get_volume_u64(src_size), 0); + dst.resize(Vector3iUtil::get_volume_u64(dst_size), 0); for (unsigned int i = 0; i < src.size(); ++i) { src[i] = i; } @@ -115,26 +122,26 @@ void test_copy_3d_region_zxy() { void test_transform_3d_array_zxy() { // YXZ int src_grid[] = { - 0, 1, 2, 3, // - 4, 5, 6, 7, // - 8, 9, 10, 11, // + 0, 1, 2, 3, // + 4, 5, 6, 7, // + 8, 9, 10, 11, // 12, 13, 14, 15, // 16, 17, 18, 19, // 20, 21, 22, 23 // }; const Vector3i src_size(3, 4, 2); - const unsigned int volume = Vector3iUtil::get_volume(src_size); + const unsigned int volume = Vector3iUtil::get_volume_u64(src_size); FixedArray dst_grid; ZN_TEST_ASSERT(dst_grid.size() == volume); { int expected_dst_grid[] = { - 0, 4, 8, // - 1, 5, 9, // - 2, 6, 10, // - 3, 7, 11, // + 0, 4, 8, // + 1, 5, 9, // + 2, 6, 10, // + 3, 7, 11, // 12, 16, 20, // 13, 17, 21, // @@ -158,9 +165,9 @@ void test_transform_3d_array_zxy() { } { int expected_dst_grid[] = { - 3, 2, 1, 0, // - 7, 6, 5, 4, // - 11, 10, 9, 8, // + 3, 2, 1, 0, // + 7, 6, 5, 4, // + 11, 10, 9, 8, // 15, 14, 13, 12, // 19, 18, 17, 16, // @@ -187,9 +194,9 @@ void test_transform_3d_array_zxy() { 19, 18, 17, 16, // 23, 22, 21, 20, // - 3, 2, 1, 0, // - 7, 6, 5, 4, // - 11, 10, 9, 8 // + 3, 2, 1, 0, // + 7, 6, 5, 4, // + 11, 10, 9, 8 // }; const Vector3i expected_dst_size(3, 4, 2); IntBasis basis; diff --git a/tests/voxel/test_stream_sqlite.cpp b/tests/voxel/test_stream_sqlite.cpp index 518c5e743..9b307237a 100644 --- a/tests/voxel/test_stream_sqlite.cpp +++ b/tests/voxel/test_stream_sqlite.cpp @@ -134,12 +134,12 @@ void test_voxel_stream_sqlite_coordinate_format(const VoxelStreamSQLite::Coordin const int radius = 10000; // TODO Generate clusters/lines instead, to match what saves look like in practice? for (unsigned int i = 0; i < blocks.size(); ++i) { - BlockInfo &bi = blocks[i]; for (int attempt = 0; attempt < 10; ++attempt) { + const uint8_t lod_index = rng.rand() % constants::MAX_LOD; const BlockInfo bi{ math::wrap( Vector3i(rng.rand(), rng.rand(), rng.rand()), Vector3iUtil::create(2 * radius) ) - Vector3iUtil::create(radius), - rng.rand() % constants::MAX_LOD, + lod_index, i }; if (!BlockInfo::contains_location(blocks, bi.position, bi.lod_index)) { blocks[i] = bi; diff --git a/tests/voxel/test_util.cpp b/tests/voxel/test_util.cpp index 8c2b19818..4eeba9dc3 100644 --- a/tests/voxel/test_util.cpp +++ b/tests/voxel/test_util.cpp @@ -36,6 +36,9 @@ bool sd_equals_approx(const VoxelBuffer &vb1, const VoxelBuffer &vb2) { return false; } } break; + default: + ZN_CRASH_MSG("Unhandled depth"); + break; } } } diff --git a/tests/voxel/test_voxel_buffer.cpp b/tests/voxel/test_voxel_buffer.cpp index dd8f67b7d..64d9430ff 100644 --- a/tests/voxel/test_voxel_buffer.cpp +++ b/tests/voxel/test_voxel_buffer.cpp @@ -51,7 +51,7 @@ class CustomMetadataTest : public ICustomVoxelMetadata { return true; } - virtual ICustomVoxelMetadata *duplicate() { + ICustomVoxelMetadata *duplicate() override { CustomMetadataTest *d = ZN_NEW(CustomMetadataTest); *d = *this; return d; @@ -265,6 +265,7 @@ void test_voxel_buffer_paste_masked() { VoxelBuffer src(VoxelBuffer::ALLOCATOR_DEFAULT); VoxelBuffer dst(VoxelBuffer::ALLOCATOR_DEFAULT); + // clang-format off const uint8_t src_values[] = { 0, 0, 0, 0, 0, // 0, 1, 0, 0, 0, // @@ -281,6 +282,7 @@ void test_voxel_buffer_paste_masked() { 0, 0, 0, 0, 0, // 0, 1, 1, 1, 0, // }; + // clang-format on // const uint8_t src_values_pretransformed[] = { // 0, 0, 0, 0, // @@ -302,6 +304,7 @@ void test_voxel_buffer_paste_masked() { // 1, 1, 1, 0, // // }; + // clang-format off const uint8_t dst_values[] = { 3, 3, 3, 3, // 3, 3, 3, 3, // @@ -322,11 +325,13 @@ void test_voxel_buffer_paste_masked() { 3, 3, 3, 3, // // }; + // clang-format on const Vector3i paste_dst_pos(-1, 0, 1); const uint8_t src_mask_value = 0; const uint8_t dst_mask_value = 3; + // clang-format off const uint8_t expected_values[] = { 3, 3, 3, 3, // 3, 3, 3, 3, // @@ -347,6 +352,7 @@ void test_voxel_buffer_paste_masked() { 1, 1, 1, 3, // // }; + // clang-format on const uint8_t copied_channel_index = 0; const uint8_t src_mask_channel_index = 0; @@ -355,16 +361,16 @@ void test_voxel_buffer_paste_masked() { load_from_array_literal(src, copied_channel_index, src_values, Vector3i(5, 3, 4)); load_from_array_literal(dst, copied_channel_index, dst_values, Vector3i(4, 3, 5)); - paste_src_masked_dst_writable_value( // - to_single_element_span(copied_channel_index), // - src, // - src_mask_channel_index, // - src_mask_value, // - dst, // - paste_dst_pos, // - dst_mask_channel_index, // - dst_mask_value, // - true // + paste_src_masked_dst_writable_value( + to_single_element_span(copied_channel_index), + src, + src_mask_channel_index, + src_mask_value, + dst, + paste_dst_pos, + dst_mask_channel_index, + dst_mask_value, + true ); VoxelBuffer expected(VoxelBuffer::ALLOCATOR_DEFAULT); diff --git a/tests/voxel/test_voxel_graph.cpp b/tests/voxel/test_voxel_graph.cpp index f4c13ce87..e81cf0f76 100644 --- a/tests/voxel/test_voxel_graph.cpp +++ b/tests/voxel/test_voxel_graph.cpp @@ -762,7 +762,7 @@ void test_voxel_graph_generate_block_with_input_sdf() { VoxelBuffer buffer(VoxelBuffer::ALLOCATOR_DEFAULT); buffer.create(Vector3i(BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)); const VoxelBuffer::ChannelId channel = VoxelBuffer::CHANNEL_SDF; - const VoxelBuffer::Depth depth = buffer.get_channel_depth(channel); + // const VoxelBuffer::Depth depth = buffer.get_channel_depth(channel); for (int z = 0; z < buffer.get_size().z; ++z) { for (int x = 0; x < buffer.get_size().x; ++x) { for (int y = 0; y < buffer.get_size().y; ++y) { @@ -1015,7 +1015,7 @@ void test_voxel_graph_functions_misc() { // Y(unused) // const uint32_t n_x = g.create_node(VoxelGraphFunction::NODE_INPUT_X, Vector2()); - const uint32_t n_y = g.create_node(VoxelGraphFunction::NODE_INPUT_Y, Vector2()); + /*const uint32_t n_y =*/g.create_node(VoxelGraphFunction::NODE_INPUT_Y, Vector2()); const uint32_t n_z = g.create_node(VoxelGraphFunction::NODE_INPUT_Z, Vector2()); const uint32_t n_add1 = g.create_node(VoxelGraphFunction::NODE_ADD, Vector2()); const uint32_t n_add2 = g.create_node(VoxelGraphFunction::NODE_ADD, Vector2()); @@ -1039,7 +1039,7 @@ void test_voxel_graph_functions_misc() { Ref generator; generator.instantiate(); // X - // \ + // \ // Z --- Func --- OutSDF // { @@ -1462,8 +1462,8 @@ void test_voxel_graph_unused_single_texture_output() { const uint32_t n_sub = func->create_node(VoxelGraphFunction::NODE_SUBTRACT, Vector2()); - const uint32_t n_out_single_texture = - func->create_node(VoxelGraphFunction::NODE_OUTPUT_SINGLE_TEXTURE, Vector2()); + // const uint32_t n_out_single_texture = + func->create_node(VoxelGraphFunction::NODE_OUTPUT_SINGLE_TEXTURE, Vector2()); func->add_connection(n_plane, 0, n_sub, 0); func->add_connection(n_noise, 0, n_mul, 0); @@ -1568,7 +1568,7 @@ void test_voxel_graph_spots2d_optimized_execution_map() { func->set_node_default_input(n7_mul, 1, 1.f); // b const uint32_t n9_sub = func->create_node(VoxelGraphFunction::NODE_SUBTRACT, Vector2(), 9); - const uint32_t n11_fnl2 = func->create_node(VoxelGraphFunction::NODE_FAST_NOISE_2D, Vector2(), 11); + /*const uint32_t n11_fnl2 =*/func->create_node(VoxelGraphFunction::NODE_FAST_NOISE_2D, Vector2(), 11); const uint32_t n12_out_tex = func->create_node(VoxelGraphFunction::NODE_OUTPUT_SINGLE_TEXTURE, Vector2(), 12); const uint32_t n13_select1 = func->create_node(VoxelGraphFunction::NODE_SELECT, Vector2(), 13); @@ -1899,7 +1899,7 @@ void test_voxel_graph_function_execute() { } const Vector3i block_size(16, 18, 20); - const int volume = Vector3iUtil::get_volume(block_size); + const size_t volume = Vector3iUtil::get_volume_u64(block_size); StdVector x_buffer; StdVector y_buffer; @@ -1929,7 +1929,7 @@ void test_voxel_graph_function_execute() { Span outputs = to_span(sd_buffer); function->execute(Span>(inputs, 3), Span>(&outputs, 1)); - for (int i = 0; i < volume; ++i) { + for (size_t i = 0; i < volume; ++i) { const float obtained_result = sd_buffer[i]; const float expected_result = Math::sin(x_buffer[i]) + Math::cos(z_buffer[i]) + y_buffer[i]; ZN_TEST_ASSERT(Math::is_equal_approx(obtained_result, expected_result)); @@ -2088,15 +2088,15 @@ void test_image_range_grid() { using namespace math; struct L { - static Color get_pixel_repeat(const Image &im, int x, int y) { + static Color get_pixel_repeat(const Image &im, const int x, const int y) { return im.get_pixel(math::wrap(x, im.get_width()), math::wrap(y, im.get_height())); } - static Interval get_range_repeat(const Image &im, Interval x, Interval y) { - const int min_x = Math::floor(x.min); - const int min_y = Math::floor(y.min); - const int max_x = Math::ceil(x.max); - const int max_y = Math::ceil(y.max); + static Interval get_range_repeat(const Image &im, const Interval x_range, const Interval y_range) { + const int min_x = Math::floor(x_range.min); + const int min_y = Math::floor(y_range.min); + const int max_x = Math::ceil(x_range.max); + const int max_y = Math::ceil(y_range.max); Interval i = Interval::from_single_value(get_pixel_repeat(im, min_x, min_y).r); for (int y = min_y; y < max_y; ++y) { @@ -2109,7 +2109,7 @@ void test_image_range_grid() { return i; } - static void test_range(const Image &im, const ImageRangeGrid &range_grid, Interval x, Interval y) { + static void test_range(const Image &im, const ImageRangeGrid &range_grid, const Interval x, const Interval y) { const Interval accurate_range = L::get_range_repeat(im, x, y); const Interval estimated_range = range_grid.get_range_repeat(x, y); ZN_TEST_ASSERT(estimated_range.contains(accurate_range)); @@ -2316,7 +2316,7 @@ void test_voxel_graph_4_default_weights() { // Related to issue #686 const uint32_t ew = buffer.get_voxel(10, 0, 0, VoxelBuffer::CHANNEL_WEIGHTS); const FixedArray indices = decode_indices_from_packed_u16(ei); - const FixedArray weights = decode_weights_from_packed_u16(ew); + // const FixedArray weights = decode_weights_from_packed_u16(ew); ZN_TEST_ASSERT(indices[0] == 0 && indices[1] == 1 && indices[2] == 2 && indices[3] == 3); ZN_TEST_ASSERT(ew == test_ew); @@ -2329,4 +2329,33 @@ void test_voxel_graph_4_default_weights() { // Related to issue #686 L::test(0.5, 0.2, 0.4, 0.8); } +void test_voxel_graph_empty_image() { + // This used to crash + + Ref generator; + generator.instantiate(); + + VoxelGraphFunction &g = **generator->get_main_function(); + + const uint32_t n_x = g.create_node(VoxelGraphFunction::NODE_INPUT_X); + const uint32_t n_z = g.create_node(VoxelGraphFunction::NODE_INPUT_Z); + const uint32_t n_image = g.create_node(VoxelGraphFunction::NODE_IMAGE_2D); + const uint32_t n_out_sdf = g.create_node(VoxelGraphFunction::NODE_OUTPUT_SDF); + + Ref image; + image.instantiate(); + g.set_node_param(n_image, 0, image); + + g.add_connection(n_x, 0, n_image, 0); + g.add_connection(n_z, 0, n_image, 1); + g.add_connection(n_image, 0, n_out_sdf, 0); + + CompilationResult result = generator->compile(false); + + // Try to generate before asserting compilation result. It should fail without crashing. + generator->generate_single(Vector3i(405, 2, 305), VoxelBuffer::CHANNEL_SDF); + + ZN_TEST_ASSERT(result.success == false); +} + } // namespace zylann::voxel::tests diff --git a/tests/voxel/test_voxel_graph.h b/tests/voxel/test_voxel_graph.h index a34939ebc..f71757c50 100644 --- a/tests/voxel/test_voxel_graph.h +++ b/tests/voxel/test_voxel_graph.h @@ -36,6 +36,7 @@ void test_image_range_grid(); void test_voxel_graph_many_subdivisions(); void test_voxel_graph_non_square_image(); void test_voxel_graph_4_default_weights(); +void test_voxel_graph_empty_image(); } // namespace zylann::voxel::tests diff --git a/thirdparty/fast_noise_2/SConscript b/thirdparty/fast_noise_2/SConscript index f847a518e..32fb15557 100644 --- a/thirdparty/fast_noise_2/SConscript +++ b/thirdparty/fast_noise_2/SConscript @@ -52,10 +52,18 @@ fn2_sources_arm = [ env_fn2 = env_voxel.Clone() # In case we need common options for FastNoise2 we can add them here -if env.msvc: +# Note: when compiling with clang-cl on Windows, `env.msvc` is still True because clang-cl behaves like an MSVC +# frontend. However, Clang is more picky and generates more warnings, so we use Clang options anyways. +if env.msvc and not env["use_llvm"]: + # Avoid a compiler bug in VS 2022 (doesn't occur in VS 2019). + # Check for existence because it was added in Godot 4.4 and currently we also support 4.3 + # https://github.com/Zylann/godot/commit/807904d9515e9d2aaf16f6260945a6632689f1b9 + if '/permissive-' in env_fn2['CCFLAGS']: + env_fn2['CCFLAGS'].remove('/permissive-') # In some places, integral constants are multiplied but cause overflow (ex: Simplex.inl(432) in 0.10.0-alpha). # This is usually expected by the devs though. env_fn2.Append(CXXFLAGS=["/wd4307"]) + else: # We compile with `-Wall`, but FastNoise2 does not, so a lot of pedantic things show up treated as errors env_fn2.Append(CXXFLAGS=["-Wno-parentheses"]) @@ -68,8 +76,8 @@ else: # env_fn2.Append(CXXFLAGS=["-Wno-shadow"]) # if '-Wshadow' in env_fn2['CXXFLAGS']: # env_fn2['CXXFLAGS'].remove('-Wshadow') - env_fn2.disable_warnings() +env_fn2.disable_warnings() env_fn2_scalar = env_fn2.Clone() env_fn2_sse2 = env_fn2.Clone() env_fn2_sse3 = env_fn2.Clone() @@ -81,7 +89,10 @@ env_fn2_avx512 = env_fn2.Clone() env_fn2_arm = env_fn2.Clone() # TODO NEON? -if env.msvc: +# Note: when compiling with clang-cl on Windows, `env.msvc` is still True because clang-cl behaves like an MSVC +# frontend. However, Clang is more picky about architecture, so we have to specify each option, which MSVC doesnt have. +# Since Clang still allows to pass `-`style arguments, might as well use the non-MSVC path... +if env.msvc and not env["use_llvm"]: if env["arch"] == "x86_32": # MSVC/64 warns: # ignoring unknown option "/arch:SSE2" as 64 bit already has SSE2 built in @@ -95,7 +106,7 @@ if env.msvc: env_fn2_avx2.Append(CCFLAGS=["/arch:AVX2"]) env_fn2_avx512.Append(CCFLAGS=["/arch:AVX512"]) -else: # Clang, GCC, AppleClang +else: # Clang, GCC, AppleClang, Clang-Cl # TODO The Cmake build script still has a big `if(MSVC)` in that section. # what does it mean? diff --git a/util/containers/span.h b/util/containers/span.h index 73f555187..738d39c96 100644 --- a/util/containers/span.h +++ b/util/containers/span.h @@ -103,14 +103,16 @@ class [[nodiscard]] Span { } } - inline void copy_to(Span other) const { + // Template because T could be const and TDst should not be + template + inline void copy_to(Span other) const { ZN_ASSERT(other.size() == _size); - ZN_ASSERT(other._ptr != nullptr); + ZN_ASSERT(other.data() != nullptr); // for (size_t i = 0; i < _size; ++i) { // other._ptr[i] = _ptr[i]; // } // Should compile to memcpy if T is simple enough - std::copy(_ptr, _ptr + _size, other._ptr); + std::copy(_ptr, _ptr + _size, other.data()); } inline bool overlaps(const Span other) const { @@ -208,13 +210,13 @@ class [[nodiscard]] Span { size_t _size; }; -template +template Span to_span(std::array &a, unsigned int count) { ZN_ASSERT(count <= a.size()); return Span(a.data(), count); } -template +template Span to_span(std::array &a) { return Span(a.data(), a.size()); } diff --git a/util/godot/classes/button.h b/util/godot/classes/button.h index 2630cc8e9..8e2ff4702 100644 --- a/util/godot/classes/button.h +++ b/util/godot/classes/button.h @@ -2,6 +2,7 @@ #define ZN_GODOT_BUTTON_H #if defined(ZN_GODOT) +#include #include #elif defined(ZN_GODOT_EXTENSION) #include @@ -12,7 +13,13 @@ namespace zylann::godot { inline void set_button_icon(Button &button, Ref icon) { #if defined(ZN_GODOT) + +#if VERSION_MAJOR == 4 && VERSION_MINOR <= 3 button.set_icon(icon); +#else + button.set_button_icon(icon); +#endif + #elif defined(ZN_GODOT_EXTENSION) button.set_button_icon(icon); #endif diff --git a/util/godot/classes/curve.h b/util/godot/classes/curve.h index dc08f87fd..386117c1f 100644 --- a/util/godot/classes/curve.h +++ b/util/godot/classes/curve.h @@ -8,4 +8,19 @@ using namespace godot; #endif +#include "../../math/interval.h" +#include "../core/version.h" + +namespace zylann::godot { + +inline math::Interval get_curve_domain(const Curve &curve) { +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR <= 3 + return math::Interval(0, 1); +#else + return math::Interval(curve.get_min_domain(), curve.get_max_domain()); +#endif +} + +} // namespace zylann::godot + #endif // ZN_GODOT_CURVE_H diff --git a/util/godot/classes/editor_import_plugin.cpp b/util/godot/classes/editor_import_plugin.cpp index 9c9ffdad5..6dd8cc647 100644 --- a/util/godot/classes/editor_import_plugin.cpp +++ b/util/godot/classes/editor_import_plugin.cpp @@ -14,7 +14,7 @@ String ZN_EditorImportPlugin::get_visible_name() const { } void ZN_EditorImportPlugin::get_recognized_extensions(List *p_extensions) const { - ZN_ASSERT(p_extensions != nullptr); + ZN_ASSERT_RETURN(p_extensions != nullptr); const PackedStringArray extensions = _zn_get_recognized_extensions(); for (const String &extension : extensions) { p_extensions->push_back(extension); @@ -47,7 +47,7 @@ int ZN_EditorImportPlugin::get_import_order() const { void ZN_EditorImportPlugin::get_import_options(const String &p_path, List *r_options, int p_preset) const { - ZN_ASSERT(r_options != nullptr); + ZN_ASSERT_RETURN(r_options != nullptr); StdVector options; _zn_get_import_options(options, p_path, p_preset); for (const ImportOptionWrapper &option : options) { @@ -65,6 +65,9 @@ bool ZN_EditorImportPlugin::get_option_visibility( } Error ZN_EditorImportPlugin::import( +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 4 + ResourceUID::ID p_source_id, +#endif const String &p_source_file, const String &p_save_path, const HashMap &p_options, @@ -72,8 +75,8 @@ Error ZN_EditorImportPlugin::import( List *r_gen_files, Variant *r_metadata ) { - ZN_ASSERT(r_platform_variants != nullptr); - ZN_ASSERT(r_gen_files != nullptr); + ZN_ASSERT_RETURN_V(r_platform_variants != nullptr, ERR_BUG); + ZN_ASSERT_RETURN_V(r_gen_files != nullptr, ERR_BUG); return _zn_import( p_source_file, p_save_path, @@ -83,6 +86,12 @@ Error ZN_EditorImportPlugin::import( ); } +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 3 +bool ZN_EditorImportPlugin::can_import_threaded() const { + return _zn_can_import_threaded(); +} +#endif + #elif defined(ZN_GODOT_EXTENSION) String ZN_EditorImportPlugin::_get_importer_name() const { @@ -172,6 +181,12 @@ Error ZN_EditorImportPlugin::_import( ); } +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 3 +bool ZN_EditorImportPlugin::_can_import_threaded() const { + return _zn_can_import_threaded(); +} +#endif + #endif String ZN_EditorImportPlugin::_zn_get_importer_name() const { @@ -244,4 +259,10 @@ Error ZN_EditorImportPlugin::_zn_import( return ERR_METHOD_NOT_FOUND; } +bool ZN_EditorImportPlugin::_zn_can_import_threaded() const { + // According to docs + // https://docs.godotengine.org/en/stable/classes/class_editorimportplugin.html#class-editorimportplugin-private-method-can-import-threaded + return true; +} + } // namespace zylann::godot diff --git a/util/godot/classes/editor_import_plugin.h b/util/godot/classes/editor_import_plugin.h index 9b1598502..3c4b08a81 100644 --- a/util/godot/classes/editor_import_plugin.h +++ b/util/godot/classes/editor_import_plugin.h @@ -8,6 +8,8 @@ using namespace godot; #endif +#include "../core/version.h" + #include "../../containers/std_vector.h" namespace zylann::godot { @@ -100,6 +102,9 @@ class ZN_EditorImportPlugin : public EditorImportPlugin { ) const override; Error import( +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 4 + ResourceUID::ID p_source_id, +#endif const String &p_source_file, const String &p_save_path, const HashMap &p_options, @@ -108,6 +113,10 @@ class ZN_EditorImportPlugin : public EditorImportPlugin { Variant *r_metadata = nullptr ) override; +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 3 + bool can_import_threaded() const override; +#endif + #elif defined(ZN_GODOT_EXTENSION) String _get_importer_name() const override; String _get_visible_name() const override; @@ -130,6 +139,10 @@ class ZN_EditorImportPlugin : public EditorImportPlugin { const TypedArray &gen_files ) const override; +#if GODOT_VERSION_MAJOR == 4 && GODOT_VERSION_MINOR >= 3 + bool _can_import_threaded() const override; +#endif + #endif protected: @@ -144,6 +157,7 @@ class ZN_EditorImportPlugin : public EditorImportPlugin { virtual String _zn_get_resource_type() const; virtual float _zn_get_priority() const; virtual int _zn_get_import_order() const; + virtual bool _zn_can_import_threaded() const; virtual void _zn_get_import_options( StdVector &p_out_options, diff --git a/util/godot/classes/editor_quick_open.h b/util/godot/classes/editor_quick_open.h index 5a0b9ceee..aea7eaef6 100644 --- a/util/godot/classes/editor_quick_open.h +++ b/util/godot/classes/editor_quick_open.h @@ -2,7 +2,13 @@ #define ZN_GODOT_EDITOR_QUICK_OPEN_H #if defined(ZN_GODOT) + +#if VERSION_MAJOR == 4 && VERSION_MINOR <= 3 #include +#else +#include +#endif + // TODO GDX: EditorQuickOpen is not exposed! // #elif defined(ZN_GODOT_EXTENSION) // #include diff --git a/util/godot/classes/rendering_device.h b/util/godot/classes/rendering_device.h index 3e450d0c6..73c3f10f2 100644 --- a/util/godot/classes/rendering_device.h +++ b/util/godot/classes/rendering_device.h @@ -22,12 +22,21 @@ void free_rendering_device_rid(RenderingDevice &rd, RID rid); Ref shader_compile_spirv_from_source(RenderingDevice &rd, RDShaderSource &p_source, bool p_allow_cache); RID shader_create_from_spirv(RenderingDevice &rd, RDShaderSPIRV &p_spirv, String name = ""); -RID texture_create(RenderingDevice &rd, RDTextureFormat &p_format, RDTextureView &p_view, - const TypedArray &p_data); +RID texture_create( + RenderingDevice &rd, + RDTextureFormat &p_format, + RDTextureView &p_view, + const TypedArray &p_data +); RID uniform_set_create(RenderingDevice &rd, Array uniforms, RID shader, int shader_set); RID sampler_create(RenderingDevice &rd, const RDSamplerState &sampler_state); Error update_storage_buffer( - RenderingDevice &rd, RID rid, unsigned int offset, unsigned int size, const PackedByteArray &pba); + RenderingDevice &rd, + RID rid, + unsigned int offset, + unsigned int size, + const PackedByteArray &pba +); } // namespace zylann::godot diff --git a/util/godot/classes/rendering_server.cpp b/util/godot/classes/rendering_server.cpp index 8d8a9f3ce..110cef074 100644 --- a/util/godot/classes/rendering_server.cpp +++ b/util/godot/classes/rendering_server.cpp @@ -29,4 +29,14 @@ void get_shader_parameter_list(const RID &shader_rid, StdVectorget_current_rendering_method(); +#elif defined(ZN_GODOT_EXTENSION) + return ""; +#endif +} + } // namespace zylann::godot diff --git a/util/godot/classes/rendering_server.h b/util/godot/classes/rendering_server.h index 136c035da..1baa473e8 100644 --- a/util/godot/classes/rendering_server.h +++ b/util/godot/classes/rendering_server.h @@ -27,6 +27,8 @@ struct ShaderParameterInfo { void get_shader_parameter_list(const RID &shader_rid, StdVector &out_parameters); +String get_current_rendering_method(); + } // namespace zylann::godot #endif // ZN_GODOT_RENDERING_SERVER_H diff --git a/util/godot/direct_static_body.cpp b/util/godot/direct_static_body.cpp index 5ba8987ec..6eac899f8 100644 --- a/util/godot/direct_static_body.cpp +++ b/util/godot/direct_static_body.cpp @@ -98,7 +98,7 @@ void DirectStaticBody::set_shape_enabled(int shape_index, bool enabled) { } } -void DirectStaticBody::set_attached_object(Object *obj) { +void DirectStaticBody::set_attached_object(const Object *obj) { // Serves in high-level collision query results, `collider` will contain the attached object ERR_FAIL_COND(!_body.is_valid()); PhysicsServer3D::get_singleton()->body_attach_object_instance_id( @@ -123,7 +123,7 @@ void DirectStaticBody::set_debug(bool enabled, World3D *world) { _debug_mesh_instance.create(); _debug_mesh_instance.set_world(world); - Transform3D transform = + const Transform3D transform = PhysicsServer3D::get_singleton()->body_get_state(_body, PhysicsServer3D::BODY_STATE_TRANSFORM); _debug_mesh_instance.set_transform(transform); diff --git a/util/godot/direct_static_body.h b/util/godot/direct_static_body.h index 4848301a6..b3d286d4f 100644 --- a/util/godot/direct_static_body.h +++ b/util/godot/direct_static_body.h @@ -25,7 +25,7 @@ class DirectStaticBody : public zylann::NonCopyable { Ref get_shape(int shape_index); void set_world(World3D *world); void set_shape_enabled(int shape_index, bool disabled); - void set_attached_object(Object *obj); + void set_attached_object(const Object *obj); void set_collision_layer(int layer); void set_collision_mask(int mask); diff --git a/util/io/log.h b/util/io/log.h index 1c3205dc3..dcebd2eb6 100644 --- a/util/io/log.h +++ b/util/io/log.h @@ -13,6 +13,15 @@ #define ZN_PRINT_WARNING(msg) zylann::print_warning(msg, __FUNCTION__, __FILE__, __LINE__) #define ZN_PRINT_ERROR(msg) zylann::print_error(msg, __FUNCTION__, __FILE__, __LINE__) +#define ZN_PRINT_ERROR_ONCE(msg) \ + { \ + static bool s_first_print = true; \ + if (s_first_print) { \ + s_first_print = false; \ + zylann::print_error(msg, __FUNCTION__, __FILE__, __LINE__); \ + } \ + } + namespace zylann { bool is_verbose_output_enabled(); diff --git a/util/island_finder.h b/util/island_finder.h index 66ee10a58..fd3b3f1cf 100644 --- a/util/island_finder.h +++ b/util/island_finder.h @@ -23,7 +23,7 @@ class IslandFinder { template void scan_3d(Box3i box, VolumePredicate_F volume_predicate_func, Span output, unsigned int *out_count) { - const size_t volume = Vector3iUtil::get_volume(box.size); + const size_t volume = Vector3iUtil::get_volume_u64(box.size); CRASH_COND(output.size() != volume); memset(output.data(), 0, volume * sizeof(uint8_t)); diff --git a/util/math/funcs.h b/util/math/funcs.h index 04a436892..6c2dec6f2 100644 --- a/util/math/funcs.h +++ b/util/math/funcs.h @@ -497,6 +497,17 @@ inline T pow(T x, T y) { return Math::pow(x, y); } +inline uint64_t multiply_check_overflow_u64(const uint64_t a, const uint64_t b) { + const uint64_t r = a * b; +#ifdef DEV_ENABLED + if (a != 0 && r / a != b) { + ZN_PRINT_ERROR("Multiplication overflow"); + return 0; + } +#endif + return r; +} + } // namespace zylann::math #endif // VOXEL_MATH_FUNCS_H diff --git a/util/math/vector3i.h b/util/math/vector3i.h index 8fc7dc04a..040f896ea 100644 --- a/util/math/vector3i.h +++ b/util/math/vector3i.h @@ -33,12 +33,15 @@ inline void sort_min_max(Vector3i &a, Vector3i &b) { } // Returning a 64-bit integer because volumes can quickly overflow INT_MAX (like 1300^3), -// even though dense volumes of that size will rarely be encountered in this module -inline int64_t get_volume(const Vector3i &v) { +// even though dense volumes of that size will rarely be encountered in this module. +inline uint64_t get_volume_u64(const Vector3i &v) { #ifdef DEBUG_ENABLED ZN_ASSERT_RETURN_V(v.x >= 0 && v.y >= 0 && v.z >= 0, 0); #endif - return v.x * v.y * v.z; + return math::multiply_check_overflow_u64( + static_cast(v.x), + math::multiply_check_overflow_u64(static_cast(v.y), static_cast(v.z)) + ); } inline unsigned int get_zxy_index(const Vector3i &v, const Vector3i area_size) { diff --git a/util/noise/fast_noise_lite/fast_noise_lite_range.cpp b/util/noise/fast_noise_lite/fast_noise_lite_range.cpp index 739a00d04..2274349f1 100644 --- a/util/noise/fast_noise_lite/fast_noise_lite_range.cpp +++ b/util/noise/fast_noise_lite/fast_noise_lite_range.cpp @@ -184,7 +184,7 @@ Interval fnl_single_opensimplex2s( ) { return get_noise_range_3d( [&fn, seed](real_t x, real_t y, real_t z) { // - return fn.SingleOpenSimplex2(seed, x, y, z); + return fn.SingleOpenSimplex2S(seed, x, y, z); }, // Max derivative found from empiric tests p_x, diff --git a/util/noise/spot_noise.h b/util/noise/spot_noise.h index f4f571c3a..5493a4642 100644 --- a/util/noise/spot_noise.h +++ b/util/noise/spot_noise.h @@ -199,7 +199,7 @@ inline math::Interval spot_noise_3d_range( ivec3 min_cell_origin_norm_i = to_vec3i(min_cell_origin_norm); ivec3 max_cell_origin_norm_i = to_vec3i(max_cell_origin_norm); - if (Vector3iUtil::get_volume(max_cell_origin_norm_i - min_cell_origin_norm_i + ivec3(1, 1, 1)) > 30) { + if (Vector3iUtil::get_volume_u64(max_cell_origin_norm_i - min_cell_origin_norm_i + ivec3(1, 1, 1)) > 30) { // Don't bother checking too many cells, assume we'll intersect a spot. return math::Interval(0, 1); } diff --git a/util/string/expression_parser.cpp b/util/string/expression_parser.cpp index f26c58485..6bb858ed3 100644 --- a/util/string/expression_parser.cpp +++ b/util/string/expression_parser.cpp @@ -5,6 +5,10 @@ #include #include +#if defined(_MSC_VER) +#pragma warning(disable : 4701) // Potentially uninitialized local variable used. +#endif + namespace zylann { namespace ExpressionParser { diff --git a/util/voxel_raycast.h b/util/voxel_raycast.h index 6f4e42272..e0234296d 100644 --- a/util/voxel_raycast.h +++ b/util/voxel_raycast.h @@ -1,3 +1,6 @@ +#ifndef ZN_VOXEL_RAYCAST_H +#define ZN_VOXEL_RAYCAST_H + #include "../util/math/vector3i.h" // #include "../util/profiling.h" #include "errors.h" @@ -59,7 +62,9 @@ bool voxel_raycast( // Note : the grid is assumed to have 1-unit square cells. +#ifdef DEBUG_ENABLED ZN_ASSERT_RETURN_V(math::is_normalized(ray_direction), false); // Must be normalized +#endif /* Initialisation */ @@ -202,3 +207,5 @@ bool voxel_raycast( } } // namespace zylann + +#endif // ZN_VOXEL_RAYCAST_H