Voxel Texture array with shadows

I spend way to long trying to figure this out below is how i got it to work

when creating your mesh use

        mesh.SetUVs(0, Uvs );

uvs are a vector 3 with the z axis set to the image index
this.GetComponent<MeshRenderer>().sharedMaterial.SetTexture("_Tex2DArray", texture); use this to set the texture array in the shader

creating the texture map

  var textureMap = new Texture2DArray(200, 200, textureMaps.Count, TextureFormat.RGB24, false, false);
            textureMap.filterMode = FilterMode.Point;
            textureMap.wrapMode = TextureWrapMode.Repeat;
            for (int i = 0; i < textureMaps.Count; i++)
            {
                textureMaps.ElementAt(i).Value.Texture2DLocation = i;

                textureMap.SetPixels(textureMaps.ElementAt(i).Value.Texture2D.GetPixels(), i);

            }

            textureMap.Apply();
            this.chunkTexure = textureMap;

and finally the shader

Shader "Lit/Diffuse With Shadows"
{
    Properties
    {
        _Tex2DArray("Tex2DArray (RGB)", 2DArray) = "white" {}
    }
        SubShader
    {
        Pass
        {
            Tags {"LightMode" = "ForwardBase"}
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

        // compile shader into multiple variants, with and without shadows
        // (we don't care about any lightmaps yet, so skip these variants)
        #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
        // shadow helper functions and macros
        #include "AutoLight.cginc"

        struct v2f
        {
            float3 uv : TEXCOORD0;
            SHADOW_COORDS(1) // put shadows data into TEXCOORD1
            fixed3 diff : COLOR0;
            fixed3 ambient : COLOR1;
            float4 pos : SV_POSITION;
        };
        v2f vert(appdata_base v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            half3 worldNormal = UnityObjectToWorldNormal(v.normal);
            half nl = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
            o.diff = nl * _LightColor0.rgb;
            o.ambient = ShadeSH9(half4(worldNormal,1));
            // compute shadows data
            TRANSFER_SHADOW(o)
            return o;
        }

        sampler2D _MainTex;
        UNITY_DECLARE_TEX2DARRAY(_Tex2DArray);
        fixed4 frag(v2f i) : SV_Target
        {
            fixed4 col = UNITY_SAMPLE_TEX2DARRAY(_Tex2DArray, i.uv);
        // compute shadow attenuation (1.0 = fully lit, 0.0 = fully shadowed)
        fixed shadow = SHADOW_ATTENUATION(i);
        // darken light's illumination with shadow, keep ambient intact
        fixed3 lighting = i.diff * shadow + i.ambient;
        col.rgb *= lighting;
        return col;
    }
    ENDCG
}

// shadow casting support
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"
    }
}

1 Like

You've done some really interesting things there! The shadows are one thing — and they do look nice — but I'm more intrigued by what you've done with the 3D UV's. I've made a voxel engine in Unity before, but I put all the voxel textures into one big texture map, and then used traditional 2D UVs to select the tiny part of that map for each square.

Do I understand correctly that you're instead using a texture array, with a different element for each square texture? And you select these with the Z value of your 3D UV coordinates, so I guess the X and Y values are always in the range 0-1?

[quote=“JoeStrout”, post:3, topic: 727954]
You’ve done some really interesting things there! The shadows are one thing — and they do look nice — but I’m more intrigued by what you’ve done with the 3D UV’s. I’ve made a voxel engine in Unity before, but I put all the voxel textures into one big texture map, and then used traditional 2D UVs to select the tiny part of that map for each square.

Do I understand correctly that you’re instead using a texture array, with a different element for each square texture? And you select these with the Z value of your 3D UV coordinates, so I guess the X and Y values are always in the range 0-1?
[/quote]
yes exactly 00 01 10 11 for the uvs texture atlas has some drawbacks on the size of the atlas and the filtering mode with this even a small texture of 30px by 30px can have better filtering

yes this is the code for the texture loader

  public void GenerateChunkTextureMap()
        {
            List<TextureMap> maps = new List<TextureMap>();
           foreach(var item in _directories)
            {
                foreach (var image in System.IO.Directory.GetFiles(item.DirectoryName).Where(o => o.EndsWith(".png")))
                {
                    var data = System.IO.File.ReadAllBytes(image);
                    var texture = new Texture2D(0, 0);
                    texture.LoadImage(data);
                    if (texture.width != 32 || texture.height != 32)
                        continue;

                    var map = new TextureMap();
                    map.Prefix = item.Prefix;
                    map.Texture = texture;
                    map.FileLocation = image;
                    map.Name = Regex.Match(image, @"(?<=\\).+(?=(\.png))").Value;
                    maps.Add(map);

                }
            }

            var sq = (int) Math.Ceiling(Math.Sqrt(maps.Count));

            var textureMap = new Texture2D(sq*32,sq*32);
            // System.IO.File.WriteAllBytes("test.png", textureMap.EncodeToPNG());
            var index = 0;

            for (int x= 0;x<sq;x++)
                for(int y= 0; y < sq; y++)
                {if (index>maps.Count-1)
                        continue;
                    var image = maps[index];
                    image.Xpos = x;
                    image.Ypos = y;


                    textureMap.SetPixels(x  *32, y  * 32, 32, 32, image.Texture.GetPixels());


                    index++;

                }
            this._textureMaps = new Dictionary<string, TextureMap>();
            foreach(var item in maps)
            {
                this._textureMaps.Add($"{item.Prefix}_{item.Name}", item);
            }
            this.chunkTexture = textureMap;
        }

    }

and the code for the entire chunk class

using System;
using System.Collections.Generic;
using System.Linq;
using V3 = UnityEngine.Vector3;
using V2 = UnityEngine.Vector2;
using UnityEngine;

public class Chunk
{
    private static readonly int chunkWidth = 12;
    private static readonly int chunkHeight = 12;
    private Mesh mesh { get; set; }
    public byte[,,] voxels = new byte[chunkWidth, chunkHeight, chunkWidth];
    public Mesh GetMesh() => mesh;
    private static System.Random random = new System.Random();
    public void GenerateChunk()
    {
        for(int x=0;x<chunkWidth;x++)
            for (int y = 0; y < chunkHeight; y++)
                for (int z = 0; z < chunkWidth; z++)
                {

                    voxels[x, y, z] = random.Next(2)==1? (byte)1 : (byte)0;
                }
    }
    public void RedrawChunk()
    {
        var mesh = new MultiVoxMesh();

        for (int x = 0; x < chunkWidth; x++)
            for (int y = 0; y < chunkHeight; y++)
                for (int z = 0; z < chunkWidth; z++)
                {
                    if (voxels[x, y, z] == 0)
                        continue;


                    if (x == 0 || voxels[x - 1, y, z] == 0)
                        mesh.Inject(FaceDefinitions.LeftFace, x, y, z);

                    if (y == 0 || voxels[x, y - 1, z] == 0)
                        mesh.Inject(FaceDefinitions.BottomFace, x, y, z);

                    if (z == 0 || voxels[x, y, z - 1] == 0)
                        mesh.Inject(FaceDefinitions.FrontFace, x, y, z);


                    if(y==chunkHeight-1||voxels[x,y+1,z]==0)
                         mesh.Inject(FaceDefinitions.TopFace, x, y, z);

                    if (x == chunkWidth - 1 || voxels[x + 1, y, z] == 0)
                        mesh.Inject(FaceDefinitions.RightFace, x, y, z);

                    if (z == chunkWidth - 1 || voxels[x, y , z + 1] == 0)
                        mesh.Inject(FaceDefinitions.BackFace, x, y, z);







                }
        this.mesh = mesh.ToMesh();
  }
}
public class CubeFace
{
    public V3[] Verticies = { };
    public int[] Triangles = { };
    public V2[] Uvs = { };
}
public class FaceDefinitions
{
    private static readonly int[] normal = { 1, 3, 0, 2, 0, 3 };
    private static readonly int[] inverted = {1,0,3,2,3,0 };
    public static CubeFace BottomFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(0,0,0),//0
            new V3(1,0,0),//1
            new V3(0,0,1),//2
            new V3(1,0,1)//3

        },
        Triangles = normal,
        Uvs = new V2[] {
            new V2(0,0),
            new V2(0,1),
            new V2(1,0),
            new V2(1,1)

        }



    };

    public static CubeFace TopFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(0,1,0),//0
            new V3(1,1,0),//1
            new V3(0,1,1),//2
            new V3(1,1,1)//3

        },
        Triangles = inverted,
        Uvs = new V2[] {
            new V2(0,0),
            new V2(0,1),
            new V2(1,0),
            new V2(1,1)

        }



    };
    public static CubeFace BackFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(0,0,1),//0
            new V3(1,0,1),//1
             new V3(0,1,1),//0
            new V3(1,1,1)


        },
        Triangles = normal,
        Uvs = new V2[] {
            new V2(0,0),
            new V2(0,1),
            new V2(1,0),
            new V2(1,1)

        }



    };
    public static CubeFace FrontFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(0,0,0),//0
            new V3(1,0,0),//1
             new V3(0,1,0),//0
            new V3(1,1,0)


        },
        Triangles = inverted,
        Uvs = new V2[] {
            new V2(0,0),
            new V2(0,1),
            new V2(1,0),
            new V2(1,1)

        }



    };
    public static CubeFace RightFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(1,0,0),
             new V3(1,1,0),
              new V3(1,0,1),
             new V3(1,1,1)
        },
        Triangles = normal,
        Uvs = new V2[] {//11 01 10 00
            new V2(1,1),
            new V2(0,1),
new V2(1,0),

new V2(0,0)


        }
    };

    public static CubeFace LeftFace = new CubeFace()
    {
        Verticies = new V3[]{
            new V3(0,0,0),
             new V3(0,1,0),
              new V3(0,0,1),
             new V3(0,1,1)
        },
        Triangles = inverted,
        Uvs = new V2[] {//11 01 10 00
            new V2(1,1),
            new V2(0,1),
new V2(1,0),

new V2(0,0)



        }
    };
}

public class MultiVoxMesh
{
    private List<V3> verticies { get; set; }
    private List<int> triangles { get; set; }
    private List<V2> uvs { get; set; }
    public MultiVoxMesh()
    {
        verticies = new List<V3>();
        triangles = new List<int>();
        uvs = new List<V2>();

    }
    public void InjectTriangles(int[] triangles)
    {
        int currentIndex = verticies.Count;

        this.triangles.AddRange(triangles.Select(o => o + currentIndex));

    }
    public void InjectUvs(V2[] uvs) => this.uvs.AddRange(uvs);
    public void InjectVectors(V3[] face,int x , int y, int z)=>this.verticies.AddRange(face.Select(o => new V3( o.x + x - 0.5f, o.y + y - 0.5f,o.z + z - 0.5f)  ));
    public void Inject(CubeFace face, int x, int y, int z)
    {
        this.InjectTriangles(face.Triangles);
        this.InjectVectors(face.Verticies,x,y,z);
        this.InjectUvs(face.Uvs);

    }
    public Mesh ToMesh()
    {
        Mesh mesh = new Mesh();
        mesh.vertices = this.verticies.ToArray();
        mesh.triangles = this.triangles.ToArray();
        mesh.uv = this.uvs.ToArray();
        return mesh;
    }

}
1 Like

That's really neat.

As far as I know, there are currently no good, well-maintained voxel assets in the Asset Store. You should consider cleaning & documenting this and making it available there!

yeah thats not a bad idea

1 Like

Please notify me if you do. Even though I have my own voxel code sitting around somewhere, I like your approach better!

I'm currently working on a Scratch-like environment in VR. One of the features I'm thinking about adding someday is a voxel chunk. You'd define a bunch of block types in terms of the 6 face textures they show, and then have script pieces to set/get the block type at any xyz position in the chunk. Pretty straightforward, but I think scripters would really get into it.