From 79d4303f8385343438af396b06eb030b7cd94c52 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 12:55:00 +0100 Subject: [PATCH 01/10] Added Doc link to movable Terrain --- AGXUnity/Model/MovableTerrain.cs | 1 + 1 file changed, 1 insertion(+) 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 { From 08976296be4cc37b097754792924424c2a1f7a12 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 13:25:59 +0100 Subject: [PATCH 02/10] Add TerrainMaterialPatch to top menu --- Editor/AGXUnityEditor/Menus/TopMenu.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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 From 05c6da24a0fe40f7285c5eb099eb517c4cd9b004 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 13:27:35 +0100 Subject: [PATCH 03/10] Added some utility shader methods --- .../Shaders/DepthInverseProjectionUtils.cginc | 61 ++++++++++++++++++ .../DepthInverseProjectionUtils.cginc.meta | 7 +++ Resources/Shaders/LightUtils.cginc | 62 +++++++++++++++++++ Resources/Shaders/LightUtils.cginc.meta | 7 +++ 4 files changed, 137 insertions(+) create mode 100644 Resources/Shaders/DepthInverseProjectionUtils.cginc create mode 100644 Resources/Shaders/DepthInverseProjectionUtils.cginc.meta create mode 100644 Resources/Shaders/LightUtils.cginc create mode 100644 Resources/Shaders/LightUtils.cginc.meta 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..efc22550 --- /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, fragPosOnSphere, 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: From db33db4785796ca827ca55ac8e214b24c9b288ec Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 13:27:59 +0100 Subject: [PATCH 04/10] Added decal shader for rendering TerrainMaterialPatches --- Resources/Shaders/TerrainPatchDecal.shader | 242 ++++++++++++++++++ .../Shaders/TerrainPatchDecal.shader.meta | 10 + 2 files changed, 252 insertions(+) create mode 100644 Resources/Shaders/TerrainPatchDecal.shader create mode 100644 Resources/Shaders/TerrainPatchDecal.shader.meta diff --git a/Resources/Shaders/TerrainPatchDecal.shader b/Resources/Shaders/TerrainPatchDecal.shader new file mode 100644 index 00000000..d08e4509 --- /dev/null +++ b/Resources/Shaders/TerrainPatchDecal.shader @@ -0,0 +1,242 @@ +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; + } + + // 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 SampleDecal(int index,float2 uv){ + if(index == 0) return float4(tex2D(_Decal0,uv).rgb,1.0f); + else if(index == 1) return float4(tex2D(_Decal1,uv).rgb,1.0f); + else if(index == 2) return float4(tex2D(_Decal2,uv).rgb,1.0f); + else return float4(tex2D(_Decal3,uv).rgb,1.0f); + } + + // 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 + + 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 + + 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: From 6aa1cf26ceec6c46c081708e440937c39e577de4 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 13:29:04 +0100 Subject: [PATCH 05/10] Added a material for rendering material patch volumes --- .../Materials/TerrainPatchShapeMaterial.mat | 82 +++++++++++++++++++ .../TerrainPatchShapeMaterial.mat.meta | 8 ++ 2 files changed, 90 insertions(+) create mode 100644 Resources/Materials/TerrainPatchShapeMaterial.mat create mode 100644 Resources/Materials/TerrainPatchShapeMaterial.mat.meta 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: From 1aff9389a76508f78fd62c7054322519c394a3e9 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 15:20:36 +0100 Subject: [PATCH 06/10] Fixed variable name error in LightUtils --- Resources/Shaders/LightUtils.cginc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Shaders/LightUtils.cginc b/Resources/Shaders/LightUtils.cginc index efc22550..35c519c7 100644 --- a/Resources/Shaders/LightUtils.cginc +++ b/Resources/Shaders/LightUtils.cginc @@ -50,7 +50,7 @@ float3 CustomLighting(float3 worldPos, float4 clipPos, float3 normal) { 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, fragPosOnSphere, normal); + unity_4LightAtten0, worldPos, normal); lighting += vertexLighting; #endif // VERTEXLIGHT_ON From 0cb580a21f4bd7f43fc0f77792edaaa4f6c83208 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 15:21:35 +0100 Subject: [PATCH 07/10] Added shader keyword to use voronoi tiling to reduce tiling patterns --- Resources/Shaders/TerrainPatchDecal.shader | 60 +++++++++++++++++++--- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/Resources/Shaders/TerrainPatchDecal.shader b/Resources/Shaders/TerrainPatchDecal.shader index d08e4509..44f222f1 100644 --- a/Resources/Shaders/TerrainPatchDecal.shader +++ b/Resources/Shaders/TerrainPatchDecal.shader @@ -45,6 +45,45 @@ Shader "AGXUnity/BuiltIn/TerrainPatchDecal" 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) { @@ -118,11 +157,20 @@ Shader "AGXUnity/BuiltIn/TerrainPatchDecal" 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 float4(tex2D(_Decal0,uv).rgb,1.0f); - else if(index == 1) return float4(tex2D(_Decal1,uv).rgb,1.0f); - else if(index == 2) return float4(tex2D(_Decal2,uv).rgb,1.0f); - else return float4(tex2D(_Decal3,uv).rgb,1.0f); + 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. @@ -218,7 +266,7 @@ Shader "AGXUnity/BuiltIn/TerrainPatchDecal" #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 + #pragma shader_feature_local _ _MAPPING_CUBEMAP REDUCE_TILING ENDCG } @@ -234,7 +282,7 @@ Shader "AGXUnity/BuiltIn/TerrainPatchDecal" #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 + #pragma shader_feature_local _ _MAPPING_CUBEMAP REDUCE_TILING ENDCG } From a88178260592c5d0297dc7ba9873d5ad78dbf5f8 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 15:23:02 +0100 Subject: [PATCH 08/10] Added option to override shape visual materials in material patches --- AGXUnity/Model/TerrainMaterialPatch.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/AGXUnity/Model/TerrainMaterialPatch.cs b/AGXUnity/Model/TerrainMaterialPatch.cs index dd676e98..4cd62d40 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; @@ -84,6 +85,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 +115,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 From 1b79de7b1849ecfb04558d9310c1134e27fcfa19 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 15:23:59 +0100 Subject: [PATCH 09/10] Added Rendering texture to Material Patch component --- AGXUnity/Model/TerrainMaterialPatch.cs | 8 ++++++++ .../AGXUnityEditor/Tools/TerrainMaterialPatchTool.cs | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/AGXUnity/Model/TerrainMaterialPatch.cs b/AGXUnity/Model/TerrainMaterialPatch.cs index 4cd62d40..73040465 100644 --- a/AGXUnity/Model/TerrainMaterialPatch.cs +++ b/AGXUnity/Model/TerrainMaterialPatch.cs @@ -73,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. /// 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(); + } + } } } From 86547be9c3f4c3c65e84f00cf422b1e405bb6751 Mon Sep 17 00:00:00 2001 From: Filip Henningsson Date: Thu, 25 Jan 2024 15:24:32 +0100 Subject: [PATCH 10/10] Swap to using decal rendering for material patches and added implicit material mappings from the patches themselves --- AGXUnity/Rendering/TerrainPatchRenderer.cs | 228 ++++++++++-------- .../Tools/TerrainPatchRendererTool.cs | 39 ++- 2 files changed, 154 insertions(+), 113 deletions(-) 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/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(); + } } } }