Created
December 29, 2021 11:14
-
-
Save gotmachine/9167b0e52dfa7c7446a0535b34c4653c to your computer and use it in GitHub Desktop.
Compute each part visible surface area from an arbitrary direction, using a custom shader and a render texture, and analyzing the results through a bursted Job.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using Unity.Burst; | |
using Unity.Collections; | |
using Unity.Jobs; | |
using UnityEngine; | |
using UnityEngine.Profiling; | |
using UnityEngine.Rendering; | |
namespace KSPVesselOcclusionTester | |
{ | |
[KSPAddon(KSPAddon.Startup.Flight, false)] | |
public class OcclusionCamera : MonoBehaviour | |
{ | |
private const int TEXTURE_SIZE = 512; | |
public Camera camera; | |
public RenderTexture renderTexture; | |
private float cameraSize; | |
private Texture2D blackTexture; | |
private List<PartSurfaceInfo> partsInfo = new List<PartSurfaceInfo>(); | |
private class PartSurfaceInfo | |
{ | |
public Part part; | |
public List<Renderer> renderers; | |
public Color partColor; | |
public double surface; | |
public PartSurfaceInfo(Part part, List<Renderer> renderers) | |
{ | |
this.part = part; | |
this.renderers = renderers; | |
partColor = UIntToColor(part.flightID); | |
} | |
} | |
private int shaderPropertyId; | |
private MaterialPropertyBlock materialPropertyBlock; | |
private NativeHashMap<uint, int> partIdIndexes; | |
private NativeArray<uint> textureArray; | |
private NativeArray<int> partsPixelCount; | |
private JobHandle currentJob; | |
private double currentRequestSquareAreaPerPixel; | |
private bool requestDone = true; | |
private bool renderPending = false; | |
private void Start() | |
{ | |
camera = gameObject.AddComponent<Camera>(); | |
camera.enabled = false; | |
camera.orthographic = true; | |
camera.cullingMask = 1; | |
camera.orthographicSize = 3f; // half size of the camera space | |
camera.nearClipPlane = 0f; | |
camera.farClipPlane = 50f; | |
camera.clearFlags = CameraClearFlags.Color; | |
camera.backgroundColor = Color.clear; | |
camera.allowMSAA = false; | |
camera.allowHDR = false; | |
camera.depthTextureMode = DepthTextureMode.Depth; | |
renderTexture = new RenderTexture(TEXTURE_SIZE, TEXTURE_SIZE, 24) | |
{ | |
antiAliasing = 1, | |
filterMode = FilterMode.Point, | |
autoGenerateMips = false | |
}; | |
camera.targetTexture = renderTexture; | |
camera.depthTextureMode = DepthTextureMode.Depth; | |
AssetBundle bundle = AssetBundle.LoadFromFile(@"K:\Projets\KSP\Kerbal Space Program 1.12.2 DEV\GameData\Kerbalism\occlusionshader.ksp"); | |
Shader[] shaders = bundle.LoadAllAssets<Shader>(); | |
bundle.Unload(false); // unload the raw asset bundle | |
camera.SetReplacementShader(shaders[0], null); | |
blackTexture = new Texture2D(TEXTURE_SIZE, TEXTURE_SIZE); | |
for (int y = 0; y < blackTexture.height; y++) | |
for (int x = 0; x < blackTexture.width; x++) | |
blackTexture.SetPixel(x, y, Color.black); | |
blackTexture.Apply(); | |
textureArray = new NativeArray<uint>(TEXTURE_SIZE * TEXTURE_SIZE, Allocator.Persistent); | |
materialPropertyBlock = new MaterialPropertyBlock(); | |
shaderPropertyId = Shader.PropertyToID("_occlusionColor"); | |
} | |
public static Color32 UIntToColor(uint number) | |
{ | |
var intBytes = BitConverter.GetBytes(number); | |
return new Color32(intBytes[0], intBytes[1], intBytes[2], intBytes[3]); | |
} | |
public static uint ColorToUInt(Color32 color) | |
{ | |
return BitConverter.ToUInt32(new byte[]{color.r, color.g , color.b , color.a }, 0); | |
} | |
private void LateUpdate() | |
{ | |
if (requestDone) | |
{ | |
requestDone = false; | |
renderPending = true; | |
Profiler.BeginSample("OcclusionTest.UpdatePartInfos"); | |
// note : this is a quick and dirty thing, ideally we should suscribe to the part count changed gameevent | |
if (partsInfo.Count != FlightGlobals.ActiveVessel.parts.Count) | |
{ | |
int partCount = FlightGlobals.ActiveVessel.parts.Count; | |
partsInfo.Clear(); | |
if (partIdIndexes.IsCreated) | |
partIdIndexes.Dispose(); | |
partIdIndexes = new NativeHashMap<uint, int>(partCount, Allocator.Persistent); | |
for (int i = 0; i < partCount; i++) | |
{ | |
Part part = FlightGlobals.ActiveVessel.parts[i]; | |
partIdIndexes[part.flightID] = i; | |
partsInfo.Add(new PartSurfaceInfo(part, part.FindModelRenderersCached())); | |
} | |
} | |
Profiler.EndSample(); | |
Profiler.BeginSample("OcclusionTest.UpdateRenderers"); | |
FastBounds vesselBounds = new FastBounds(partsInfo[0].part.transform.position); | |
foreach (PartSurfaceInfo partSurfaceInfo in partsInfo) | |
{ | |
Profiler.BeginSample("OcclusionTest.UpdateRenderers.Encapsulate"); | |
vesselBounds.Encapsulate(partSurfaceInfo.part.transform.position); | |
Profiler.EndSample(); | |
Profiler.BeginSample("OcclusionTest.UpdateRenderers.SetPropertyBlock"); | |
foreach (Renderer renderer in partSurfaceInfo.renderers) | |
{ | |
if (renderer.HasPropertyBlock()) | |
renderer.GetPropertyBlock(materialPropertyBlock); // this always overwrite everything in the MaterialPropertyBlock instance | |
else | |
materialPropertyBlock.Clear(); | |
materialPropertyBlock.SetColor(shaderPropertyId, partSurfaceInfo.partColor); | |
renderer.SetPropertyBlock(materialPropertyBlock); | |
} | |
Profiler.EndSample(); | |
} | |
Profiler.EndSample(); | |
Profiler.BeginSample("OcclusionTest.UpdateCamera"); | |
Vector3 vesselSize = FlightGlobals.ActiveVessel.vesselSize; | |
cameraSize = Math.Max(vesselSize.x, Math.Max(vesselSize.y, vesselSize.z)) * 0.6f; | |
Vector3 vesselCenter = vesselBounds.GetCenter(); | |
Vector3 sunDir = (vesselCenter - FlightGlobals.Bodies[0].position).normalized; | |
camera.transform.position = vesselCenter + (-sunDir * cameraSize); | |
camera.orthographicSize = cameraSize; | |
camera.farClipPlane = 50f + cameraSize; | |
camera.transform.forward = sunDir; | |
double pixelLength = (cameraSize * 2.0) / TEXTURE_SIZE; | |
currentRequestSquareAreaPerPixel = pixelLength * pixelLength; | |
Profiler.EndSample(); | |
// note : this can be delayed by WaitForTargetFPS, this measurement isn't significant | |
Profiler.BeginSample("OcclusionTest.UpdateCamera.Render"); | |
camera.Render(); | |
Profiler.EndSample(); | |
} | |
} | |
private struct FastBounds | |
{ | |
private float xMin; | |
private float xMax; | |
private float yMin; | |
private float yMax; | |
private float zMin; | |
private float zMax; | |
public FastBounds(Vector3 initialPoint) | |
{ | |
xMin = xMax = initialPoint.x; | |
yMin = yMax = initialPoint.y; | |
zMin = zMax = initialPoint.z; | |
} | |
public void Encapsulate(Vector3 point) | |
{ | |
if (xMin > point.x) | |
xMin = point.x; | |
else if (xMax < point.x) | |
xMax = point.x; | |
if (yMin > point.y) | |
yMin = point.y; | |
else if (yMax < point.y) | |
yMax = point.y; | |
if (zMin > point.z) | |
zMin = point.z; | |
else if (zMax < point.z) | |
zMax = point.z; | |
} | |
public Vector3 GetCenter() | |
{ | |
return new Vector3( | |
((xMax - xMin) * 0.5f) + xMin, | |
((yMax - yMin) * 0.5f) + yMin, | |
((zMax - zMin) * 0.5f) + zMin); | |
} | |
} | |
private void OnPostRender() | |
{ | |
if (renderPending) | |
{ | |
renderPending = false; | |
AsyncGPUReadback.RequestIntoNativeArray(ref textureArray, renderTexture, 0, OnGPUReadback); | |
} | |
} | |
private void OnGPUReadback(AsyncGPUReadbackRequest readbackRequest) | |
{ | |
if (readbackRequest.hasError) | |
{ | |
requestDone = true; | |
return; | |
} | |
Profiler.BeginSample("OcclusionTest.ParseTextureAsNativeArray"); | |
textureArray = readbackRequest.GetData<uint>(); | |
partsPixelCount = new NativeArray<int>(partsInfo.Count, Allocator.TempJob); | |
ProcessTextureJob processTextureJob = new ProcessTextureJob(); | |
processTextureJob.textureData = textureArray; | |
processTextureJob.partIdIndexes = partIdIndexes; | |
processTextureJob.partsPixelCount = partsPixelCount; | |
currentJob = processTextureJob.Schedule(textureArray.Length, new JobHandle()); | |
StartCoroutine(WaitForTextureProcessing()); | |
Profiler.EndSample(); | |
} | |
// perf figures : | |
// 0.2-0.5 ms with burst | |
// 8-12 ms without | |
[BurstCompile] | |
public struct ProcessTextureJob : IJobFor | |
{ | |
[ReadOnly] public NativeArray<uint> textureData; | |
[ReadOnly] public NativeHashMap<uint, int> partIdIndexes; | |
public NativeArray<int> partsPixelCount; | |
public void Execute(int index) | |
{ | |
uint color = textureData[index]; | |
if (color == 0) | |
return; | |
if (partIdIndexes.TryGetValue(color, out int resultIndex)) | |
partsPixelCount[resultIndex] = partsPixelCount[resultIndex] + 1; | |
} | |
} | |
private IEnumerator WaitForTextureProcessing() | |
{ | |
if (!currentJob.IsCompleted) | |
yield return null; | |
Profiler.BeginSample("OcclusionTest.UpdatePartInfoSurface"); | |
foreach (PartSurfaceInfo partSurfaceInfo in partsInfo) | |
{ | |
if (partIdIndexes.TryGetValue(partSurfaceInfo.part.flightID, out int partIndex)) | |
{ | |
partSurfaceInfo.surface = partsPixelCount[partIndex] * currentRequestSquareAreaPerPixel; | |
} | |
else | |
{ | |
partSurfaceInfo.surface = 0.0; | |
} | |
} | |
Profiler.EndSample(); | |
partsPixelCount.Dispose(); | |
requestDone = true; | |
} | |
private void OnGUI() | |
{ | |
GUI.DrawTexture(new Rect(0f, 0f, TEXTURE_SIZE, TEXTURE_SIZE), blackTexture); | |
GUI.DrawTexture(new Rect(0f, 0f, TEXTURE_SIZE, TEXTURE_SIZE), renderTexture); | |
float vPos = 0f; | |
foreach (PartSurfaceInfo partSurfaceInfo in partsInfo) | |
{ | |
GUI.Label(new Rect(512f, vPos, 400f, 20f), $"{partSurfaceInfo.part.partInfo.name} : {partSurfaceInfo.surface:F3}m²"); | |
vPos += 20f; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Shader "Custom/OcclusionShader" | |
{ | |
Properties | |
{ | |
_occlusionColor ("Color", Color) = (0.5,0.5,0.5,1) | |
} | |
SubShader | |
{ | |
Pass | |
{ | |
CGPROGRAM | |
// pragmas | |
#pragma vertex vert | |
#pragma fragment frag | |
uniform float4 _occlusionColor; | |
struct vertexInput | |
{ | |
float4 vertex: POSITION; | |
}; | |
struct vertexOutput | |
{ | |
float4 pos: SV_POSITION; | |
}; | |
vertexOutput vert(vertexInput v) | |
{ | |
vertexOutput o; | |
o.pos = UnityObjectToClipPos(v.vertex); | |
return o; | |
} | |
float4 frag(vertexOutput i) : COLOR | |
{ | |
return _occlusionColor; | |
} | |
ENDCG | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment