3D Voxel Model Shader

Welcome to our brand new Clickteam Community Hub! We hope you will enjoy using the new features, which we will be further expanding in the coming months.

A few features including Passport are unavailable initially whilst we monitor stability of the new platform, we hope to bring these online very soon. Small issues will crop up following the import from our old system, including some message formatting, translation accuracy and other things.

Thank you for your patience whilst we've worked on this and we look forward to more exciting community developments soon!

Clickteam.
  • Just thought I'd share my latest DX11 pixel shader, made for displaying 3D voxel models in CF2.5, which also includes some basic directional lighting (look out for an updated voxel terrain shader, coming soon).

    Please login to see this picture.

    Download: Please login to see this link.

    As always, feel free to share, modify or use it in any way you like.
    PS. Thanks to NaitorStudios for his recent help, which made this possible.

  • Yeah, I don't know the limitations of the other runtimes. Using DX11 definitely made me very lazy though, since it has so few limitations - there's definitely plenty of potential for more streamlining if needed.

    I'm wondering if I need to modify the shader to support multiple rows of images in a texture. My B24 Liberator model, which is not huge, still ends up with a texture almost 4000px wide (but far less tall), and in WebGL I believe the maximum texture size that's guaranteed to be supported by all hardware, is 4096 x 4096 pixels, so perhaps some other runtimes might have similar restrictions?

    EDIT: In fact, I just tried it again, and since the last changes I made to the shader, the B24 has started causing the app to just silently close on startup (it worked before) :(
    EDIT2: It turns out that isn't the problem - it just crashes any time I choose the texture using the file selector instead an expression with apppath$ + "filename.png". That's really weird...

    Edited 2 times, last by MuddyMole (December 12, 2022 at 9:58 PM).

  • Version 2 is out now:

    Please login to see this picture.
    (100x100x100 cube!)

    The big change is that it now uses a two-dimensional grid of frames, rather than a single row, and you can specify which frames to use (allowing for easy animation).
    Other enhancements include a new fog/darkness system, the ability to set the centre of rotation (like the "hotspot" in Cf2.5) and the order of rotations.
    I've also included a very basic user guide.

    Download: Please login to see this link.

  • I'm experimenting with making the voxels more like proper cubes. The results look quite nice, but performance has taken a big hit, so unless I can manage some serious optimisation, this may be a dead end... :(

    Please login to see this picture.

    Please login to see this picture.

  • There's not much to it really - in the getPixel function, right after where it says "int3 pos = round(position);" you simply divide "pos" by a scaling factor. I used 8 in those examples, so each cube is technically 8x8x8 voxels. And then you need to increase the number of loop iterations in the main function, because the maximum view distance is also reduced by a factor of 8 - and of course, that's what causes the performance to suffer.

    That example with the trees is actually quite a challenging scenario, since more than half of the view is sky, meaning that the loops are running to completion (500 iterations), rather than stopping early when the ray hits a solid object (if you tilt the camera down towards the ground a bit, it runs much faster). If used for a game with more confined spaces, I think performance could be very much better. I remember Sinclair Strange recreated the first level of Alien Trilogy using P3D, and that would be the perfect scenario - very confined spaces (including low ceilings), and a lot of darkness to limit the player's range of vision: Please login to see this link.

    In fact, I believe the original Playstation generally used a 320x240 display mode, and at that low resolution, I can already manage 60fps, so there's hope yet!
    I've also found heaps of ways to simplify and optimise the engine, which should help.

    The next part of the plan is to make that "3D tilemap" version I talked about, where each pixel on a map texture becomes a 16x16x16 (same as minecraft) volume of voxels. These cubes were actually the first step towards that. And then I'm going to try that "fake depth buffer" idea, so that voxel models or billboarded sprites can be used inside the voxel world! (though it won't be user-friendly)

  • Nah, it's not for yet another Minecraft clone. Cubes are boring and ugly.

    Please login to see this picture.

    It might not look like much, as I'm still testing with cubes, but each one you see is actually now a proper 16x16x16 voxel model, that you could make any shape you like - for example, remember my old Sopwith biplane?

    Please login to see this picture.

    Just having some troublesome rounding issues at the moment, but it's getting there...

  • I've managed to fake a depth buffer in DX11, so now separate actives can be placed "inside" the voxel world, which is quite a significant change:

    Please login to see this picture.

    The trick is that instead of using the red, green and blue components to store the colour of the voxel, I just use the red component to define a palette index, and then use the green and blue components to store lighting and depth information. And then each object samples the background, reading the blue component to decide whether it's in front of or behind it. And finally, another shader samples the red component and uses it to look up an RGB colour from a palettte image to display.

  • Merry Christmas!
    Just thought I'd share the progress I've made on my Voxel World shader.
    I decided to try making a more complex scene (based on an image found online) as a way to test the shader, and decide what works and what doesn't, and what needs adding. I know I could fix some things with it, or add more detail, but I think it's served its purpose now.

    In a nutshell:
    * Happy with performance.
    * Not happy with lighting - will change to using a point light source instead of directional lighting.
    * Desperately needed a way to flip/mirror voxel blocks, so have now added that.

    Please login to see this picture.

  • Merry Christmas!
    Just thought I'd share the progress I've made on my Voxel World shader.
    I decided to try making a more complex scene (based on an image found online) as a way to test the shader, and decide what works and what doesn't, and what needs adding. I know I could fix some things with it, or add more detail, but I think it's served its purpose now.

    In a nutshell:
    * Happy with performance.
    * Not happy with lighting - will change to using a point light source instead of directional lighting.
    * Desperately needed a way to flip/mirror voxel blocks, so have now added that.

    Looks awesome! I agree with the Minecraft thing.
    However, the way minecraft only shows you surfaces you actually need to see is awesome for saving resources.
    Excellent work so far!

    As far as lighting goes. Maybe to save resources just have a .png in front of the player which size is controlled by floating Actives around the player.
    I.E. Left Active controls the stretch of the .png out to whatever it collides with. Right active does the same off to the right of the player's view. Same with up and down / Directional.
    If the active comes in contact with an object it forces the X of the active inwards squeezing the .png to make the light look like it is reflecting. Even though, it's obviously not true light!

    Example file
    Please login to see this attachment.

    Please login to see this picture.

  • Thanks :) I like the vignetting for a kind of flashlight effect - especially in horror games.

    The lighting system actually has a pretty negligible cost in terms of performance. Basically, the shader relies heavily on one complex function which gets the contents of a point in 3D space. The lighting system is mostly just calling that function once for each space neighbouring a solid voxel that a ray hits (so 6 times if we just consider orthogonally adjacent spaces) in order to calculate a surface normal. However, the same function gets called once for each space between the camera and that first solid voxel that the ray hits, and that distance is generally going to be much, much greater than 6, and potentially as much as 1000 (or whatever the maximum draw distance is). The cost of the fog/darkness effect is essentially zero, since it's just using information that we needed to calculate anyway.

    The real issue is that unlike outdoor environments, which often only have one lightsource (the sun), indoor environments usually have many (that test scene has rows of lights in both the ceiling and floor). Baking the light into the model is the obvious solution, and that's mostly fine (apart from making it even more difficult to create maps), except for when there are glowing objects such as lamps - it looks very wrong for them to be affected by the darkness/fog, the same as any other material.

    I think I need to replace the directional light with a point light (ie. instead of using a fixed light vector, I'd use the vector from a fixed point to each voxel). It could still be used like a directional light if placed far away from the scene, or it could be always placed at the player's location for more of a torchlight effect.

    And then I'm thinking that the 256 colour palette could be divided into sections with different properties - for example:
    0: transparent
    1-50: matte surfaces (no specular highlights)
    51-150: standard surfaces
    151-200: glossy sufaces (more intense specular highlights)
    200-245: illuminated surfaces (effect of darkness varies continuously, from 200:"almost normal", to 245:"almost no effect").
    246-255: emissive materials (not affected by darkness at all)

    The real challenge will be "inserting" models using separate actives into the same scene. The demo above with the smiley face shows that the depth system should work, but the objects still need to be correctly positioned, scaled, rotated and illuminated to match everything else (or at least positioned and scaled in the case of a billboarded 2D sprite) - that's going to involve a lot of complicated maths, much of which needs to be done outside the shader, using CF2.5 events...

  • Who wants a little maths lesson?

    The shader for the tile-based voxel environment is pretty much finished now, so it's time to move on to the next part of the engine - other objects that can be placed within the environment.

    Please login to see this picture.

    Let's start with 2D "billboard" sprites - and just while we figure out the maths, we'll work in two-dimensions only.

    So, first of all we need a vector pointing in the direction the camera is facing:
    camDirX = cos(cameraAngle)
    camDirY = 0 - sin(cameraAngle)
    (right now, that's a "unit vector", with a length of 1)

    Please login to see this picture.

    Next we need a vector (purple) perpendicular to that, some distance in front of the camera, and with a length equal to the width of the viewing window. If if we were to draw lines from each end of this vector to the camera, the area inside would be our field of vision.
    The distance of this vector from the camera (ie. the length of the orange vector) defines how wide the field of vision is - the shorter the distance, the wider the field of vision (we just pick a value that looks "right").

    Next, let's calculate the vector perpendicular to the camera direction vector. To do this, we simply swap the X and Y components, inverting one of them. Note that there are two possibilities - either 90 degrees to the left or 90 degrees to the right - so we'll pick the vector to the right.
    perpCamDirX = -1 * camDirY
    perpCamDirY = camDirX

    Please login to see this picture.

    Now, we need the vector from the camera to the sprite (white).
    camToSprX = spritePosX - cameraPosX
    camToSprY = spritePosY - cameraPosY

    Next comes the clever part. Instead of using the sprite's position relative to the X and Y axis, we can find its position relative to the camera direction (orange) and the vector perpendicular to that (blue). We can find the length of the light purple vector by calculating the "dot product" of the sprite's position vector and the camera direction vector:
    camToSprA = (camToSprX * camDirX ) + (camToSprY * camDirY)

    Note that if this value is negative, it means the sprite is behind the camera, and will therefore not be visible.

    And we can find the length of the light green vector by calculating the dot product of the sprite's position vector and the vector perpendicular to the camera direction:
    camToSprB = (camToSprX * perpCamDirX ) + (camToSprY * perpCamDirY)

    To get the gradient of the line from the camera to the sprite (white), we can divide the second of those values (green) by the first (purple).
    gradSpr = camToSprB / camToSprA

    Next, we need to find the gradient of a line from the camera to the right edge of the field of vision (red). We could do the same as before, but there's an easier way. We know that the length of the orange vector is simply our cameraDistance variable. We also know that the total length of the purple vector is equal to the width of the viewing window, so if we halve that we get the length of vector to the right of the camera direction. Divide one by the other, and we have the gradient of the red line:
    gradView = (viewWidth / 2) / cameraDistance

    Please login to see this picture.

    If we divide gradSpr by gradView, we get the position of the horizontal position of the sprite relative to the centre of the viewing window. A value of 0 means the sprite is in the centre of the view. A value of +1 means the sprite is at the right edge of the view. A value of -1 means the sprite is at the left edge of the view. A value outside the range -1 to +1 means the sprite is outside the field of view.

    If we add 1 and then divide the result by 2, we convert that to a value in the range 0 to 1, and if we multiply this value by the width of the viewing window, we get the horizontal position of the sprite on screen, in pixels (light mauve).

    It's basically the same for the vertical position, although things do start to get complicated if we add another degree of freedom for the camera (roll)... And then there's the small matter of scaling the sprite with distance...

    Edited 3 times, last by MuddyMole (December 29, 2022 at 4:33 PM).

Participate now!

Don’t have an account yet? Register yourself now and be a part of our community!