Best Practice for Custom Shaped 3D Fog Effect

Godot Version

4.0

Question

I’m trying to fill a 3D hexagonal shape with a fog-like effect, I was looking into fogvolumes but saw that they are limited to the shapes they can have. There are ways of using custom 3D textures with these, but I’m not entirely sure what that is or how it works or how I could use it to achieve the effect I want.

Am I better off using a mesh with a custom fog shader? Do I have other options? Any input appreciated, thanks!

1 Like

Hey! I know this is a bit late, but I’ve been starting to work with fog shaders recently and I actually came up with an answer for this. I only saw your post now as I wanted to look through what the forum had to say on them.

I’m currently working on a tool that should make the creation of 3d textures to use for fog, its not open source yet as its not ready but I’m happy to share how im approaching this in case you wanted to try an implementation for yourself (if you still care, lol)

The idea is to take some custom mesh data, voxelize it, and use that data to create a texture3d. There are multiple ways to go about it, you can create a mesh with whatever modelling program you have or you can and give that to a mishinstance3d node, or you can block something out multiple mesh instance primitives. this is going to be the shape of the fog volume. then in a script you ant to find all of the mesh intances and call YourMeshInstance.create_convex_collision() on each of them. this will give those mesh instances a child staticbody3d with a shape resource we’re going to use.

then you want to define some bounds around the fog volume, so create a box shape and make sure the dimensions are some power of 2. the actual size of the sculpted mesh for the fog and this shapr thats bounding it dont matter as they’ll scale to fit whatever fog node we give the texture to, but it WILL be important to make sure the bounding shape fits the meshs we used to sculpt the fog AND to make sure the bounds are some power of two. its also worth making sure all of its rotations are set to 0 otherwise ther’ll be some addec complexity to deal with later

the next step is to figure out the resolution of voxels, 1m cubes can workd, but you can go with a higher resolution if you want (1/2 meter, 1/8 meter) but keep in mind 3d textures can take up a huge amount of vram memory. this wont THAT big of a bottle neck in performance (the majority of the hit to performance when using a fog volume like this is from the amount of screen space they take up) but it can still take up much more space than you realise.

the in code create a 3d nested array like this use the size of the bounding area and the voxel size youve decided upon to figure out how many voxels tall/wide/deep the area is going to be (xyz in worldspace and xyz in the nested array). You can make it an array of ints if you want, floats would be better. then get the vertex from the bounding area with the lowest value for xyw in world space and keep it in a vec3 somewhere. then make a triple nested for loop that iterates all of the indices in the 3d array, something like this :

for X in VoxelX:
	for Y in VoxelY:
		for Z in VoxelZ:

before the loop starts call

var Point : PhysicsPointQueryParameters3D = PhysicsPointQueryParameters3D.new()

and then in the loop

Point.position = LowestValueVertexInBounds + Vector3(VoxelDimension, VoxelDimension, VoxelDimension) + Vector3(X, Y, Z)
var Intersects : Array[Dictionary] = PhysicsDirectSpaceState3D.intersect_point(Point, 1)
if(Intersects.is_empty()):
	VoxelArr[X][Y][Z] = 0.0
else:
	VoxelArr[X][Y][Z] = 0.0

then when youre done, make anotherarray of image objects and iterate over it (the number is the number of voxels in depth i think)

var ImageArray : Array[Image]
ImageArray.resize(VoxelZ)
for i in ImageArray:
	Var Slice : Image = Image.create_empty(X, Y, false, Image.Format.FORMAT_R8)
	for X in VoxelX:
		for Y in VoxelY:
			var C : Color
			C.r8 = VoxelArr[x][y][i]
			Slice.set_pixel(x, y, C)
	ImageArray[i] = Slice
var FogTexture : ImageTexture3D = ImageTexture3D.create(Image.Format.FORMAT_R8, VoxelX, VoxelY, VoxelZ, false, ImageArray)

and then there you have it! your fog texture! just one 8bit channel. in theory if you wanted something like antialiasing you could do a point intersect at each octant of a voxel an set the VoxelArr[y][z] to += 1/8 for each point that returns intersecting (or go even higher in resolution if you wish, kind og like MSAA, but this isnt really necessary because fog volumes are pretty nebulous so aliasing isnt a bit issue).

the format FORMAT_R8 is being used here because its a single channel format with only one byte so its pretty memory efficient, but even a 256256256 volume is going to be 16 MB. there are other things you can do with this method as well. Using the PhysicsServer in theory you could find the normal of the surface thats intersexting a voxel and store a normal value as well and use the FORMAT_RGBAH format (which takes EIGHT times as much memory) and store the fog density in alpha and store the normal vector in RGB. then maybe you could pass the normal of a single light source to the shader (the direction its coming from) and change the albedo/emission based on the dot product of the voxels normal and the light normal. I think clouds using this could look really nice.

You could also do internally varying density which wold make things look a bit more realistic. like you when you block out the shape of the fog volume you can make it all on one specific collision mask, and then do a second “layer” of your block out that represents denser internal regions and set them on a second collision mask, and then when you use the point query use Point.collision_mask and iterate over each collision mask and instead of adding a value of x to the array, you can add (x / totamasks)

The problem with this approach is that the actual shapes in the volumes would be static, but if you really wanted to get crazy you could make an “animation” by moving the meshes you used for blocking around the the editor and creating multiple 3d textures. this would take a lot of time, use a heap of memory, and you GPU might try and kill you in your sleep for trying to update megabytes of data at runtime, but the effect could look pretty cool.

but yeah most of that code probably wont work as it is lol, but thats the general approach im using. its not super complicated to implement yourself in the meantime, but I do intend to open source this when its in a respectable state.

good luck!

2 Likes

Wow I love you!

From one rodent to another, thank you so so so much! This a fantastic reply with thorough explanation and I could not have asked for better help. Extremely excited to put this to use.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.