

I managed to write a lowpoly shader for the built-in Unity terrain. The shadows were troublesome because I’m not using the URP (Universal render Pipeline) and I was writing the code and using macros that work with that renderer.
Here’s the shader code:
Shader "Custom/FlatShading"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma target 4.0
#include "UnityCG.cginc"
#include "Lighting.cginc"
#pragma require geometry
#pragma vertex vert
#pragma geometry geom
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "AutoLight.cginc"
struct appdata
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
struct v2g
{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 worldPos : TEXCOORD1;
SHADOW_COORDS(2)
};
struct g2f
{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 normal : TEXCOORD1;
SHADOW_COORDS(2)
float3 worldPos : TEXCOORD3;
};
sampler2D _MainTex;
fixed4 _Color;
//float4 _LightColor0;
v2g vert(appdata v)
{
v2g o;
o.pos = UnityObjectToClipPos(v.pos);
o.uv = v.uv;
o.worldPos = mul(unity_ObjectToWorld, v.pos).xyz; // Convert to world position
TRANSFER_SHADOW( o );
return o;
}
[maxvertexcount(3)]
void geom(triangle v2g input[3], inout TriangleStream<g2f> output)
{
// Calculate the surface normal using cross product
float3 v0 = input[0].worldPos;
float3 v1 = input[1].worldPos;
float3 v2 = input[2].worldPos;
float3 normal = normalize(cross(v1 - v0, v2 - v0));
for (int i = 0; i < 3; ++i)
{
g2f o;
o.pos = input[i].pos;
o.uv = input[i].uv;
o.normal = normal;
o.worldPos = input[i].worldPos;
TRANSFER_SHADOW( o );
output.Append(o);
}
}
half4 frag(g2f i) : SV_Target
{
//return fixed4(i._ShadowCoord.x, i._ShadowCoord.y, i._ShadowCoord.z, 1);
float3 normal = normalize( i.normal );
float shadow = SHADOW_ATTENUATION( i );
// Simple Lambertian reflectance for flat shading
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz);
float diff = max(0, dot(normal, lightDir));
return _Color * tex2D( _MainTex, i.uv ) * diff * shadow;
}
ENDCG
}
}
FallBack "Diffuse"
}
This is what I just got working so it may still have problems. But it seems to work.
Also note that there is no way in Unity 6 to set the shader for the built-in terrain using the editor. I had to write a script that would do it at startup. here’s that script:
using UnityEngine;
public class ChangeTerrainMaterial : MonoBehaviour
{
public Terrain terrain; // Drag your terrain object here in the Inspector
public Material terrainMaterial; // Drag your custom material here in the Inspector
void Start()
{
if (terrain != null && terrainMaterial != null)
{
terrain.materialTemplate = terrainMaterial;
}
else
{
Debug.LogWarning("Please assign both the terrain and the material in the Inspector.");
}
}
}
This isn’t too difficult to understand. Just set the settings/variables in the editor and at runtime, the code will set the terrain to use the shader of your choice. Here’s a picture of the terrain with a single “mountain”:

And finally, here’s a video of the shader working on the built-in terrain: