Skip to content

Instantly share code, notes, and snippets.

@gotmachine
Created December 29, 2021 11:14
Show Gist options
  • Save gotmachine/9167b0e52dfa7c7446a0535b34c4653c to your computer and use it in GitHub Desktop.
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.
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;
}
}
}
}
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