Shaders would be easily capable of this, though you’ll want to be careful with your equations as, if each layer has their own, that can get expensive quickly with a lot of layers.
The short version is you get the world position of the pixel being drawn, take your equation and apply it to the worldPos.xz components of the position, and then test the resulting value against the worldPos.z.
Shader "Custom/GroundLayers" {
Properties {
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.0
[Header(Base Layer)]
_Color ("Top Color", Color) = (0.8,0.6,0.35,1)
[Header(Layer 1)]
_Color1 ("Color", Color) = (0.65,0,0,1)
_LayerHeight1 ("Layer Height", Float) = 0.165
_WaveScale1 ("Wave Height Scale", Float) = 0.025
_WaveScaleOffset1 ("Wave Frequency and Horizontal Offset", Vector) = (20, 20, 0, 0.5)
[Header(Layer 2)]
_Color2 ("Color", Color) = (0.5,0.5,0.5,1)
_LayerHeight2 ("Layer Height", Float) = -0.165
_WaveScale2 ("Wave Height Scale", Float) = 0.025
_WaveScaleOffset2 ("Wave Frequency and Horizontal Offset", Vector) = (19, 19, 0.33, 0.15)
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
float3 worldPos;
};
half _Glossiness;
fixed3 _Color, _Color1, _Color2;
float4 _WaveScaleOffset1, _WaveScaleOffset2;
float _WaveScale1, _WaveScale2, _LayerHeight1, _LayerHeight2;
float calcLayerMask(float3 worldPos, float4 waveScaleOffset, float waveHeightScale, float layerHeight)
{
// scale & offset the xz world position
float2 scaledWorldPlane = worldPos.xz * waveScaleOffset.xy + waveScaleOffset.zw;
// calculate wave pattern from 2D position
float waveHeight = sin(scaledWorldPlane.x) + cos(scaledWorldPlane.y);
// scale wave pattern and apply world height offset
waveHeight *= waveHeightScale;
waveHeight += layerHeight;
// screen space anti-aliasing
float layerHeightDerive = fwidth(waveHeight) * 1.5;
return smoothstep(worldPos.y - layerHeightDerive, worldPos.y + layerHeightDerive, waveHeight);
}
void surf (Input IN, inout SurfaceOutputStandard o) {
// lerp between base color and first layer's color
fixed3 layerColor = lerp(_Color, _Color1, calcLayerMask(IN.worldPos, _WaveScaleOffset1, _WaveScale1, _LayerHeight1));
// lerp between previous layers' colors and second layer's color
layerColor = lerp(layerColor, _Color2, calcLayerMask(IN.worldPos, _WaveScaleOffset2, _WaveScale2, _LayerHeight2));
fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
o.Albedo = c.rgb * layerColor;
o.Smoothness = _Glossiness;
}
ENDCG
}
FallBack "Diffuse"
}
This uses the same sine & cosine based wave pattern equation for both layers for simplicity sake. Being able to choose what pattern to use for each layer, or having more layers, gets more difficult, but is possible to a degree. You’d need to have every equation you want to use in the shader itself and choose which one to use based on another material property. If you want to have dynamic control over the number of layers you’d need to pass in the layer parameters as an array of values, which means you’ll need to do this from a custom script as you can’t have arrays as material properties (those in the Properties at the top of the shader file, that show up in the inspector and are saved on the material asset), but can set array values on materials from a custom script.