Skip to content
Open
1 change: 1 addition & 0 deletions AGXUnity/Model/MovableTerrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
27 changes: 27 additions & 0 deletions AGXUnity/Model/TerrainMaterialPatch.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using AGXUnity.Collide;
using AGXUnity.Rendering;
using System.Linq;
using UnityEngine;

Expand Down Expand Up @@ -72,6 +73,14 @@ public ShapeMaterial MaterialHandle
}
}

[SerializeField]
private Texture2D m_renderTexture = null;

public Texture2D RenderTexture {
get => m_renderTexture;
set { m_renderTexture = value; }
}

/// <summary>
/// Whether to disable collision shapes used to define this patch during initialization.
/// </summary>
Expand All @@ -84,6 +93,12 @@ public ShapeMaterial MaterialHandle
[field: SerializeField]
public bool DisableVisuals { get; set; } = true;

/// <summary>
/// Whether to set child shape visuals to the default terrain patch shape material
/// </summary>
[field: SerializeField]
public bool OverrideVisuals { get; set; } = true;

// The shapes used to define this patch.
public Collide.Shape[] Shapes { get => GetComponentsInChildren<Collide.Shape>(); }

Expand All @@ -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<Material>( @"Materials/TerrainPatchShapeMaterial" );
foreach( var visual in gameObject.GetComponentsInChildren<ShapeVisual>() )
visual.SetMaterial( m_replaceMat );
}
}
}
}
228 changes: 123 additions & 105 deletions AGXUnity/Rendering/TerrainPatchRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,170 +2,188 @@
using AGXUnity.Utils;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

namespace AGXUnity.Rendering
{
/// <summary>
/// Wrapper class for storing/resetting initial state of TerrainData.
/// This is by no means a complete store/restore, only the parts used by <see cref="TerrainPatchRenderer"/>.
/// </summary>
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<DeformableTerrainMaterial,Texture2D> m_explicitMaterialRenderMap = new SerializableDictionary<DeformableTerrainMaterial, Texture2D>();

public InitialTerrainData( TerrainData td )
{
m_alphamaps = td.GetAlphamaps( 0, 0, td.alphamapWidth, td.alphamapHeight );
m_layers = td.terrainLayers;
}
[HideInInspector]
public SerializableDictionary<DeformableTerrainMaterial, Texture2D> ExplicitMaterialRenderMap => m_explicitMaterialRenderMap;

public void Reset( TerrainData td )
public Dictionary<DeformableTerrainMaterial, Texture2D> ImplicitMaterialRenderMap
{
if ( td != null ) {
td.terrainLayers = m_layers;
td.SetAlphamaps( 0, 0, m_alphamaps );
get
{
Dictionary<DeformableTerrainMaterial, Texture2D> res = new Dictionary<DeformableTerrainMaterial, Texture2D>();
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<agxTerrain.TerrainMaterial, int> m_materialMapping;
private InitialTerrainData m_initialData;

[SerializeField]
private TerrainLayer m_defaultLayer;

/// <summary>
/// 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.
/// </summary>
[IgnoreSynchronization]
public TerrainLayer DefaultLayer
public Dictionary<DeformableTerrainMaterial, Texture2D> 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<DeformableTerrainMaterial,TerrainLayer> m_materialRenderMap = new SerializableDictionary<DeformableTerrainMaterial, TerrainLayer>();
private bool m_reduceTextureTiling = false;

/// <summary>
/// Defines a map from DeformableTerrainMaterials to the TerrainLayers used to render patches of the specified terrain material.
/// </summary>
[HideInInspector]
[IgnoreSynchronization]
public SerializableDictionary<DeformableTerrainMaterial, TerrainLayer> 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<TerrainMaterialPatch>();

private Dictionary<agxTerrain.TerrainMaterial, int> 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<DeformableTerrainBase>();
if ( terrain is MovableTerrain ) {
Debug.LogError( "Terrain Patch Renderer does not yet support MovableTerrain!", this );
m_terrain = gameObject.GetInitializedComponent<DeformableTerrainBase>();
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<TerrainMaterialPatch>() )
foreach ( var patch in RenderedPatches )
patch.GetInitialized();

var uTerr = GetComponent<Terrain>();
var td = uTerr.terrainData;
m_unityTerrain = GetComponent<UnityEngine.Terrain>();
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<agxTerrain.TerrainMaterial, int>();
var layers = new List<TerrainLayer> { 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<DeformableTerrainMaterial>().Native;
if ( terrMat != null ) {
m_materialMapping.Add( mat.GetInitialized<DeformableTerrainMaterial>().Native, idx++ );
layers.Add( tl );
m_materialMapping.Add( mat.GetInitialized<DeformableTerrainMaterial>().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<Terrain>().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<Terrain>().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 );
}
}
}
20 changes: 20 additions & 0 deletions Editor/AGXUnityEditor/Menus/TopMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.Model.TerrainMaterialPatch>();

AGXUnity.Utils.PrefabUtils.PlaceInCurrentStange( go );

go.AddComponent<AGXUnity.Model.TerrainMaterialPatch>();

var box = Factory.Create<AGXUnity.Collide.Box>();
box.transform.SetParent( go.transform, false );
box.GetComponent<AGXUnity.Collide.Box>().HalfExtents = new Vector3( 2.5f, 1.0f, 2.5f );
AGXUnity.Rendering.ShapeVisual.Create( box.GetComponent<AGXUnity.Collide.Box>() );

Undo.RegisterCreatedObjectUndo( go, "New Terrain Material Patch" );

return Selection.activeGameObject = go;
}

#endregion

#region Managers
Expand Down
Loading