Files
2025-06-09 23:23:13 +08:00

435 lines
17 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Burst;
using Unity.Collections;
using UnityEngine.Rendering;
using Unity.Collections.LowLevel.Unsafe;
using System.Runtime.InteropServices;
namespace JBooth.MicroVerseCore
{
public static class NativeArrayExtensions
{
public static unsafe void CopyToFast<T>(
this NativeArray<T> nativeArray,
T[,] array)
where T : struct
{
int byteLength = nativeArray.Length * Marshal.SizeOf(default(T));
void* managedBuffer = UnsafeUtility.AddressOf(ref array[0, 0]);
void* nativeBuffer = nativeArray.GetUnsafePtr();
UnsafeUtility.MemCpy(managedBuffer, nativeBuffer, byteLength);
}
public static unsafe void CopyToFastByteToInt(
this NativeArray<byte> nativeArray,
int[,] array)
{
int byteLength = nativeArray.Length * Marshal.SizeOf(default(byte));
void* managedBuffer = UnsafeUtility.AddressOf(ref array[0, 0]);
void* nativeBuffer = nativeArray.GetUnsafePtr();
UnsafeUtility.MemCpyStride(managedBuffer, 4, nativeBuffer, 1,1, byteLength);
}
}
[BurstCompile]
struct UnityAPISucksJob : IJobParallelFor
{
[ReadOnly] public NativeArray<byte> source;
[WriteOnly] public NativeArray<int> target;
public void Execute(int i)
{
target[i] = (int)source[i];
}
}
public class DetailJobHolder
{
private AsyncGPUReadbackRequest gpuRequest;
RenderTexture detailLayer;
public int detailIndex { get; private set; }
public Terrain terrain;
NativeArray<byte> rawData;
int width, height;
static int[,] resultValues = null;
public bool canceled { get; set; }
public bool IsDone()
{
return gpuRequest.done;
}
public void Dispose()
{
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(detailLayer);
}
private void OnAsynComplete(AsyncGPUReadbackRequest obj)
{
// Unity could we get a way to do this that doesn't suck? Details are int[i,i]
// A) you only use a byte worth of each int anyway (255 max value)
// B) none of this makes sense with GPU stuff
// C) Or native array stuff
// D) Forced into 16mb allocation for 4 2k terrains
if (canceled)
{
if (rawData.IsCreated)
rawData.Dispose();
return;
}
UnityEngine.Profiling.Profiler.BeginSample("Apply Details: Shit Unity API == mem!");
UnityEngine.Profiling.Profiler.BeginSample("Alloc buffer");
if (resultValues == null || width * height != resultValues.Length)
{
resultValues = new int[width, height];
}
UnityEngine.Profiling.Profiler.EndSample();
NativeArray<int> temp = new NativeArray<int>(rawData.Length, Allocator.TempJob);
UnityAPISucksJob job = new UnityAPISucksJob()
{
source = rawData,
target = temp
};
// turns out, forcing the job to complete is faster than doing this asyncronously.
// With this method, in the test scene, we peak at 90ms per update frame. Where as
// in the version that lets this run then finishes later, it peaks at 125ms with
// a less consistent frame rate. Whats odd is that the call to set the data on the
// terrain takes way more time than in the amortized version. I think this is because
// the async readback callback is hapenning earlier in the frame, and whatever that API
// does is async, so it's able to get done faster with less waiting by being earlier in
// the frame. Fucking hell.
job.Schedule(temp.Length, 4096).Complete();
temp.CopyToFast(resultValues);
temp.Dispose();
rawData.Dispose();
UnityEngine.Profiling.Profiler.BeginSample("Set data on terrain");
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(detailLayer);
if (terrain != null && terrain.terrainData != null)
{
terrain.terrainData.SetDetailLayer(0, 0, detailIndex, resultValues);
}
UnityEngine.Profiling.Profiler.EndSample();
UnityEngine.Profiling.Profiler.EndSample();
// this was more direct, but ultimately slower.
/*
UnityEngine.Profiling.Profiler.BeginSample("Apply Details: Shit Unity API == mem!");
UnityEngine.Profiling.Profiler.BeginSample("Alloc buffer");
if (resultValues == null || width * height != resultValues.Length)
{
resultValues = new int[width, height];
}
UnityEngine.Profiling.Profiler.EndSample();
UnityEngine.Profiling.Profiler.BeginSample("Copy to buffer");
rawData.CopyToFastByteToInt(resultValues);
rawData.Dispose();
RenderTexture.active = null;
UnityEngine.Profiling.Profiler.EndSample();
UnityEngine.Profiling.Profiler.BeginSample("Set data on terrain");
RenderTexture.ReleaseTemporary(detailLayer);
terrain.terrainData.SetDetailLayer(0, 0, detailIndex, resultValues);
UnityEngine.Profiling.Profiler.EndSample();
UnityEngine.Profiling.Profiler.EndSample();
Dispose();
*/
}
public void AddJob(RenderTexture detailLayer, int detailIndex)
{
this.width = detailLayer.width;
this.height = detailLayer.height;
this.detailIndex = detailIndex;
this.detailLayer = detailLayer;
if (MicroVerse.noAsyncReadback)
{
Texture2D tex = new Texture2D(detailLayer.width, detailLayer.height, TextureFormat.R8, false, true);
RenderTexture.active = detailLayer;
tex.ReadPixels(new Rect(0, 0, tex.width, tex.height), 0, 0);
RenderTexture.active = null;
tex.Apply();
if (resultValues == null || width * height != resultValues.Length)
{
resultValues = new int[width, height];
}
NativeArray<byte> rawData = tex.GetRawTextureData<byte>();
rawData.CopyToFastByteToInt(resultValues);
rawData.Dispose();
RenderTexture.active = null;
RenderTexture.ReleaseTemporary(detailLayer);
GameObject.DestroyImmediate(tex);
terrain.terrainData.SetDetailLayer(0, 0, detailIndex, resultValues);
}
else
{
rawData = new NativeArray<byte>(width * height, Allocator.Persistent);
gpuRequest = AsyncGPUReadback.RequestIntoNativeArray(ref rawData, detailLayer, 0, OnAsynComplete);
}
}
}
[ExecuteAlways]
public class DetailStamp : Stamp, IDetailModifier
{
public override FilterSet GetFilterSet()
{
return filterSet;
}
public DetailPrototypeSerializable prototype = new DetailPrototypeSerializable();
public DetailPrototypeSettings settings = null;
public FilterSet filterSet = new FilterSet();
Material material;
public bool occludedByOthers = true;
public float minDistanceFromTree = 0;
public float maxDistanceFromTree = 0;
public float minDistanceFromObject = 0;
public float maxDistanceFromObject = 0;
public float minDistanceFromParent = 0;
public float maxDistanceFromParent = 0;
public bool sdfClamp;
[Tooltip("Weight Range in which details will spawn")]
public Vector2 weightRange = new Vector2(0, 999999);
public bool NeedCurvatureMap() { return filterSet.NeedCurvatureMap(); }
public bool NeedFlowMap() { return filterSet.NeedFlowMap(); }
public bool UsesOtherTreeSDF() { return minDistanceFromTree > 0 || maxDistanceFromTree > 0; }
public bool UsesOtherObjectSDF() { return minDistanceFromObject > 0 || maxDistanceFromObject > 0; }
public bool NeedSDF() { return false; }
// do I need my parent to generate an SDF is parented
public bool NeedParentSDF()
{
return minDistanceFromParent > 0 || maxDistanceFromParent > 0;
}
// Do I need to generate an SDF for subspawners
public bool NeedToGenerateSDFForChilden() { return false; }
public void SetSDF(Terrain t, RenderTexture rt) { }
public RenderTexture GetSDF(Terrain t) { return null; }
public override Bounds GetBounds()
{
FalloffOverride fo = GetComponentInParent<FalloffOverride>();
var foType = filterSet.falloffFilter.filterType;
var foFilter = filterSet.falloffFilter;
if (fo != null && fo.enabled)
{
foType = fo.filter.filterType;
foFilter = fo.filter;
}
#if __MICROVERSE_SPLINES__
if (foType == FalloffFilter.FilterType.SplineArea && foFilter.splineArea != null)
{
return foFilter.splineArea.GetBounds();
}
#endif
if (foType == FalloffFilter.FilterType.Global && foFilter.paintArea != null && foFilter.paintArea.clampOutsideOfBounds)
{
return foFilter.paintArea.GetBounds();
}
if (foType == FalloffFilter.FilterType.Global)
return new Bounds(Vector3.zero, new Vector3(99999, 999999, 99999));
else
{
return TerrainUtil.GetBounds(transform);
}
}
static Shader detailShader = null;
public void Initialize()
{
if (detailShader == null)
{
detailShader = Shader.Find("Hidden/MicroVerse/DetailFilter");
}
material = new Material(detailShader);
keywordBuilder.ClearInitial();
filterSet.PrepareMaterial(this.transform, material, keywordBuilder.initialKeywords);
}
static int _Heightmap = Shader.PropertyToID("_Heightmap");
static int _Normalmap = Shader.PropertyToID("_Normalmap");
static int _Curvemap = Shader.PropertyToID("_Curvemap");
static int _Flowmap = Shader.PropertyToID("_Flowmap");
static int _WeightRange = Shader.PropertyToID("_WeightRange");
static int _Density = Shader.PropertyToID("_Density");
static int _PlacementMask = Shader.PropertyToID("_PlacementMask");
static int _IndexMap = Shader.PropertyToID("_IndexMap");
static int _WeightMap = Shader.PropertyToID("_WeightMap");
static int _TextureLayerWeights = Shader.PropertyToID("_TextureLayerWeights");
static int _ClearLayer = Shader.PropertyToID("_ClearLayer");
static int _ClearMask = Shader.PropertyToID("_ClearMask");
static int _DensityNoise = Shader.PropertyToID("_DensityNoise");
public void ApplyDetailStamp(DetailData dd, Dictionary<Terrain, Dictionary<int, List<RenderTexture>>> resultBuffers, OcclusionData od)
{
var proto = prototype;
if (settings != null && settings.prototype != null)
{
proto = settings.prototype;
}
if (!proto.IsValid())
return;
int detailIndex = VegetationUtilities.FindDetailIndex(od.terrain, proto);
var textureLayerWeights = filterSet.GetTextureWeights(od.terrain.terrainData.terrainLayers);
keywordBuilder.Clear();
keywordBuilder.Add("_RECONSTRUCTNORMAL");
UnityEngine.Profiling.Profiler.BeginSample("Detail Modifier");
material.SetTexture(_ClearMask, dd.clearMap);
material.SetFloat(_ClearLayer, dd.layerIndex);
material.SetTexture(_Heightmap, dd.heightMap);
material.SetTexture(_Normalmap, dd.normalMap);
material.SetTexture(_Curvemap, dd.curveMap);
material.SetTexture(_Flowmap, dd.flowMap);
material.SetVector(_WeightRange, weightRange);
#if UNITY_2022_2_OR_NEWER
if (od.terrain.terrainData.detailScatterMode == DetailScatterMode.CoverageMode)
{
material.SetVector(_DensityNoise, Vector2.zero);
material.SetFloat(_Density, 1);
}
else
{
if (proto.density < 1)
{
material.SetFloat(_Density, 1.0f / 128.0f);
material.SetVector(_DensityNoise, new Vector2(1.0f - Mathf.Pow(proto.density, 4), 0.25f));
}
else
{
material.SetFloat(_Density, proto.density / 128.0f);
material.SetVector(_DensityNoise, Vector2.zero);
}
}
#else
if (proto.density < 1)
{
material.SetFloat(_Density, 1.0f / 128.0f);
material.SetVector(_DensityNoise, new Vector2(1.0f - Mathf.Pow(proto.density, 4), 0.25f));
}
else
{
material.SetFloat(_Density, proto.density / 128.0f);
material.SetVector(_DensityNoise, Vector2.zero);
}
#endif
if (material.GetFloat(_Density) < 1)
{
keywordBuilder.Add("_DENSITYNOISENEEDED");
}
if (occludedByOthers)
material.SetTexture(_PlacementMask, od.terrainMask);
else
material.SetTexture(_PlacementMask, null);
float ratio = dd.heightMap.width / dd.terrain.terrainData.size.x;
FilterSet.PrepareSDFFilter(keywordBuilder, material, transform, od, ratio, sdfClamp,
minDistanceFromTree, maxDistanceFromTree,
minDistanceFromObject, maxDistanceFromObject,
minDistanceFromParent, maxDistanceFromParent);
material.SetTexture(_IndexMap, dd.dataCache.indexMaps[dd.terrain]);
material.SetTexture(_WeightMap, dd.dataCache.weightMaps[dd.terrain]);
material.SetVectorArray(_TextureLayerWeights, textureLayerWeights);
filterSet.PrepareTransform(this.transform, dd.terrain, material, keywordBuilder.keywords, GetTerrainScalingFactor(dd.terrain));
keywordBuilder.Assign(material);
RenderTexture rt = RenderTexture.GetTemporary(dd.terrain.terrainData.detailWidth, dd.terrain.terrainData.detailHeight, 0, UnityEngine.Experimental.Rendering.GraphicsFormat.R8_UNorm);
rt.name = "DetailStamp::rt";
Graphics.Blit(null, rt, material);
if (!resultBuffers.ContainsKey(dd.terrain))
resultBuffers.Add(dd.terrain, new Dictionary<int, List<RenderTexture>>());
var dbuffer = resultBuffers[dd.terrain];
if (dbuffer.ContainsKey(detailIndex))
{
dbuffer[detailIndex].Add(rt);
}
else
{
dbuffer.Add(detailIndex, new List<RenderTexture>(1) { rt });
}
UnityEngine.Profiling.Profiler.EndSample();
}
protected override void OnDestroy()
{
if (material != null) DestroyImmediate(material);
base.OnDestroy();
}
public void Dispose()
{
}
void OnDrawGizmosSelected()
{
if (filterSet.falloffFilter.filterType != FalloffFilter.FilterType.Global &&
filterSet.falloffFilter.filterType != FalloffFilter.FilterType.SplineArea)
{
if (MicroVerse.instance != null)
{
Gizmos.color = MicroVerse.instance.options.colors.detailStampColor;
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(new Vector3(0, 0.5f, 0), Vector3.one);
}
}
}
public void InqDetailPrototypes(List<DetailPrototypeSerializable> prototypes)
{
if (settings != null && settings.prototype != null)
{
prototypes.Add(settings.prototype);
}
else
{
prototypes.Add(prototype);
}
}
public bool NeedDetailClear()
{
return false;
}
public void ApplyDetailClear(DetailData td)
{
}
}
}