Zylann / godot_voxel

Voxel module for Godot Engine
MIT License
2.72k stars 251 forks source link

Guidance on using `VoxelGeneratorScript` to generate smooth SDF voxels? #579

Closed SummitCollie closed 1 year ago

SummitCollie commented 1 year ago

Thanks so much for your making your work available, this module is really cool!

I'm having trouble getting my VoxelLodTerrain to generate any visible output. My scene looks like this: Scene Tree

I'm using a recent Windows build of the editor (1a51cc2)

My VoxelLodTerrain is set up with the default ShaderMaterial, just added a fragment shader which sets the albedo and left the vertex shader alone for now. This is the main script on the scene root:

extends Node3D

@onready var terrain = $VoxelLodTerrain
const MyGenerator = preload("res://voxel_generator_test/my_generator.gd")

func _ready():
    terrain.generator = MyGenerator.new()
    # My level is bounded so I was hoping to contain the voxel rendering like this,
    # this bounding box looks correct in the editor
    terrain.set_voxel_bounds(AABB(Vector3.ZERO, Vector3.ONE * 512))

And my_generator.gd looks like:

extends VoxelGeneratorScript

const channel : int = VoxelBuffer.CHANNEL_SDF

func _get_used_channels_mask() -> int:
    return 1 << channel

func _generate_block(buffer : VoxelBuffer, origin : Vector3i, lod : int) -> void:
    if lod != 0: return

    var distance_to_zero = Vector3(origin).distance_to(Vector3.ZERO)
    if distance_to_zero > 100 && distance_to_zero < 150:
      buffer.fill_f(1, channel)
    else:
      buffer.fill_f(0, channel)

And... nothing renders. I'm pretty new to Godot and this module so it's quite possible I'm missing an important step or doing something dumb, but the docs and sample projects seem to be lacking examples of how to make a generator for SDF VoxelLodTerrain so I'm stuck.

The script above is my attempt to just produce any visible result, but my ultimate goal is to generate a solid 3D block of voxels and carve tunnels out of it based on other logic in my game code, like distance from a Curve3D (inside the radius of the tunnel = no voxels). IDK whether this is even a sane approach but it seems like a better option than using VoxelTool to carve tunnels at runtime.

Any advice is greatly appreciated!

Zylann commented 1 year ago

the docs and sample projects seem to be lacking examples of how to make a generator for SDF VoxelLodTerrain

There is indeed no copypastable example, but you can get close to this. Smooth terrain is based on signed distance fields. The doc explains what it is, and contains some snippets: https://voxel-tools.readthedocs.io/en/latest/smooth_terrain/#signed-distance-fields

And... nothing renders.

In the editor? Or did you launch the game? Don't forget to have a VoxelViewer in your scene, as this is necessary to tell the terrain where to stream chunks. Also you should make sure the 3D camera looks at the right angle because sometimes surfaces will show up out of your view.

Also:

    if distance_to_zero > 100 && distance_to_zero < 150:
      buffer.fill_f(1, channel)
    else:
      buffer.fill_f(0, channel)

Signed distances need to cross 0 in order to produce a surface. All your voxels here will either have distance 0 or 1, so never crossing zero. So no surface will appear. At least maybe you need to set -1 instead of 0.

if distance_to_zero > 100 && distance_to_zero < 150:

With this logic I suppose you will obtain a very crude boxy hollow sphere, but inverted, so if your camera spawns at the origin, it will actually be below the surface and won't see it due to backface culling. And since it is inverted, there is a "ceiling" you could see 150 units away, but I suspect default view distance wont be enough to see it. I might be wrong on that one tho.

if lod != 0: return

And this pretty much prevents you to see any of it as well. Because by default LOD 0 only extends through a limited distance (I think about 50 units?) away from the camera, after which LOD 1 starts, then LOD 2 further away etc. But then your generator will return empty chunks beyond LOD 0 so nothing will show up since your shape "starts" at around 100 units away from origin. You can see how the expected VoxelBuffers look like in this LOD system here: https://voxel-tools.readthedocs.io/en/latest/smooth_terrain/#level-of-detail-lod

If you need an example:

extends VoxelGeneratorScript

const channel : int = VoxelBuffer.CHANNEL_SDF

func _get_used_channels_mask() -> int:
    return 1 << channel

func _generate_block(buffer : VoxelBuffer, origin : Vector3i, lod : int) -> void:
    var bs := buffer.get_size()
    var sphere_radius := 50.0

    # This is not required, but helps getting a smooth result,
    # see https://voxel-tools.readthedocs.io/en/latest/smooth_terrain/#scaling-and-clamping
    var quantization_factor := 0.01

    for z in bs.z:
        for x in bs.x:
            for y in bs.y:
                # The `a << b` operator is equivalent to `a * pow(2, b)`, but is faster than `pow`.
                # Unfortunately Godot's `Vector3i` does not support this operator for some reason.
                var position := Vector3(origin) + Vector3(x << lod, y << lod, z << lod)
                var distance_to_zero := Vector3(position).distance_to(Vector3.ZERO)
                # Makes a sphere centered at the origin. Move the camera above 50 units in order to see it,
                # or invert the sign to turn it inwards.
                # This is one of the simplest SDF shapes. 
                # More here: https://iquilezles.org/articles/distfunctions/
                var sphere_signed_distance := distance_to_zero - sphere_radius
                buffer.set_voxel_f(sphere_signed_distance * quantization_factor, x, y, z, channel)

If you want to fill all space with matter, you can also just do buffer.fill_f(-1, channel). Nothing will show up until you carve something, but that's normal because if all space is filled, there will be no surface to show, because existence of a surface implies existence of a non-filled area.

SummitCollie commented 1 year ago

In the editor? Or did you launch the game?

Ah sorry I should've clarified since I know people frequently neglect to do that stuff: I initially used the flat generator as a sanity check and I did see its output using the same setup (both in editor & in game). The camera (with VoxelViewer child) is moveable with WASD/mouse via a script, and I always check by zooming and rotating to different angles.

I'm also aware that my generator script's output won't be visible in the editor unless I enable the "Run Stream In Editor" option on VoxelLodTerrain, which I shouldn't do unless I'm planning not to change any script files (because the editor accessing them would cause problems).

Signed distances need to cross 0 in order to produce a surface. All your voxels here will either have distance 0 or 1, so never crossing zero. So no surface will appear. At least maybe you need to set -1 instead of 0.

Ah yeah thanks, I think this is the concept I wasn't understanding, even having read over your SDF doc (perhaps not thoroughly enough).

if lod != 0: return

And this pretty much prevents you to see any of it as well.

Okay, that makes sense. I copied my script from the example I found in the scripting docs, I guess it works in that scenario because blocky voxels don't support LOD therefore always have lod == 0? But I was curious about how anything outside the first LOD zone would get rendered with that line there... figured I should at least see some stuff near the camera though.

Thanks for all your help, and appreciate the example, I'll have to play with that after work! Closing since you've given me plenty to work with and it's my fault if I can't get it to render at this point lol.

If you feel like indulging me further, >The main thing I'm still wondering how I'll accomplish is applying noise to the interior walls of the tunnels I carve (to make them look like caves, maybe add stuff like stalactites etc). I was just gonna throw sine waves at it but maybe you know of a good strategy? > >I need a guaranteed minimum clearance between cave walls (as well as known start/end points) which is why I'm not just using a noise algo for the whole thing. Your [procedural generation docs on determinism](https://voxel-tools.readthedocs.io/en/latest/procedural_generation/#deterministic-approach) seem like a good starting point but if you've got any other thoughts/resources on that topic I'd be happy to hear/learn of them!