////////////////////////////////////////////////////// // MicroSplat // Copyright (c) Jason Booth ////////////////////////////////////////////////////// using System.Collections; using System.Collections.Generic; using UnityEngine; namespace JBooth.MicroSplat { public partial class MicroSplatDecalReceiver : MonoBehaviour { List staticDecals = null; [HideInInspector] public Texture2D cacheMask = null; // holds pre-culled indexes [HideInInspector] public Color[] cacheMaskBuffer = null; [HideInInspector] public Texture2D staticCacheData = null; // holds decal data, because uploading texture is fast, property block arrays not so much public int staticCount { get { return staticDecals == null ? 0 : staticDecals.Count; } } int maxStaticDecals = 0; public enum StaticCacheSize { k64 = 64, k128 = 128, k256 = 256, k512 = 512, k1024 = 1024 } public StaticCacheSize staticCacheSize = StaticCacheSize.k256; void ClearStaticCacheData() { if (staticCacheData != null) { DestroyImmediate(staticCacheData); } staticCacheData = new Texture2D(maxStaticDecals, 8, TextureFormat.RGBAFloat, false, true); staticCacheData.Apply(false, false); staticCacheData.hideFlags = HideFlags.HideAndDontSave; } void ClearCacheMask() { if (cacheMask != null) { DestroyImmediate(cacheMask); } // setup cache mask int size = (int)staticCacheSize; if (cacheMaskBuffer == null || cacheMaskBuffer.Length != size * size) { ClearCacheMaskBuffer(); } cacheMask = new Texture2D(size, size, TextureFormat.RGBAHalf, false, true); cacheMask.hideFlags = HideFlags.HideAndDontSave; cacheMask.filterMode = FilterMode.Point; cacheMask.wrapMode = TextureWrapMode.Clamp; cacheMask.SetPixels(cacheMaskBuffer); cacheMask.Apply(false, false); } void ClearCacheMaskBuffer() { // setup cache mask int size = (int)staticCacheSize; cacheMaskBuffer = new Color[size * size]; Color black = new Color(0, 0, 0, 0); for (int x = 0; x < size; ++x) { for (int y = 0; y < size; ++y) { cacheMaskBuffer[y * size + x] = black; } } } void InitStatic() { maxStaticDecals = 1; if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX64")) { maxStaticDecals = 64; } else if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX128")) { maxStaticDecals = 128; } else if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX256")) { maxStaticDecals = 256; } else if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX512")) { maxStaticDecals = 512; } else if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX1024")) { maxStaticDecals = 1024; } else if (msObj.keywordSO.IsKeywordEnabled("_DECAL_STATICMAX2048")) { maxStaticDecals = 2048; } staticDecals = new List(maxStaticDecals); if (Application.IsPlaying(this) && cacheMaskBuffer != null && cacheMaskBuffer.Length == maxStaticDecals * maxStaticDecals) { loadStaticFromCache = true; } else { ClearCacheMask(); ClearStaticCacheData(); needsStaticUpdate = true; } } void RegisterStaticDecal(MicroSplatDecal d) { staticDecals.Add(d); needsStaticUpdate = true; } void UnregisterStaticDecal(MicroSplatDecal d) { if (terrain != null) { // this might need to use the old data, in case we're out of sync.. if (staticDecals != null && staticDecals.Contains(d)) { staticDecals.Remove(d); needsStaticUpdate = true; } } #if __MICROSPLAT_MESHTERRAIN__ else if (meshTerrain != null) { if (staticDecals != null && staticDecals.Contains(d)) { staticDecals.Remove(d); needsStaticUpdate = true; } } #endif } void UpdateStaticCache() { if (staticDecals == null) return; int count = staticDecals.Count; if (count > maxStaticDecals) count = maxStaticDecals; for (int i = 0; i < count; ++i) { SetData(staticDecals[i], i, staticCacheData); } staticCacheData.Apply(false, false); } void UpdateStaticPropertyBlocks() { decalBlock.SetTexture("_DecalControl", cacheMask); decalBlock.SetTexture("_DecalStaticData", staticCacheData); } Vector2 WorldToTerrainPixel(Vector3 terrainPos, Vector3 terrainSize, Vector3 point, Texture2D splatControl) { point = point - terrainPos; float x = (point.x / terrainSize.x) * splatControl.width; float z = (point.z / terrainSize.z) * splatControl.height; return new Vector2(x, z); } Vector3 TerrainPixelToWorld(Vector3 terrainPos, Vector3 terrainSize, int x, int y, Texture2D splatControl) { Vector3 wp = new Vector3(x, 0, y); wp.x *= terrainSize.x / (float)splatControl.width; wp.z *= terrainSize.z / (float)splatControl.height; return wp += terrainPos; } Vector3 TerrainPixelToWorldWithHeight(Terrain t, Vector3 terrainPos, Vector3 terrainSize, int x, int y, Texture2D splatControl) { Vector3 wp = new Vector3(x, 0, y); wp.x *= terrainSize.x / (float)splatControl.width; wp.y = t.terrainData.GetInterpolatedHeight(x, y); wp.z *= terrainSize.z / (float)splatControl.height; return wp += terrainPos; } struct PixelBounds { public int xmin; public int xmax; public int ymin; public int ymax; } bool GetDecalPixelBounds(Vector3 terrainPos, Vector3 terrainSize, Matrix4x4 decalMtx, ref PixelBounds bounds) { // this seems to be texel size dependent.. Expect to fix bugs here.. float sz = 0.5f; Bounds aabb = new Bounds(decalMtx.MultiplyPoint(new Vector3(-sz, -sz, -sz)), Vector3.one); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(sz, sz, sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(-sz, sz, sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(sz, -sz, sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(sz, sz, -sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(-sz, -sz, sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(sz, -sz, -sz))); aabb.Encapsulate(decalMtx.MultiplyPoint(new Vector3(-sz, sz, -sz))); var min = (aabb.min); var max = (aabb.max); Vector2 minUV = WorldToTerrainPixel(terrainPos, terrainSize, min, cacheMask); Vector2 maxUV = WorldToTerrainPixel(terrainPos, terrainSize, max, cacheMask); // expand by an extra pixel. bounds.xmin = Mathf.FloorToInt(minUV.x - 1); bounds.ymin = Mathf.FloorToInt(minUV.y - 1); bounds.xmax = Mathf.FloorToInt(maxUV.x + 1); bounds.ymax = Mathf.FloorToInt(maxUV.y + 1); // are we out of the terrain bounds? If so, skip if (bounds.xmin < 0 && bounds.xmax < 0) return false; if (bounds.ymin < 0 && bounds.ymax < 0) return false; if (bounds.xmin >= cacheMask.width && bounds.xmax >= cacheMask.width) return false; if (bounds.ymin >= cacheMask.height && bounds.ymax >= cacheMask.height) return false; // clamp to edges bounds.xmin = Mathf.Clamp(bounds.xmin, 0, cacheMask.width); bounds.xmax = Mathf.Clamp(bounds.xmax, 0, cacheMask.width); bounds.ymin = Mathf.Clamp(bounds.ymin, 0, cacheMask.height); bounds.ymax = Mathf.Clamp(bounds.ymax, 0, cacheMask.height); // make sure we are at least one pixel if (bounds.xmin == bounds.xmax) { if (bounds.xmax < cacheMask.width) bounds.xmax += 1; else bounds.xmin--; } if (bounds.ymin == bounds.ymax) { if (bounds.ymax < cacheMask.height) bounds.ymax += 1; else bounds.ymin--; } return true; } bool PointInOABB(Vector3 pt, Matrix4x4 decalMtx) { var point = decalMtx.MultiplyPoint(pt); if (point.x < 1 && point.x > -1 && point.y < 1 && point.y > -1 && point.z < 1 && point.z > -1) return true; else return false; } void ClearDecalInCache(Vector3 terrainPos, Vector3 terrainSize, Matrix4x4 dmtx, int index, PixelBounds pb) { int sz = cacheMask.width; for (int x = pb.xmin; x < pb.xmax; ++x) { for (int y = pb.ymin; y < pb.ymax; ++y) { /* TODO: Make this clip for extra speed var wp = TerrainPixelToWorld (terrainPos, terrainSize, x, y, cacheMask); var wp1 = TerrainPixelToWorld (terrainPos, terrainSize, x - 1, y - 1, cacheMask); var wp2 = TerrainPixelToWorld (terrainPos, terrainSize, x + 1, y + 1, cacheMask); if (PointInOABB (wp, dmtx) || PointInOABB (wp1, dmtx) || PointInOABB (wp2, dmtx)) */ { int cidx = y * sz + x; Color values = cacheMaskBuffer[cidx]; if (Mathf.RoundToInt(values.r - 1) == index) { values.r = values.g; values.g = values.b; values.b = values.a; values.a = 0; cacheMaskBuffer[cidx] = values; } else if (Mathf.RoundToInt(values.g - 1) == index) { values.g = values.b; values.b = values.a; values.a = 0; cacheMaskBuffer[cidx] = values; } else if (Mathf.RoundToInt(values.b - 1) == index) { values.b = values.a; values.a = 0; cacheMaskBuffer[cidx] = values; } else if (Mathf.RoundToInt(values.a - 1) == index) { values.a = 0; cacheMaskBuffer[cidx] = values; } } } } } void ClearDecalInCache(Vector3 terrainPos, Vector3 terrainSize, MicroSplatDecal d, Matrix4x4 oldMtx, int index) { PixelBounds pb = new PixelBounds(); if (GetDecalPixelBounds(terrainPos, terrainSize, oldMtx, ref pb)) { ClearDecalInCache(terrainPos, terrainSize, oldMtx, index, pb); } } void RenderDecalIntoCache(int index, Vector3 terrainPos, Vector3 terrainSize, MicroSplatDecal d, PixelBounds pb) { // draw Transform tfm = d.transform; var dmtx = tfm.worldToLocalMatrix; int sz = cacheMask.width; for (int x = pb.xmin; x < pb.xmax; ++x) { for (int y = pb.ymin; y < pb.ymax; ++y) { // man, I wish I could get this to work. The idea is to clip the point if it's inside the scaled box. This shrinks the area to // be more exact, tightening up performance and preventing false decal overlaps. But for some reason, this only works on // a top down decal projection - once rotated, the box is clipped as if it's not rotated at all. //var wp = TerrainPixelToWorldWithHeight (t, terrainPos, terrainSize, x, y, cacheMask); //var wp1 = TerrainPixelToWorldWithHeight (t, terrainPos, terrainSize, x-1, y-1, cacheMask); //var wp2 = TerrainPixelToWorldWithHeight (t, terrainPos, terrainSize, x+1, y+1, cacheMask); //var wp3 = TerrainPixelToWorldWithHeight (t, terrainPos, terrainSize, x + 1, y - 1, cacheMask); //var wp4 = TerrainPixelToWorldWithHeight (t, terrainPos, terrainSize, x - 1, y + 1, cacheMask); //if (PointInOABB (wp, dmtx) || PointInOABB (wp1, dmtx) || PointInOABB (wp2, dmtx) || PointInOABB (wp3, dmtx) || PointInOABB (wp4, dmtx)) { int cidx = y * sz + x; Color values = cacheMaskBuffer[cidx]; // skip if we're already in the cache if (Mathf.RoundToInt(values.r - 1) != index && Mathf.RoundToInt(values.g - 1) != index && Mathf.RoundToInt(values.b - 1) != index && Mathf.RoundToInt(values.a - 1) != index) { if (values.r < 0.5f) { values.r = index + 1; values.g = 0; values.b = 0; values.a = 0; } else if (values.g < 0.5f) { values.g = values.r; values.r = index + 1; values.b = 0; values.a = 0; } else if (values.b < 0.5f) { values.b = values.g; values.g = values.r; values.r = index + 1; values.a = 0; } else { values.a = values.b; values.b = values.g; values.g = values.r; values.r = index + 1; } cacheMaskBuffer[cidx] = values; } } } } } void RenderDecalIntoCache(Vector3 terrainPos, Vector3 terrainSize, MicroSplatDecal d, int index) { if (d.isActiveAndEnabled) { PixelBounds pb = new PixelBounds(); if (GetDecalPixelBounds(terrainPos, terrainSize, d.transform.localToWorldMatrix, ref pb)) { RenderDecalIntoCache(index, terrainPos, terrainSize, d, pb); } } } public void UpdateDecalInCache(Vector3 terrainPos, Vector3 terrainSize, MicroSplatDecal d, Matrix4x4 oldMtx) { UnityEngine.Profiling.Profiler.BeginSample("UpdateDecalInCache"); int listIndex = staticDecals.IndexOf(d); ClearDecalInCache(terrainPos, terrainSize, d, oldMtx, listIndex); RenderDecalIntoCache(terrainPos, terrainSize, d, listIndex); cacheMask.SetPixels(cacheMaskBuffer); cacheMask.Apply(false, false); UpdateStaticPropertyBlocks(); UpdateStaticCache(); //UpdatePropertyBlocks (); UnityEngine.Profiling.Profiler.EndSample(); } public void RerenderCacheMap() { UnityEngine.Profiling.Profiler.BeginSample("RerenderCacheMap"); ClearCacheMaskBuffer(); ClearCacheMask(); SortStaticDecals(); UpdateStaticCache(); UpdatePropertyBlocks(); if (terrain != null) { for (int i = 0; i < staticDecals.Count; ++i) { RenderDecalIntoCache(terrain.transform.position, terrain.terrainData.size, staticDecals[i], i); } } else { #if __MICROSPLAT_MESHTERRAIN__ if (meshTerrain != null) { for (int i = 0; i < staticDecals.Count; ++i) { RenderDecalIntoCache(meshTerrain.transform.position, meshTerrain.GetBounds().size, staticDecals[i], i); } } #endif } cacheMask.SetPixels(cacheMaskBuffer); cacheMask.Apply(false, false); staticCacheData.Apply(false, false); UnityEngine.Profiling.Profiler.EndSample(); } // sort by hash, then order, that way sorting produces the same results every time void SortStaticDecals() { if (staticDecals == null) return; staticDecals.Sort((x, y) => x.GetHashCode().CompareTo(y.GetHashCode())); staticDecals.Sort((x, y) => x.sortOrder.CompareTo(y.sortOrder)); } public void LoadFromCache() { ClearCacheMask(); SortStaticDecals(); UpdateStaticCache(); UpdatePropertyBlocks(); cacheMask.SetPixels(cacheMaskBuffer); cacheMask.Apply(false, false); staticCacheData.Apply(false, false); } } }