I’d need a bit more info first, but I think I have the gist of it. This is a long read, so I apologize! This is my warm-up before I settle into some math calculations I have to set up, I needed something to get my brain going.
So to be clear - this is a modeled terrain, not a Unity terrain? Did you model it yourself, or use say, a voxel program? How experienced with code are you? Basically speaking - a vertex on a mesh (that’s the individual point, the triangles are 3 vertexes where it draws between them) has data associated with it.
One such piece of data is vertex colors. These are either 4 8-bit floats or 4 32-bit floats, depending on the precision used. For each vertex, there’s also a normal stored. That’s 3 floats. In addition to that, there’s tangents. That’s 4 floats, but it’s auto-normalized with surface shaders, so I would not touch it.
Additionally, there’s the UV channels. These are 4 sets of 2 floats. You’re actually using them for textures already - the two floats are basically a 2d mapping to texture coordinates, usually. However, you do not need to use them for that - they can be used for anything. You use them currently individually for each texture, but you can use the same texture coordinates for different textures - instead of having a different set for each. Example at the very bottom.
Check out this page, and you’ll see all the parameters. They can be assigned through scripts, or set through a modeling program. I don’t know exactly how you are painting textures, but painting UV’s can be done exactly the same way - it’s just accessing a different data set. Basically, instead of using vertex colors in a fancy way, you can avoid all the special forms of encoding and just use additional data on the mesh.
Now, for the Unity libraries…
There’s the built in shaders. However, it’s also stored inside your Unity installation in “Unity\Editor\Data\CGIncludes”. Don’t touch those, though, if you want to edit them, edit the built in ones, and just reference them instead.
Here’s a snippet from one my terrain shaders;
Pass {
Name "DEFERRED"
Tags {
"LightMode"="Deferred"
}
CGPROGRAM
#pragma hull hull
#pragma domain domain
#pragma vertex tessvert
#pragma fragment frag
#define UNITY_PASS_DEFERRED
#pragma multi_compile __ TEX0
#pragma multi_compile __ TEX1
#pragma multi_compile __ TEX2
#pragma multi_compile __ TEX3
#pragma multi_compile __ TEX4
#pragma multi_compile __ TEX5
#pragma multi_compile __ TEX6
#pragma multi_compile __ TEX7
#pragma multi_compile __ TEX8
#pragma multi_compile __ TEX9
#pragma multi_compile __ TEX10
#pragma multi_compile __ TEX11
#include "UnityCG.cginc"
#include "Tessellation.cginc"
#include "UnityPBSLighting.cginc"
#include "UnityStandardBRDF.cginc"
#pragma fragmentoption ARB_precision_hint_fastest
#pragma multi_compile_shadowcaster
#pragma exclude_renderers xbox360 ps3
#pragma target 5.0
#include "DeferredMain.cginc"
ENDCG
}
The key part her is how the #include works. You can see how they go there. Basically, when you #include a cginc file, it inserts the code from that file into that spot. So for the UnityCG.cginc, what it’s inserting into the compiled shader is a bunch of constants, settings, and a bunch of functions. It doesn’t have any of the code that is ‘ran’, but it is similar to including a file in C where it will include the functions from that file. If you go through the UnityCG.cginc file, you’ll see functions for “EncodeFloatRG”, “EncodeFloatRGBA”, etc, along with decodes.
The bulk of my code for the main pass, then, actually lies in the DeferredMain.cginc.
I won’t put all of it here, as I have some nested includes, too… but in my vertex input, I do this;
struct VertexInput {
float4 vertex : POSITION;
float3 normal : NORMAL;
float2 texcoord0 : TEXCOORD0;
float2 texcoord1 : TEXCOORD1;
float2 texcoord2 : TEXCOORD2;
float2 texcoord3 : TEXCOORD3;
float4 vertexColor : COLOR;
};
When you use a surface shader, the appdata_full will automatically include the UV channels for the other sets of texture coordinates. And like I mentioned, they don’t have to be texture coordinates - the shader interprets them however it would like.
Here’s where I get the diffuse color;
Texture2D<float4> _Diffuse0; uniform float4 _Diffuse0_ST; SamplerState sampler_Diffuse0;
Texture2D<float4> _Diffuse1; uniform float4 _Diffuse1_ST;
Texture2D<float4> _Diffuse2; uniform float4 _Diffuse2_ST;
Texture2D<float4> _Diffuse3; uniform float4 _Diffuse3_ST;
Texture2D<float4> _Diffuse4; uniform float4 _Diffuse4_ST;
Texture2D<float4> _Diffuse5; uniform float4 _Diffuse5_ST;
Texture2D<float4> _Diffuse6; uniform float4 _Diffuse6_ST;
Texture2D<float4> _Diffuse7; uniform float4 _Diffuse7_ST;
Texture2D<float4> _Diffuse8; uniform float4 _Diffuse8_ST;
Texture2D<float4> _Diffuse9; uniform float4 _Diffuse9_ST;
Texture2D<float4> _Diffuse10; uniform float4 _Diffuse10_ST;
Texture2D<float4> _Diffuse11; uniform float4 _Diffuse11_ST;
float3 GetTextureDiffuse( float2 UV, float4 vertexColor, float4 uv0 , float4 uv1 )
{
float4 tex = float4(0,0,0,0);
#ifdef TEX0
if (vertexColor.r > .01)
{
tex += _Diffuse0.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse0)) * vertexColor.r;
}
#endif
#ifdef TEX1
if (vertexColor.g > .01)
{
tex += _Diffuse1.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse1)) * vertexColor.g;
}
#endif
#ifdef TEX2
if (vertexColor.b > .01)
{
tex += _Diffuse2.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse2)) * vertexColor.b;
}
#endif
#ifdef TEX3
if (vertexColor.a > .01)
{
tex += _Diffuse3.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse3)) * vertexColor.a;
}
#endif
#ifdef TEX4
if (uv0.r > .01)
{
tex += _Diffuse4.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse4)) * uv0.r;
}
#endif
#ifdef TEX5
if (uv0.g > .01)
{
tex += _Diffuse5.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse5)) * uv0.g;
}
#endif
#ifdef TEX6
if (uv0.b > .01)
{
tex += _Diffuse6.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse6)) * uv0.b;
}
#endif
#ifdef TEX7
if (uv0.a > .01)
{
tex += _Diffuse7.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse7)) * uv0.a;
}
#endif
#ifdef TEX8
if (uv1.r > .01)
{
tex += _Diffuse8.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse8)) * uv1.r;
}
#endif
#ifdef TEX9
if (uv1.g > .01)
{
tex += _Diffuse9.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse9)) * uv1.g;
}
#endif
#ifdef TEX10
if (uv1.b > .01)
{
tex += _Diffuse10.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse10)) * uv1.b;
}
#endif
#ifdef TEX11
if (uv1.a > .01)
{
tex += _Diffuse11.Sample(sampler_Diffuse0, TRANSFORM_TEX(UV, _Diffuse11)) * uv1.a;
}
#endif
return tex.rgb;
}
Now, not all of that would make sense at first. I combine the UV0, 1, 2, and 3 into two float4’s when I pass the data from the vertex to the fragment (in a surface shader, that’s a vertex to a surface). So it’s the same info though. I also use multi_compile to optimize the textures, so it only ever uses a shader variant that loads as many textures as it has. That is controlled by scripts, though, separately, the shader cannot do anything to control that on it’s own easily.
The other is that I’m using direct HLSL, and you can replace all the “.Sample…” with tex2D just fine. It’s a different syntax which is windows only.
Anyways, to close up; read through this stuff here;
That should give you a good overview. I’ve dumped a lot of info on you, but I hope it helps. The main thing is that you can take the UV channels on the meshes and assign them to mean something.
Quick (won’t compile, though) example!
Shader "Example/Custom Vertex Data"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf Lambert vertex:vert
struct Input {
float2 uv_MainTex;
float4 color1;
float4 color2;
float4 color3;
};
void vert (inout appdata_full v, out Input o)
{
UNITY_INITIALIZE_OUTPUT(Input,o);
o.color1 = float4(v.texcoord0, v.texcoord1);
o.color2 = float4(v.texcoord2, v.texcoord3);
o.color3 = v.color;
}
sampler2D _MainTex;
void surf (Input IN, inout SurfaceOutput o)
{
float4 tex = float4(0,0,0,0);
tex += tex2D (_MainTex, IN.uv_MainTex) * IN.color1.x;
tex += tex2D (_SecondTex, IN.uv_MainTex) * IN.color1.y;
// blah blah
}
ENDCG
}
Fallback "Diffuse"
}