I’m new to creating custom shaders in the URP. My project involves creating procedural terrain using noise algorithms for elevation. I’ve also implemented a biome system using Voronoi diagrams.
While I’ve successfully visualized the biomes using Unity’s Gizmo class, my next goal is to texture the Voronoi regions with corresponding biome textures. I have attempted to accomplish this by creating a texture atlas in Unity and sampling from it in the shader. However, I’m running into issues with the SetVectorArray function and array handling in the shader.
public void SetMaterialProperties (Material mat) {
biomeTex.Clear ();
for (int i = 0; i < biomes.Count; i++) {
biomeTex.Add (biomes[i].texture);
}
mat.SetTexture ("_BiomeTextureAtlas", CreateTextureAtlas (biomeTex));
// Send Voronoi points to shader
Vector4[] voronoiPointsArray = new Vector4[256]; // Ensure a size of 256
for (int i = 0; i < biomePoints.Count && i < 256; i++) {
voronoiPointsArray[i] = new Vector4 (biomePoints[i].x, biomePoints[i].y, biomePoints[i].z, 0);
}
mat.SetVectorArray ("_VoronoiPoints", voronoiPointsArray);
}
Error Message:
The console outputs the following message:
Property (_VoronoiPoints) exceeds previous array size (256 vs 1). Cap to previous size. Restart Unity to recreate the arrays.
UnityEngine.Material:SetVectorArray (string,UnityEngine.Vector4[])
MeshGen:SetMaterialProperties (UnityEngine.Material) (at Assets/Scripts/MeshGen.cs:69)
MeshGen:CreateMesh (MeshGen,int) (at Assets/Scripts/MeshGen.cs:179)
MeshGenEditor:OnInspectorGUI () (at Assets/Scripts/Editor/MeshGenEditor.cs:22)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
It seems like there’s a mismatch between the array lengths in the shader and the one I’m passing in. I don’t understand how this can be.
What does the message mean, particularly about the array size (256 vs 1)?
Is SetVectorArray the correct function for what I’m trying to achieve?
How can I successfully pass an array of biome textures to the shader and sample from it?
I appreciate any help or insights.
Code:
using System.Collections.Generic;
using UnityEngine;
public class MeshGen : MonoBehaviour {
public bool autoUpdate;
public Vector2Int meshSize = Vector2Int.one;
public Vector2Int voronoiWindowSize = Vector2Int.one;
public int subdivisions = 1;
public List<NoiseLayer> noiseLayers;
public Material meshMaterial;
public bool generateOnStart;
public Material waterMaterial;
public float seaLevel = 0;
public Texture baseTexture;
public Texture seaFloorTexture;
[Range (0, 1)] public float transitionSmoothness = 0.5f;
public Mesh mesh;
public Vector3[] vertices;
public int[] triangles;
public Vector2[] uvs;
public MeshFilter meshFilter;
public MeshRenderer meshRenderer;
[HideInInspector] public GameObject waterPlane;
public float voronoiRegionSize = 5.0f;
public List<Vector3> voronoiPoints = new List<Vector3> ();
[HideInInspector] public List<int> voronoiPointBiomeTypes = new List<int> ();
[System.Serializable]
public class NoiseLayer {
public float frequency = 1;
public float amplitude = 1;
public float offsetX = 0;
public float offsetY = 0;
public bool active = true;
}
[System.Serializable]
public class Biome {
public string name;
public Texture2D texture;
public Color debugColor;
}
public List<Biome> biomes;
public int seed;
List<Texture2D> biomeTex = new List<Texture2D> ();
void Start () {
if (generateOnStart) {
CreateMesh (this, seed);
}
}
public void SetMaterialProperties (Material mat) {
biomeTex.Clear ();
for (int i = 0; i < biomes.Count; i++) {
biomeTex.Add (biomes[i].texture);
}
mat.SetTexture ("_BiomeTextureAtlas", CreateTextureAtlas (biomeTex));
// Send Voronoi points to shader
Vector4[] voronoiPointsArray = new Vector4[256]; // Ensure a size of 256
for (int i = 0; i < voronoiPoints.Count && i < 256; i++) {
voronoiPointsArray[i] = new Vector4 (voronoiPoints[i].x, voronoiPoints[i].y, voronoiPoints[i].z, 0);
}
mat.SetVectorArray ("_VoronoiPoints", voronoiPointsArray);
}
void OnDrawGizmos () {
// Draw bounding box
Gizmos.color = Color.white;
Gizmos.DrawWireCube (Vector3.zero, new Vector3 (voronoiWindowSize.x, 0, voronoiWindowSize.y));
// Draw Voronoi points
for (int i = 0; i < voronoiPoints.Count; i++) {
// Change color based on biome type
int biomeType = voronoiPointBiomeTypes[i];
if (biomeType >= 0 && biomeType < biomes.Count) {
Gizmos.color = biomes[biomeType].debugColor;
}
Gizmos.DrawSphere (voronoiPoints[i], 0.5f);
}
// Draw Voronoi regions with biome debug colors
if (vertices != null) {
foreach (Vector3 vertex in vertices) {
int closestPointIndex = FindClosestVoronoiPoint (voronoiPoints, vertex);
int biomeType = voronoiPointBiomeTypes[closestPointIndex];
if (biomeType >= 0 && biomeType < biomes.Count) {
Gizmos.color = biomes[biomeType].debugColor;
}
Gizmos.DrawCube (vertex, Vector3.one * 0.1f);
}
}
}
public static void GenerateVoronoiPoints (MeshGen meshGen, int seed) {
meshGen.voronoiPoints.Clear ();
meshGen.voronoiPointBiomeTypes.Clear ();
System.Random pseudoRandom = new System.Random (seed);
int numberOfBiomes = meshGen.biomes.Count;
Vector2Int size = meshGen.voronoiWindowSize;
float voronoiRegionSize = meshGen.voronoiRegionSize;
int pointsX = Mathf.FloorToInt (size.x / 1.0f);
int pointsY = Mathf.FloorToInt (size.y / 1.0f);
// Calculate the offsets to center the Voronoi points
float offsetX = (-0.5f * voronoiRegionSize * pointsX);
float offsetZ = (-0.5f * voronoiRegionSize * pointsY);
for (int i = 0; i < pointsX; i++) {
for (int j = 0; j < pointsY; j++) {
float randomX = (float) pseudoRandom.NextDouble ();
float randomZ = (float) pseudoRandom.NextDouble ();
float x = (i + randomX) * voronoiRegionSize + offsetX;
float z = (j + randomZ) * voronoiRegionSize + offsetZ;
meshGen.voronoiPoints.Add (new Vector3 (x, 0, z));
int biomeType = pseudoRandom.Next (0, numberOfBiomes);
meshGen.voronoiPointBiomeTypes.Add (biomeType); // Store biome type for this point
}
}
}
public static void CreateOrUpdateWater (MeshGen meshGen) {
Vector2Int size = meshGen.meshSize;
// Look for existing water plane under this GameObject
Transform existingWaterPlane = meshGen.transform.Find ("WaterPlane");
if (existingWaterPlane != null) {
meshGen.waterPlane = existingWaterPlane.gameObject;
} else {
meshGen.waterPlane = GameObject.CreatePrimitive (PrimitiveType.Plane);
meshGen.waterPlane.name = "WaterPlane";
meshGen.waterPlane.transform.parent = meshGen.transform;
}
meshGen.waterPlane.transform.position = new Vector3 (0f, meshGen.seaLevel, 0f);
meshGen.waterPlane.transform.localScale = new Vector3 (size.x / 10.0f, 1, size.y / 10.0f);
MeshRenderer waterRenderer = meshGen.waterPlane.GetComponent<MeshRenderer> ();
if (meshGen.waterMaterial != null) {
waterRenderer.material = meshGen.waterMaterial;
}
}
public static void CreateMesh (MeshGen meshGenInstance, int seed) {
GenerateVoronoiPoints (meshGenInstance, seed);
CreateOrUpdateWater (meshGenInstance);
if (meshGenInstance.meshFilter == null) {
meshGenInstance.meshFilter = meshGenInstance.gameObject.GetComponent<MeshFilter> ();
if (meshGenInstance.meshFilter == null) {
meshGenInstance.meshFilter = meshGenInstance.gameObject.AddComponent<MeshFilter> ();
}
}
if (meshGenInstance.meshRenderer == null) {
meshGenInstance.meshRenderer = meshGenInstance.gameObject.GetComponent<MeshRenderer> ();
if (meshGenInstance.meshRenderer == null) {
meshGenInstance.meshRenderer = meshGenInstance.gameObject.AddComponent<MeshRenderer> ();
}
}
if (meshGenInstance.meshMaterial != null) {
if (Application.isPlaying) {
meshGenInstance.SetMaterialProperties (meshGenInstance.meshRenderer.material);
} else {
meshGenInstance.SetMaterialProperties (meshGenInstance.meshRenderer.sharedMaterial);
}
}
meshGenInstance.mesh = new Mesh ();
meshGenInstance.meshFilter.mesh = meshGenInstance.mesh;
int xSize = meshGenInstance.meshSize.x * (int) Mathf.Pow (2, meshGenInstance.subdivisions);
int zSize = meshGenInstance.meshSize.y * (int) Mathf.Pow (2, meshGenInstance.subdivisions);
meshGenInstance.vertices = new Vector3[(xSize + 1) * (zSize + 1)];
meshGenInstance.uvs = new Vector2[meshGenInstance.vertices.Length];
// Calculate offsets to ensure the mesh is centered at (0, 0)
float offsetX = xSize / 2.0f;
float offsetZ = zSize / 2.0f;
for (int i = 0, z = 0; z <= zSize; z++) {
for (int x = 0; x <= xSize; x++) {
float scaledX = (x - offsetX) / (float) Mathf.Pow (2, meshGenInstance.subdivisions);
float scaledZ = (z - offsetZ) / (float) Mathf.Pow (2, meshGenInstance.subdivisions);
float y = 0;
foreach (MeshGen.NoiseLayer layer in meshGenInstance.noiseLayers) {
if (layer.active) {
y += Mathf.PerlinNoise (scaledX * layer.frequency + layer.offsetX, scaledZ * layer.frequency + layer.offsetY) * layer.amplitude;
}
}
meshGenInstance.vertices[i] = new Vector3 (scaledX, y, scaledZ);
meshGenInstance.uvs[i] = new Vector2 ((float) x / xSize, (float) z / zSize);
i++;
}
}
meshGenInstance.triangles = new int[xSize * zSize * 6];
for (int ti = 0, vi = 0, y = 0; y < zSize; y++, vi++) {
for (int x = 0; x < xSize; x++, ti += 6, vi++) {
meshGenInstance.triangles[ti] = vi;
meshGenInstance.triangles[ti + 3] = meshGenInstance.triangles[ti + 2] = vi + 1;
meshGenInstance.triangles[ti + 4] = meshGenInstance.triangles[ti + 1] = vi + xSize + 1;
meshGenInstance.triangles[ti + 5] = vi + xSize + 2;
}
}
meshGenInstance.mesh.vertices = meshGenInstance.vertices;
meshGenInstance.mesh.uv = meshGenInstance.uvs;
meshGenInstance.mesh.triangles = meshGenInstance.triangles;
meshGenInstance.mesh.RecalculateNormals ();
}
public static int FindClosestVoronoiPoint (List<Vector3> voronoiPoints, Vector3 target) {
float minDistanceSqr = float.MaxValue;
int closestIndex = -1;
for (int i = 0; i < voronoiPoints.Count; i++) {
float distanceSqr = (voronoiPoints[i] - target).sqrMagnitude;
if (distanceSqr < minDistanceSqr) {
minDistanceSqr = distanceSqr;
closestIndex = i;
}
}
return closestIndex;
}
public static Texture2D CreateTextureAtlas (List<Texture2D> textures, int maxAtlasSize = 16) {
if (textures.Count == 0) {
return null;
}
int textureWidth = textures[0].width;
int textureHeight = textures[0].height;
int atlasWidth = textureWidth * maxAtlasSize;
int atlasHeight = textureHeight;
Texture2D atlas = new Texture2D (atlasWidth, atlasHeight);
for (int i = 0; i < textures.Count; i++) {
Color[] pixelData = textures[i].GetPixels ();
atlas.SetPixels (i * textureWidth, 0, textureWidth, textureHeight, pixelData);
}
Color[] blankPixels = new Color[textureWidth * textureHeight];
for (int i = textures.Count; i < maxAtlasSize; i++) {
atlas.SetPixels (i * textureWidth, 0, textureWidth, textureHeight, blankPixels);
}
atlas.Apply ();
return atlas;
}
}
Shader code:
Shader "Custom/VoronoiBiomeShader"
{
Properties
{
_BiomeTextureAtlas ("Biome Texture Atlas", 2D) = "white" {}
_VoronoiPoints ("Voronoi Points", Vector) = (0,0,0,0)
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry" }
Pass
{
Name "ForwardBase"
CGPROGRAM
#pragma target 3.0
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata_t
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
float4 vertex : SV_POSITION;
};
sampler2D _BiomeTextureAtlas;
float4 _VoronoiPoints[256]; // Set of voronoi points. Max 256
v2f vert(appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
float4 frag(v2f i) : SV_Target
{
//FindClosestVoronoiPoint
float closestDist = 99999999.0;
int closestPointIndex = -1;
for (int j = 0; j < 256; j++)
{
float dist = distance(i.worldPos, _VoronoiPoints[j].xyz);
if (dist < closestDist)
{
closestDist = dist;
closestPointIndex = j;
}
}
float atlasWidth = 1.0 / 16.0;
float2 biomeTexUV = float2(atlasWidth * closestPointIndex + i.uv.x * atlasWidth, i.uv.y);
float4 biomeColor = tex2D(_BiomeTextureAtlas, biomeTexUV);
return biomeColor;
}
ENDCG
}
}
}