diff --git a/AGXUnity/Model/MovableTerrain.cs b/AGXUnity/Model/MovableTerrain.cs
index 047fd4fb..cff4b4e2 100644
--- a/AGXUnity/Model/MovableTerrain.cs
+++ b/AGXUnity/Model/MovableTerrain.cs
@@ -17,6 +17,7 @@ public abstract class MovableAdapter : DeformableTerrainBase
[AddComponentMenu( "AGXUnity/Model/Movable Terrain" )]
[RequireComponent( typeof( MeshFilter ) )]
[RequireComponent( typeof( MeshRenderer ) )]
+ [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#movable-terrain" )]
[DisallowMultipleComponent]
public class MovableTerrain : MovableAdapter
{
diff --git a/AGXUnity/Model/TerrainMaterialPatch.cs b/AGXUnity/Model/TerrainMaterialPatch.cs
index dd676e98..73040465 100644
--- a/AGXUnity/Model/TerrainMaterialPatch.cs
+++ b/AGXUnity/Model/TerrainMaterialPatch.cs
@@ -1,4 +1,5 @@
using AGXUnity.Collide;
+using AGXUnity.Rendering;
using System.Linq;
using UnityEngine;
@@ -72,6 +73,14 @@ public ShapeMaterial MaterialHandle
}
}
+ [SerializeField]
+ private Texture2D m_renderTexture = null;
+
+ public Texture2D RenderTexture {
+ get => m_renderTexture;
+ set { m_renderTexture = value; }
+ }
+
///
/// Whether to disable collision shapes used to define this patch during initialization.
///
@@ -84,6 +93,12 @@ public ShapeMaterial MaterialHandle
[field: SerializeField]
public bool DisableVisuals { get; set; } = true;
+ ///
+ /// Whether to set child shape visuals to the default terrain patch shape material
+ ///
+ [field: SerializeField]
+ public bool OverrideVisuals { get; set; } = true;
+
// The shapes used to define this patch.
public Collide.Shape[] Shapes { get => GetComponentsInChildren(); }
@@ -108,5 +123,17 @@ protected override bool Initialize()
return true;
}
+
+ private Material m_replaceMat = null;
+
+ public override void EditorUpdate()
+ {
+ if(OverrideVisuals) {
+ if ( m_replaceMat == null )
+ m_replaceMat = Resources.Load( @"Materials/TerrainPatchShapeMaterial" );
+ foreach( var visual in gameObject.GetComponentsInChildren() )
+ visual.SetMaterial( m_replaceMat );
+ }
+ }
}
}
\ No newline at end of file
diff --git a/AGXUnity/Rendering/TerrainPatchRenderer.cs b/AGXUnity/Rendering/TerrainPatchRenderer.cs
index 311e7028..fb4cf00b 100644
--- a/AGXUnity/Rendering/TerrainPatchRenderer.cs
+++ b/AGXUnity/Rendering/TerrainPatchRenderer.cs
@@ -2,170 +2,188 @@
using AGXUnity.Utils;
using System.Collections.Generic;
using UnityEngine;
+using UnityEngine.Rendering;
namespace AGXUnity.Rendering
{
- ///
- /// Wrapper class for storing/resetting initial state of TerrainData.
- /// This is by no means a complete store/restore, only the parts used by .
- ///
- class InitialTerrainData
+ [RequireComponent( typeof( DeformableTerrainBase ) )]
+ [DisallowMultipleComponent]
+ [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#using-different-terrain-materials" )]
+ public class TerrainPatchRenderer : ScriptComponent
{
- private float[,,] m_alphamaps;
- private TerrainLayer[] m_layers;
+ [SerializeField]
+ private SerializableDictionary m_explicitMaterialRenderMap = new SerializableDictionary();
- public InitialTerrainData( TerrainData td )
- {
- m_alphamaps = td.GetAlphamaps( 0, 0, td.alphamapWidth, td.alphamapHeight );
- m_layers = td.terrainLayers;
- }
+ [HideInInspector]
+ public SerializableDictionary ExplicitMaterialRenderMap => m_explicitMaterialRenderMap;
- public void Reset( TerrainData td )
+ public Dictionary ImplicitMaterialRenderMap
{
- if ( td != null ) {
- td.terrainLayers = m_layers;
- td.SetAlphamaps( 0, 0, m_alphamaps );
+ get
+ {
+ Dictionary res = new Dictionary();
+ foreach ( var patch in RenderedPatches )
+ if ( patch.TerrainMaterial != null && patch.RenderTexture != null )
+ res[ patch.TerrainMaterial ] = patch.RenderTexture;
+ return res;
}
}
- }
- [RequireComponent( typeof( DeformableTerrainBase ) )]
- [DisallowMultipleComponent]
- [HelpURL( "https://us.download.algoryx.se/AGXUnity/documentation/current/editor_interface.html#using-different-terrain-materials" )]
- public class TerrainPatchRenderer : ScriptComponent
- {
- private DeformableTerrainBase terrain;
- private float[,,] alphamap;
- private Dictionary m_materialMapping;
- private InitialTerrainData m_initialData;
-
- [SerializeField]
- private TerrainLayer m_defaultLayer;
-
- ///
- /// The deafult TerrainLayer to use to render the terrain in cells where no material patch is present
- /// or for patches which does not have an explicitly mapped layer.
- ///
- [IgnoreSynchronization]
- public TerrainLayer DefaultLayer
+ public Dictionary MaterialRenderMap
{
- get => m_defaultLayer;
- set
+ get
{
- if ( m_initialData != null )
- Debug.LogError( "Setting material TerrainLayers during runtime is not supported!" );
- else
- m_defaultLayer = value;
+ var res = ImplicitMaterialRenderMap;
+ foreach ( var (k, v) in ExplicitMaterialRenderMap )
+ res[ k ] = v;
+ return res;
}
}
[SerializeField]
- private SerializableDictionary m_materialRenderMap = new SerializableDictionary();
+ private bool m_reduceTextureTiling = false;
- ///
- /// Defines a map from DeformableTerrainMaterials to the TerrainLayers used to render patches of the specified terrain material.
- ///
- [HideInInspector]
- [IgnoreSynchronization]
- public SerializableDictionary MaterialRenderMap
- {
- get => m_materialRenderMap;
+ public bool ReduceTextureTiling {
+ get => m_reduceTextureTiling;
set
- {
- if ( m_initialData != null )
- Debug.LogError( "Setting material TerrainLayers during runtime is not supported!" );
- else
- m_materialRenderMap = value;
+ {
+ m_reduceTextureTiling = value;
+ if ( m_material != null )
+ m_material.SetKeyword(new LocalKeyword(m_material.shader,"REDUCE_TILING"), value);
}
}
+ public TerrainMaterialPatch[] RenderedPatches => gameObject.GetComponentsInChildren();
+
+ private Dictionary m_materialMapping;
+
+ private Terrain m_unityTerrain;
+ private DeformableTerrainBase m_terrain;
+ private Mesh m_mesh = null;
+ private Material m_material;
+
+ private bool m_changed = false;
+ private byte[] m_materialAtlas;
+ private Texture2D m_materialTexture;
+
protected override bool Initialize()
{
- terrain = gameObject.GetInitializedComponent();
- if ( terrain is MovableTerrain ) {
- Debug.LogError( "Terrain Patch Renderer does not yet support MovableTerrain!", this );
+ m_terrain = gameObject.GetInitializedComponent();
+ if ( m_terrain is not DeformableTerrain ) {
+ Debug.LogError( "Terrain Patch Renderer currently only supports DeformableTerrain!", this );
return false;
}
// The patches need to be initialized before the initial update pass, otherwise the materials might not yet have been added.
- foreach (var patch in gameObject.GetComponentsInChildren() )
+ foreach ( var patch in RenderedPatches )
patch.GetInitialized();
- var uTerr = GetComponent();
- var td = uTerr.terrainData;
+ m_unityTerrain = GetComponent();
+ var td = m_unityTerrain.terrainData;
- m_initialData = new InitialTerrainData( td );
+ m_mesh = new Mesh();
+ m_mesh.vertices = new Vector3[ 3 ];
+ m_mesh.triangles = new int[] { 0, 1, 2 };
- if ( DefaultLayer == null ) {
- Debug.LogError( "No DefaultLayer provided!", this );
- return false;
- }
+ m_material = new Material( Shader.Find( "AGXUnity/BuiltIn/TerrainPatchDecal" ) );
- // Initialize terrain layers: 0 is default, 1+ are mapped.
m_materialMapping = new Dictionary();
- var layers = new List { DefaultLayer };
int idx = 1;
foreach ( var (mat, tl) in MaterialRenderMap ) {
+ if ( idx == 5 ) {
+ Debug.LogWarning( "The TerrainDecalRenderer currently only supports rendering 4 patch materials. Further materials will not be rendered.", this );
+ break;
+ }
var terrMat = mat.GetInitialized().Native;
if ( terrMat != null ) {
- m_materialMapping.Add( mat.GetInitialized().Native, idx++ );
- layers.Add( tl );
+ m_materialMapping.Add( mat.GetInitialized().Native, idx );
+ if ( tl == null )
+ Debug.LogWarning( $"Terrain Material '{mat.name}' is mapped to null texture.", this );
+ m_material.SetTexture( $"_Decal{idx-1}", tl );
+ idx++;
}
}
- td.terrainLayers = layers.ToArray();
- alphamap = td.GetAlphamaps( 0, 0, td.alphamapWidth, td.alphamapHeight );
+ var size = td.size;
+ m_mesh.bounds = new Bounds( m_terrain.transform.position + size / 2.0f, size );
+ m_mesh.UploadMeshData( false );
- Simulation.Instance.StepCallbacks.SimulationPost += PostStep;
- terrain.OnModification += UpdateTextureAt;
+ m_materialAtlas = new byte[ td.heightmapResolution * td.heightmapResolution ];
+ m_materialTexture = new Texture2D( td.heightmapResolution, td.heightmapResolution, TextureFormat.R8, false );
+ m_materialTexture.filterMode = FilterMode.Point;
+ m_materialTexture.anisoLevel = 0;
- terrain.TriggerModifyAllCells();
+ m_material.SetTexture( "_Materials", m_materialTexture );
+ m_material.SetTexture( "_Heightmap", td.heightmapTexture );
+ m_material.SetVector( "_TerrainScale", td.size );
+ m_material.SetVector( "_TerrainPosition", m_terrain.transform.position );
+ m_material.SetFloat( "_TerrainResolution", td.heightmapResolution );
- td.SetAlphamaps( 0, 0, alphamap );
+ m_terrain.OnModification += UpdateTextureAt;
+ m_terrain.TriggerModifyAllCells();
+ PostStep();
+
+ Simulation.Instance.StepCallbacks.PostStepForward += PostStep;
return true;
}
- protected override void OnDestroy()
+ private void UpdateTextureAt( agxTerrain.Terrain aTerr, agx.Vec2i aIdx, UnityEngine.Terrain uTerr, Vector2Int uIdx )
{
- m_initialData?.Reset( GetComponent().terrainData );
+ var td = uTerr.terrainData;
+ var heightsRes = td.heightmapResolution;
- base.OnDestroy();
+ var modPos = aTerr.getSurfacePositionWorld( aIdx );
+ var mat = aTerr.getTerrainMaterial( modPos );
+
+ var index = m_materialMapping.GetValueOrDefault(mat,0);
+
+ m_materialAtlas[ uIdx.y * heightsRes + uIdx.x ] = (byte)index;
+ m_changed = true;
}
- private void PostStep()
+ void PostStep()
{
- var td = GetComponent().terrainData;
+ if ( m_changed ) {
+ m_materialTexture.SetPixelData( m_materialAtlas, 0 );
+ m_materialTexture.Apply( false );
- td.SetAlphamaps( 0, 0, alphamap );
+ // Updating terrain heights seems to invalidiate the heightmap texture so we need to reset it here
+ m_material.SetTexture( "_Heightmap", m_unityTerrain.terrainData.heightmapTexture );
+ }
}
- private void UpdateTextureAt( agxTerrain.Terrain aTerr, agx.Vec2i aIdx, Terrain uTerr, Vector2Int uIdx )
+ protected override void OnEnable()
{
- var td = uTerr.terrainData;
- var alphamapRes = td.alphamapResolution;
- var heightsRes = td.heightmapResolution - 1;
+ // We hook into the rendering process to render even when the application is paused.
+ // For the Built-in render pipeline this is done by adding a callback to the Camera.OnPreCull event which is called for each camera in the scene.
+ // For SRPs such as URP and HDRP the beginCameraRendering event serves a similar purpose.
+ RenderPipelineManager.beginCameraRendering -= SRPRender;
+ RenderPipelineManager.beginCameraRendering += SRPRender;
+ Camera.onPreCull -= Render;
+ Camera.onPreCull += Render;
+ }
- var modPos = aTerr.getSurfacePositionWorld( aIdx );
- var mat = aTerr.getTerrainMaterial( modPos );
+ protected override void OnDisable()
+ {
+ Camera.onPreCull -= Render;
+ RenderPipelineManager.beginCameraRendering -= SRPRender;
+ }
- var index = m_materialMapping.GetValueOrDefault(mat,0);
+ private void SRPRender( ScriptableRenderContext context, Camera cam )
+ {
+ if ( !RenderingUtils.CameraShouldRender( cam ) )
+ return;
- var modAlphaX = Mathf.RoundToInt((uIdx.x - 0.5f)/heightsRes * alphamapRes);
- var modAlphaY = Mathf.RoundToInt((uIdx.y - 0.5f)/heightsRes * alphamapRes);
- var modAlphaXend = Mathf.RoundToInt((uIdx.x + 0.5f)/heightsRes * alphamapRes);
- var modAlphaYend = Mathf.RoundToInt((uIdx.y + 0.5f)/heightsRes * alphamapRes);
-
- for ( int y = modAlphaY; y < modAlphaYend; y++ ) {
- if ( y < 0 || y >= alphamapRes )
- continue;
- for ( int x = modAlphaX; x < modAlphaXend; x++ ) {
- if ( x < 0 || x >= alphamapRes )
- continue;
- for ( int i = 0; i < MaterialRenderMap.Count + 1; i++ )
- alphamap[ y, x, i ] = i == index ? 1.0f : 0.0f;
- }
- }
+ Render( cam );
+ }
+
+ private void Render( Camera cam )
+ {
+ if ( !RenderingUtils.CameraShouldRender( cam ) )
+ return;
+
+ Graphics.DrawMesh( m_mesh, Matrix4x4.identity, m_material, 0, cam, 0, null, false );
}
}
}
\ No newline at end of file
diff --git a/Editor/AGXUnityEditor/Menus/TopMenu.cs b/Editor/AGXUnityEditor/Menus/TopMenu.cs
index 23ace36c..3e9236ee 100644
--- a/Editor/AGXUnityEditor/Menus/TopMenu.cs
+++ b/Editor/AGXUnityEditor/Menus/TopMenu.cs
@@ -378,6 +378,26 @@ public static GameObject CreateMovableTerrain()
return Selection.activeGameObject = go;
}
+ [MenuItem( "AGXUnity/Model/Terrain Material Patch", priority = 50 )]
+ public static GameObject CreateTerrainMaterialPatch()
+ {
+ var go = new GameObject();
+ go.name = Factory.CreateName();
+
+ AGXUnity.Utils.PrefabUtils.PlaceInCurrentStange( go );
+
+ go.AddComponent();
+
+ var box = Factory.Create();
+ box.transform.SetParent( go.transform, false );
+ box.GetComponent().HalfExtents = new Vector3( 2.5f, 1.0f, 2.5f );
+ AGXUnity.Rendering.ShapeVisual.Create( box.GetComponent() );
+
+ Undo.RegisterCreatedObjectUndo( go, "New Terrain Material Patch" );
+
+ return Selection.activeGameObject = go;
+ }
+
#endregion
#region Managers
diff --git a/Editor/AGXUnityEditor/Tools/TerrainMaterialPatchTool.cs b/Editor/AGXUnityEditor/Tools/TerrainMaterialPatchTool.cs
index e3ef454a..4de00a4b 100644
--- a/Editor/AGXUnityEditor/Tools/TerrainMaterialPatchTool.cs
+++ b/Editor/AGXUnityEditor/Tools/TerrainMaterialPatchTool.cs
@@ -1,4 +1,7 @@
+using AGXUnity;
using AGXUnity.Model;
+using AGXUnity.Rendering;
+using UnityEditor;
using UnityEngine;
namespace AGXUnityEditor.Tools
@@ -17,6 +20,13 @@ public TerrainMaterialPatchTool( Object[] targets )
public override void OnPostTargetMembersGUI()
{
InspectorGUI.ToolArrayGUI( this, TerrainMaterialPatch.Shapes, "Shapes" );
+ if( TerrainMaterialPatch.RenderTexture != null &&
+ TerrainMaterialPatch.GetComponentInParent() == null ) {
+ EditorGUILayout.HelpBox( "This patch has an associated Render Texture but the parent terrain does not contain a renderer", MessageType.Warning );
+ if ( GUILayout.Button( AGXUnity.Utils.GUI.MakeLabel( "Add a renderer to parent" ) ) ) {
+ TerrainMaterialPatch.transform.parent.gameObject.AddComponent();
+ }
+ }
}
}
diff --git a/Editor/AGXUnityEditor/Tools/TerrainPatchRendererTool.cs b/Editor/AGXUnityEditor/Tools/TerrainPatchRendererTool.cs
index 240e0eec..b92beb65 100644
--- a/Editor/AGXUnityEditor/Tools/TerrainPatchRendererTool.cs
+++ b/Editor/AGXUnityEditor/Tools/TerrainPatchRendererTool.cs
@@ -21,11 +21,11 @@ public TerrainPatchRendererTool( Object[] targets )
public override void OnPostTargetMembersGUI()
{
- if ( InspectorGUI.Foldout( EditorData.Instance.GetData( TerrainPatchRenderer, "TerrainPatchMaterialMap" ), GUI.MakeLabel( "Material Mapping" ) ) ) {
- List> toSet = new List>();
- List> toRemove = new List>();
+ if ( InspectorGUI.Foldout( EditorData.Instance.GetData( TerrainPatchRenderer, "TerrainPatchExplicitMaterialMap" ), GUI.MakeLabel( "Explicit Material Mapping" ) ) ) {
+ List> toSet = new List>();
+ List> toRemove = new List>();
- foreach ( var (k, v) in TerrainPatchRenderer.MaterialRenderMap ) {
+ foreach ( var (k, v) in TerrainPatchRenderer.ExplicitMaterialRenderMap ) {
using ( new HorizontalScope() ) {
using ( new VerticalScope( GUILayout.Width( 20 ) ) ) {
GUILayout.FlexibleSpace();
@@ -39,7 +39,7 @@ public override void OnPostTargetMembersGUI()
if ( news.Item1 == null )
toRemove.Add( System.Tuple.Create( k, v ) );
else if ( news.Item1 != k ) {
- if ( TerrainPatchRenderer.MaterialRenderMap.ContainsKey( news.Item1 ) )
+ if ( TerrainPatchRenderer.ExplicitMaterialRenderMap.ContainsKey( news.Item1 ) )
Debug.LogWarning( $"{news.Item1}" );
else {
toSet.Add( news );
@@ -62,12 +62,35 @@ public override void OnPostTargetMembersGUI()
}
if ( itemToAdd )
- toSet.Add( System.Tuple.Create( itemToAdd, null ) );
+ toSet.Add( System.Tuple.Create( itemToAdd, null ) );
foreach ( var t in toRemove )
- TerrainPatchRenderer.MaterialRenderMap.Remove( t.Item1 );
+ TerrainPatchRenderer.ExplicitMaterialRenderMap.Remove( t.Item1 );
foreach ( var t in toSet )
- TerrainPatchRenderer.MaterialRenderMap[ t.Item1 ] = t.Item2;
+ TerrainPatchRenderer.ExplicitMaterialRenderMap[ t.Item1 ] = t.Item2;
+
+ }
+
+ if ( InspectorGUI.Foldout( EditorData.Instance.GetData( TerrainPatchRenderer, "TerrainPatchFinalMaterialMap" ), GUI.MakeLabel( "Final Material Mapping" ) ) ) {
+ var map = TerrainPatchRenderer.MaterialRenderMap;
+ using ( new HorizontalScope() ) {
+ GUILayout.FlexibleSpace();
+ using (new VerticalScope() ) {
+ foreach( var mat in map.Keys )
+ GUILayout.Label( mat.name );
+ }
+ GUILayout.FlexibleSpace();
+ using ( new VerticalScope() ) {
+ for ( int i = 0; i < map.Count; i++)
+ GUILayout.Label( GUI.Symbols.ArrowRight.ToString() );
+ }
+ GUILayout.FlexibleSpace();
+ using ( new VerticalScope() ) {
+ foreach ( var tex in map.Values )
+ GUILayout.Label( tex != null ? tex.name : "None" );
+ }
+ GUILayout.FlexibleSpace();
+ }
}
}
}
diff --git a/Resources/Materials/TerrainPatchShapeMaterial.mat b/Resources/Materials/TerrainPatchShapeMaterial.mat
new file mode 100644
index 00000000..407c9ae2
--- /dev/null
+++ b/Resources/Materials/TerrainPatchShapeMaterial.mat
@@ -0,0 +1,82 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: TerrainPatchShapeMaterial
+ m_Shader: {fileID: 46, guid: 0000000000000000f000000000000000, type: 0}
+ m_ValidKeywords:
+ - _ALPHAPREMULTIPLY_ON
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses: []
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BumpMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailAlbedoMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailMask:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _DetailNormalMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _EmissionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MetallicGlossMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _OcclusionMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _ParallaxMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _BumpScale: 1
+ - _Cutoff: 0.5
+ - _DetailNormalMapScale: 1
+ - _DstBlend: 10
+ - _GlossMapScale: 1
+ - _Glossiness: 0
+ - _GlossyReflections: 1
+ - _Metallic: 0
+ - _Mode: 3
+ - _OcclusionStrength: 1
+ - _Parallax: 0.02
+ - _SmoothnessTextureChannel: 0
+ - _SpecularHighlights: 1
+ - _SrcBlend: 1
+ - _UVSec: 0
+ - _ZWrite: 0
+ m_Colors:
+ - _Color: {r: 0, g: 1, b: 0.07450986, a: 0.27450982}
+ - _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
+ m_BuildTextureStacks: []
diff --git a/Resources/Materials/TerrainPatchShapeMaterial.mat.meta b/Resources/Materials/TerrainPatchShapeMaterial.mat.meta
new file mode 100644
index 00000000..95689035
--- /dev/null
+++ b/Resources/Materials/TerrainPatchShapeMaterial.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: df9e06c180b9b3d439bb126223c85da9
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Resources/Shaders/DepthInverseProjectionUtils.cginc b/Resources/Shaders/DepthInverseProjectionUtils.cginc
new file mode 100644
index 00000000..871c20cb
--- /dev/null
+++ b/Resources/Shaders/DepthInverseProjectionUtils.cginc
@@ -0,0 +1,61 @@
+// Contains utility methods for generating fragments with reconstructed world space positions based on the
+// current camera depth buffer.
+// Adapted from https://github.com/keijiro/DepthInverseProjection/blob/master/Assets/InverseProjection/Resources/InverseProjection.shader
+
+// Sets up a vertex as part of a fullscreen triangle meant to be used to reconstruct the current
+// fragment positions based on the depth buffer.
+inline void DIPVertex(uint id, out float4 position, out float2 texcoord, out float3 ray){
+ // Render settings
+ float far = _ProjectionParams.z;
+ float2 orthoSize = unity_OrthoParams.xy;
+ float isOrtho = unity_OrthoParams.w; // 0: perspective, 1: orthographic
+
+ // Vertex ID -> clip space vertex position
+ float x = (id != 1) ? -1 : 3;
+ float y = (id == 2) ? -3 : 1;
+ float3 vpos = float3(x, y, 1.0);
+
+ // Perspective: view space vertex position of the far plane
+ float3 rayPers = mul(unity_CameraInvProjection, vpos.xyzz * far).xyz;
+
+ // Orthographic: view space vertex position
+ float3 rayOrtho = float3(orthoSize * vpos.xy, 0);
+
+ position = float4(vpos.x, -vpos.y, 1, 1);
+ texcoord = (vpos.xy + 1) / 2;
+ ray = lerp(rayPers, rayOrtho, isOrtho);
+}
+
+// Reconstructs the world fragment position based on the current camera depth buffer.
+inline float3 DIPFragment(float2 texcoord, float3 ray){
+ // Render settings
+ float near = _ProjectionParams.y;
+ float far = _ProjectionParams.z;
+ float isOrtho = unity_OrthoParams.w; // 0: perspective, 1: orthographic
+
+ // Z buffer sample
+ float z = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, texcoord);
+
+ // Far plane exclusion
+ #if defined(UNITY_REVERSED_Z)
+ float mask = z > 0;
+ #else
+ float mask = z < 1;
+ #endif
+
+ // Perspective: view space position = ray * depth
+ float3 vposPers = ray * Linear01Depth(z);
+
+ // Orthographic: linear depth (with reverse-Z support)
+ #if defined(UNITY_REVERSED_Z)
+ float depthOrtho = -lerp(far, near, z);
+ #else
+ float depthOrtho = -lerp(near, far, z);
+ #endif
+
+ // Orthographic: view space position
+ float3 vposOrtho = float3(ray.xy, depthOrtho);
+
+ // Result: view space position
+ return lerp(vposPers, vposOrtho, isOrtho) * mask;
+}
\ No newline at end of file
diff --git a/Resources/Shaders/DepthInverseProjectionUtils.cginc.meta b/Resources/Shaders/DepthInverseProjectionUtils.cginc.meta
new file mode 100644
index 00000000..2c9e2180
--- /dev/null
+++ b/Resources/Shaders/DepthInverseProjectionUtils.cginc.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8d66a0899e9a1e2468c8905c1952e91c
+ShaderIncludeImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Resources/Shaders/LightUtils.cginc b/Resources/Shaders/LightUtils.cginc
new file mode 100644
index 00000000..35c519c7
--- /dev/null
+++ b/Resources/Shaders/LightUtils.cginc
@@ -0,0 +1,62 @@
+#include "UnityCG.cginc"
+#include "AutoLight.cginc"
+
+#if defined(UNITY_PASS_FORWARDBASE) || defined(UNITY_PASS_FORWARDADD)
+
+half3 _LightColor0;
+
+// Some of the unity lighting macros requires the shadow coords to be in a struct
+// A dummy struct is created and passed to these macros.
+struct shadow_dummy_struct
+{
+ SHADOW_COORDS(0)
+};
+
+#endif
+
+// Calculates a lighting factor given the world position, clip position, and normal of a fragment
+// This works for point, spot, and directional lights.
+// returns all ones if the current pass is not a lighting pass
+float3 CustomLighting(float3 worldPos, float4 clipPos, float3 normal) {
+ #if defined(UNITY_PASS_FORWARDBASE) || defined(UNITY_PASS_FORWARDADD)
+ #if defined (SHADOWS_SCREEN)
+ // setup shadow struct for screen space shadows
+ shadow_dummy_struct shadow_dummy;
+ #if defined(UNITY_NO_SCREENSPACE_SHADOWS)
+ // mobile directional shadow
+ shadow_dummy._ShadowCoord = mul(unity_WorldToShadow[0], worldPos);
+ #else
+ // screen space directional shadow
+ shadow_dummy._ShadowCoord = ComputeScreenPos(clipPos);
+ #endif // UNITY_NO_SCREENSPACE_SHADOWS
+ #else
+ // no shadow, or no directional shadow
+ float shadow_dummy = 0;
+ #endif // SHADOWS_SCREEN
+
+ half3 worldLightDir = UnityWorldSpaceLightDir(worldPos);
+ half ndotl = saturate(dot(normal, worldLightDir));
+ UNITY_LIGHT_ATTENUATION(atten, shadow_dummy, worldPos);
+
+ // Per pixel lighting
+ half3 lighting = _LightColor0 * ndotl * atten;
+
+ #if defined(UNITY_SHOULD_SAMPLE_SH)
+ // Add ambient only in base pass
+ lighting += ShadeSH9(float4(normal, 1.0));
+
+ #if defined(VERTEXLIGHT_ON)
+ // "per vertex" non-important lights
+ half3 vertexLighting = Shade4PointLights(
+ unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
+ unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
+ unity_4LightAtten0, worldPos, normal);
+
+ lighting += vertexLighting;
+ #endif // VERTEXLIGHT_ON
+ #endif
+ return lighting;
+ #else
+ return half3(1.0f,1.0f,1.0f);
+ #endif
+}
\ No newline at end of file
diff --git a/Resources/Shaders/LightUtils.cginc.meta b/Resources/Shaders/LightUtils.cginc.meta
new file mode 100644
index 00000000..266d2769
--- /dev/null
+++ b/Resources/Shaders/LightUtils.cginc.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e9866d9cf37e9804f9f6a9b13c45d0c7
+ShaderIncludeImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Resources/Shaders/TerrainPatchDecal.shader b/Resources/Shaders/TerrainPatchDecal.shader
new file mode 100644
index 00000000..44f222f1
--- /dev/null
+++ b/Resources/Shaders/TerrainPatchDecal.shader
@@ -0,0 +1,290 @@
+Shader "AGXUnity/BuiltIn/TerrainPatchDecal"
+{
+ Properties
+ {
+ _Color ("Color", Color) = (1,1,1,1)
+ }
+ SubShader
+ {
+ Tags { "RenderType"="Overlay" "Queue" = "Geometry-99" }
+ LOD 100
+
+ CGINCLUDE
+
+ // Depth texture is used in DepthInverseProjectionUtils it has to be declared before including
+ UNITY_DECLARE_DEPTH_TEXTURE(_CameraDepthTexture);
+
+ #include "./LightUtils.cginc"
+ #include "./DepthInverseProjectionUtils.cginc"
+
+ struct v2f
+ {
+ float4 vertex : SV_POSITION;
+ float2 texcoord : TEXCOORD0;
+ float3 ray : TEXCOORD1;
+ };
+
+ float _TerrainResolution;
+ float3 _TerrainScale;
+ float3 _TerrainPosition;
+ fixed4 _Color;
+
+ sampler2D _Heightmap;
+ sampler2D _Materials;
+ sampler2D _Decal0;
+ sampler2D _Decal1;
+ sampler2D _Decal2;
+ sampler2D _Decal3;
+
+ v2f vert (uint vertexID : SV_VertexID)
+ {
+ v2f o;
+ // Populate vertex out with data to prepare for view space position recomputation in fragment shader
+ // This creates a fullscreen triangle based on the vertex index of the given vertex
+ DIPVertex(vertexID, o.vertex, o.texcoord, o.ray);
+ return o;
+ }
+
+ float4 hash4( float2 p ) {
+ return frac(sin(float4( 1.0+dot(p,float2(37.0,17.0)),
+ 2.0+dot(p,float2(11.0,47.0)),
+ 3.0+dot(p,float2(41.0,29.0)),
+ 4.0+dot(p,float2(23.0,31.0))))*103.0);
+ }
+
+ // Samples a texture with an offset based on a random voronoi region.
+ // Adapted from https://www.shadertoy.com/view/4tsGzf
+ float3 textureNoTile( sampler2D samp, float2 uv, float v )
+ {
+ float2 p = floor( uv );
+ float2 f = frac( uv );
+
+ // derivatives (for correct mipmapping)
+ float2 dx = ddx( uv );
+ float2 dy = ddy( uv );
+
+ float3 va = float3(0.0,0.0,0.0);
+ float w1 = 0.0;
+ float w2 = 0.0;
+ for( int j=-1; j<=1; j++ )
+ for( int i=-1; i<=1; i++ )
+ {
+ float2 g = float2( float(i),float(j) );
+ float4 o = hash4( p + g );
+ float2 r = g - f + o.xy;
+ float d = dot(r,r);
+ float w = exp(-20.0*d );
+ float3 c = tex2D( samp, uv + v*o.zw, dx, dy ).xyz;
+ va += w*c;
+ w1 += w;
+ w2 += w*w;
+ }
+
+ // normal averaging --> lowers contrasts
+ return va/w1;
+ }
+
+ // Calculate normal based on heightmap height differences
+ float3 filterNormal(float2 uv, float texelSize)
+ {
+ // Sample heightmap orthogonal to the uv
+ float4 h;
+ h[0] = tex2D(_Heightmap, uv + texelSize * float2(0,-1)).r * _TerrainScale.y;
+ h[1] = tex2D(_Heightmap, uv + texelSize * float2(-1,0)).r * _TerrainScale.y;
+ h[2] = tex2D(_Heightmap, uv + texelSize * float2(1,0)).r * _TerrainScale.y;
+ h[3] = tex2D(_Heightmap, uv + texelSize * float2(0,1)).r * _TerrainScale.y;
+
+ float3 n;
+ n.z = (h[0] - h[3]);
+ n.x = (h[1] - h[2]);
+ n.y = 2 * texelSize * _TerrainScale.x; // pixel space -> uv space -> world space
+
+ return normalize(n);
+ }
+
+ // Calculates the barycentric coordinates for point p relative to points a,b, and c
+ float3 Barycentric(float3 a, float3 b, float3 c, float3 p)
+ {
+ float3 v0 = b - a;
+ float3 v1 = c - a;
+ float3 v2 = p - a;
+ float den = v0.x * v1.y - v1.x * v0.y;
+ float3 res;
+ res.y = (v2.x * v1.y - v1.x * v2.y) / den;
+ res.z = (v0.x * v2.y - v2.x * v0.y) / den;
+ res.x = 1.0f - res.y - res.z;
+ return res;
+ }
+
+ // Since we compare the fragment depth against the terrain the height needs to be rather close to the actual
+ // height used by unity when rendering. It turns out that Unity uses barycentric interpolation
+ // based on the triangle from which the height is sampled. This function replicates this.
+ // See https://github.com/chanfort/Terrain-heightmap-manual-interpolation/tree/master
+ float textureBarycentric(sampler2D samp, float2 texCoords){
+ // Convert texCoords to texel space and fake point sampling of the heightmap texture to avoid
+ // hardware texture sampler interpolation
+ float texSize = _TerrainResolution -1;
+ float invTexSize = 1.0 / texSize;
+ float2 texInd = texCoords * texSize;
+
+ float2 fxy = frac(texInd);
+ float2 ltc = floor(texInd+0.5);
+ float2 utc = ceil(texInd-0.5);
+
+ float4 tc = float4(ltc,utc) * invTexSize;
+
+ // Sample heights at low (l) and high (h) texel coords xy
+ float4 ll = tex2Dlod(samp,float4(tc.xy,0,0));
+ float4 hh = tex2Dlod(samp,float4(tc.zw,0,0));
+ float4 lh = tex2Dlod(samp,float4(tc.xw,0,0));
+ float4 hl = tex2Dlod(samp,float4(tc.zy,0,0));
+
+ float4 last;
+ float3 bary;
+
+ // Different handling depending on if triangle is upper or lower
+ // Calculate the barycentric coordinates for the texCoord and assign the relevant height to last
+ if(fxy.x > fxy.y) {
+ last = hl;
+ bary = Barycentric(float3(tc.xy,0.0f),float3(tc.zw,0.0f),float3(tc.zy,0.0f),float3(texCoords,0.0f));
+ }
+ else {
+ last = lh;
+ bary = Barycentric(float3(tc.xy,0.0f),float3(tc.zw,0.0f),float3(tc.xw,0.0f),float3(texCoords,0.0f));
+ }
+
+ // Interpolate heights based on barycentric coordinates
+ return (ll * bary.x + hh * bary.y + last * bary.z).x;
+ }
+
+ fixed4 SampleFunc(sampler2D samp, float2 uv){
+ #ifdef REDUCE_TILING
+ return float4(textureNoTile(samp,uv,0.74f),1.0f);
+ #else
+ return float4(tex2D(samp,uv).rgb,1.0f);
+ #endif
+
+ }
+
+ fixed4 SampleDecal(int index,float2 uv){
+ if(index == 0) return SampleFunc(_Decal0,uv);
+ else if(index == 1) return SampleFunc(_Decal1,uv);
+ else if(index == 2) return SampleFunc(_Decal2,uv);
+ else return SampleFunc(_Decal3,uv);
+ }
+
+ // Find the bilinear interpolation of the decal materials at the four closest texel coords.
+ fixed4 FilterDecals(float2 indexUV, float2 decalUV){
+ // Convert indexUVs to texel space and find the 4 closest texels to sample
+ float texSize = _TerrainResolution -1;
+ float invTexSize = 1.0 / texSize;
+ float2 texInd = indexUV * texSize;
+
+ float2 fxy = frac(texInd);
+ float2 ltc = floor(texInd);
+ float2 utc = ceil(texInd);
+
+ float4 tc = float4(ltc,utc) * invTexSize;
+
+ // Sample indices at low (l) and high (h) texel coords xy
+ int ill = int(round(tex2D(_Materials,tc.xy).r * 255.0f)) - 1;
+ int ilh = int(round(tex2D(_Materials,tc.xw).r * 255.0f)) - 1;
+ int ihl = int(round(tex2D(_Materials,tc.zy).r * 255.0f)) - 1;
+ int ihh = int(round(tex2D(_Materials,tc.zw).r * 255.0f)) - 1;
+
+ // Discard fragments where no material is present
+ if(all(int4(ill,ilh,ihl,ihh) < 0))
+ discard;
+
+ // Sample corresponding decal texture for each index with transparent black as a default.
+ float4 ll,lh,hl,hh;
+
+ if(ill > -1) ll = SampleDecal(ill,decalUV);
+ else ll = float4(0.0f,0.0f,0.0f,0.0f);
+ if(ilh > -1) lh = SampleDecal(ilh,decalUV);
+ else lh = float4(0.0f,0.0f,0.0f,0.0f);
+ if(ihl > -1) hl = SampleDecal(ihl,decalUV);
+ else hl = float4(0.0f,0.0f,0.0f,0.0f);
+ if(ihh > -1) hh = SampleDecal(ihh,decalUV);
+ else hh = float4(0.0f,0.0f,0.0f,0.0f);
+
+ // Bilinearly interpolate between the materials
+ float4 xl = lerp(ll,hl,fxy.x);
+ float4 xh = lerp(lh,hh,fxy.x);
+
+ return lerp(xl,xh,fxy.y);
+ }
+
+ fixed4 frag (v2f i) : SV_Target
+ {
+ // Find position of the fragment in view space
+ float3 viewPos = DIPFragment(i.texcoord,i.ray);
+ if(viewPos.z == 0)
+ discard;
+
+ // Find position of fragment in world space
+ float3 pixelPos = mul(unity_MatrixInvV, float4(viewPos,1.0f)).xyz;
+ // Find position of fragment relative to terrain, scaled to [0,1]
+ float3 terrainPos = (pixelPos.xyz - _TerrainPosition) / _TerrainScale.xyz ;
+
+ // Discard any fragments outside of the terrain bounds
+ if(any(terrainPos != saturate(terrainPos)))
+ discard;
+
+ // Find the height of the terrain at terrain XZ pos and compare it against the Y pos
+ float rawHeight = textureBarycentric (_Heightmap, terrainPos.xz);
+ float height = rawHeight * _TerrainScale.y * 2.0f + _TerrainPosition.y;
+ float absDiff = abs(height - pixelPos.y);
+
+ // Discard fragments which are not on the terrain
+ if(absDiff > 0.01f)
+ discard;
+
+ // Get the color of the decals at the fragment position based on the material texture
+ fixed4 c = _Color;
+ c *= FilterDecals(terrainPos.xz,pixelPos.xz);
+
+ // Compute normal from the terrain heightmap
+ half3 normal = filterNormal(terrainPos.xz,1.0 / (_TerrainResolution - 1));
+
+ // CustomLighting function handles various light sources
+ float3 lighting = CustomLighting(pixelPos,float4(i.texcoord,0.0f,0.0f),normal);
+ c.xyz *= lighting;
+ return c;
+ }
+
+ ENDCG
+
+ Pass {
+ Tags {"LightMode" = "ForwardBase"}
+ ZWrite Off ZTest Off
+ Blend One OneMinusSrcAlpha
+ CGPROGRAM
+
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma multi_compile_fwdbase nolightmap nodirlightmap nodynlightmap novertexlight
+ #pragma multi_compile _ VERTEXLIGHT_ON
+ #pragma skip_variants LIGHTMAP_ON DYNAMICLIGHTMAP_ON DIRLIGHTMAP_COMBINED SHADOWS_SHADOWMASK
+ #pragma shader_feature_local _ _MAPPING_CUBEMAP REDUCE_TILING
+
+ ENDCG
+ }
+
+ Pass{
+ Tags {"LightMode" = "ForwardAdd"}
+
+ Blend One One, Zero One
+ ZWrite Off ZTest Off
+ CGPROGRAM
+
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma multi_compile_fwdadd_fullshadows nolightmap nodirlightmap nodynlightmap novertexlight
+ #pragma skip_variants LIGHTMAP_ON DYNAMICLIGHTMAP_ON DIRLIGHTMAP_COMBINED SHADOWS_SHADOWMASK SPOT_COOKIE
+ #pragma shader_feature_local _ _MAPPING_CUBEMAP REDUCE_TILING
+
+ ENDCG
+ }
+ }
+}
diff --git a/Resources/Shaders/TerrainPatchDecal.shader.meta b/Resources/Shaders/TerrainPatchDecal.shader.meta
new file mode 100644
index 00000000..9e88f01a
--- /dev/null
+++ b/Resources/Shaders/TerrainPatchDecal.shader.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: bdb0d0c4aa8545c4b844f314cc48255b
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ preprocessorOverride: 0
+ userData:
+ assetBundleName:
+ assetBundleVariant: