Kursach_StealthAction/Assets/Tremble/Utils/MapTextureSnapshotter.cs
2026-10-11 19:16:11 +03:00

468 lines
14 KiB
C#

//
// This file is part of the Tremble package by Tiny Goose.
// Copyright (c) 2024-2026 TinyGoose Ltd., All Rights Reserved.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using Debug = UnityEngine.Debug;
#if HDRP_INSTALLED
using File = System.IO.File;
#endif
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace TinyGoose.Tremble
{
/// <summary>
/// Camera/lighting setup for taking material screenshots. Do not use directly.
/// </summary>
public class MapTextureSnapshotter
{
// -----------------------------------------------------------------------------------------------------------------------------
// State
// -----------------------------------------------------------------------------------------------------------------------------
private GameObject m_SceneRoot;
private Camera m_Camera;
private MeshRenderer m_Renderer;
private readonly Dictionary<Vector2Int, TexturePair> m_RenderTextures = new();
private Texture2D m_CaptureTexture;
private Light m_StudioLight;
private AmbientMode m_OriginalAmbientMode;
private Color m_OriginalAmbientColour;
private float m_OriginalAmbientIntensity;
private readonly List<GameObject> m_Lights = new();
private TrembleSyncSettings m_Settings;
private struct TexturePair
{
public Texture2D Texture;
public RenderTexture RenderTexture;
}
public void Init(TrembleSyncSettings settings)
{
m_Settings = settings;
SetupGameObjects(m_Settings.MaterialCaptureLightIntensity, m_Settings.MaterialCaptureLightAngle);
List<Vector2Int> resolutions = new()
{
m_Settings.MaterialExportSize.ToVector2IntSize()
};
foreach (Material material in m_Settings.GetMaterialGroupsOrDefault().SelectMany(mg => mg.Materials))
{
MaterialRenderData renderData = m_Settings.GetMaterialRenderData(material);
Vector2Int dataRes = new(renderData.ResolutionX.ToIntSize(), renderData.ResolutionY.ToIntSize());
if (!resolutions.Contains(dataRes))
resolutions.Add(dataRes);
if (!material.TryGetMainTex(out Texture2D tex))
continue;
Vector2Int res = new(tex.width, tex.height);
if (!resolutions.Contains(res))
resolutions.Add(res);
}
foreach (Vector2Int res in resolutions)
{
GetOrCreateTexturePair(res);
}
// Move far away!
m_SceneRoot.transform.position = Vector3.up * 10000f;
// Set up lighting
m_OriginalAmbientMode = RenderSettings.ambientMode;
m_OriginalAmbientColour = RenderSettings.ambientLight;
m_OriginalAmbientIntensity = RenderSettings.ambientIntensity;
#if UNITY_6000_5_OR_NEWER
foreach (Light foundLight in GameObject.FindObjectsByType<Light>(FindObjectsInactive.Exclude))
#elif UNITY_2022_1_OR_NEWER
foreach (Light foundLight in GameObject.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
#else
foreach (Light foundLight in GameObject.FindObjectsOfType<Light>(false))
#endif
{
if (!foundLight.gameObject.activeSelf || foundLight == m_StudioLight)
continue;
m_Lights.Add(foundLight.gameObject);
foundLight.gameObject.SetActive(false);
}
RenderSettings.ambientMode = AmbientMode.Flat;
RenderSettings.ambientLight = Color.white;
}
public void DeInit()
{
GameObject.DestroyImmediate(m_SceneRoot);
if (m_RenderTextures.Values.Any(x => RenderTexture.active == x.RenderTexture))
{
RenderTexture.active = null;
}
// Release RT next frame - it seems if it's active above, it's not
// cleared/unset until next frame causing an error (:
foreach (TexturePair texPair in m_RenderTextures.Values)
{
#if UNITY_EDITOR
EditorApplication.delayCall += () =>
{
if (texPair.RenderTexture)
{
#endif
texPair.RenderTexture.Release();
#if UNITY_EDITOR
}
};
#endif
}
m_RenderTextures.Clear();
foreach (GameObject foundLight in m_Lights)
{
foundLight.SetActive(true);
}
m_Lights.Clear();
// Restore lighting
RenderSettings.ambientMode = m_OriginalAmbientMode;
RenderSettings.ambientLight = m_OriginalAmbientColour;
RenderSettings.ambientIntensity = m_OriginalAmbientIntensity;
}
private TexturePair GetOrCreateTexturePair(Vector2Int resolution)
{
if (!m_RenderTextures.TryGetValue(resolution, out TexturePair texturePair))
{
RenderTexture rt = new(resolution.x, resolution.y, 8, GraphicsFormat.R8G8B8A8_UNorm);
rt.Create();
Texture2D output = new(resolution.x, resolution.y, rt.graphicsFormat, TextureCreationFlags.None);
m_RenderTextures[resolution] = texturePair = new()
{
Texture = output,
RenderTexture = rt,
};
}
return texturePair;
}
public void SnapshotMaterial(Material m, string outPath)
{
MaterialRenderData renderData = m_Settings.GetMaterialRenderData(m);
Vector2Int res = renderData.GetResolutionOrDefault();
Texture2D mainTex = null;
if (renderData.ExportMode == MaterialImportMode.Dynamic)
{
if (!m.TryGetMainTex(out mainTex))
{
renderData.ExportMode = MaterialImportMode.FixedSize;
Debug.LogWarning($"Couldn't find a Main Texture for material \"{m.name}\"," +
"falling back to legacy render mode." +
"Make sure the shader has a texture marked as [Main Texture] or" +
"a texture named _MainTex, _BaseColor, or _Color.");
}
if (mainTex && !renderData.IsResolutionOverridden)
{
res = new(mainTex.width, mainTex.height);
}
}
TexturePair tex = GetOrCreateTexturePair(res);
// Do not remove - these are used in HDRP
bool createdTempMaterial = false;
Material tempMaterial = null;
switch (renderData.ExportMode)
{
case MaterialImportMode.FixedSize:
// Set up RT and material - we have to save it for HDRP
createdTempMaterial = TryCreateUnlitCopy(m, out tempMaterial, renderData.ExportMode);
#if HDRP_INSTALLED && UNITY_EDITOR
if (createdTempMaterial)
{
string tempName = Path.GetFileNameWithoutExtension(AssetDatabase.GetAssetPath(m)) + "_TrembleTemp.mat";
string tempPath = Path.Combine("Assets", tempName);
AssetDatabase.CreateAsset(tempMaterial, tempPath);
}
#endif
m_Renderer.material = tempMaterial ? tempMaterial : m;
// Render once
m_Camera.targetTexture = tex.RenderTexture;
m_Camera.Render();
if (RenderTexture.active != tex.RenderTexture)
RenderTexture.active = tex.RenderTexture;
// Copy texture from RT
tex.Texture.ReadPixels(new(0, 0, res.x, res.y), 0, 0);
File.WriteAllBytes(outPath, tex.Texture.EncodeToPNG());
break;
case MaterialImportMode.Dynamic:
#if UNITY_EDITOR
string path = AssetDatabase.GetAssetPath(mainTex);
//TODO(jwf): colourise by main colour?
// Is it a PNG, and we're not overriding resolution? Just copy it directly
if (!renderData.IsResolutionOverridden && path.EndsWithInvariant(".png"))
{
File.Copy(Application.dataPath + path.Remove(0, 6), outPath, true);
return;
}
#endif
// Else we make a copy to resize
m_Camera.targetTexture = null;
GL.sRGBWrite = true;
Graphics.Blit(mainTex, tex.RenderTexture);
RenderTexture.active = tex.RenderTexture;
// Copy texture from RT
tex.Texture.ReadPixels(new(0, 0, res.x, res.y), 0, 0);
File.WriteAllBytes(outPath, tex.Texture.EncodeToPNG());
break;
}
// Delete temp material, if we made one
#if HDRP_INSTALLED && UNITY_EDITOR
if (createdTempMaterial)
{
AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(tempMaterial));
}
#endif
}
private static readonly Dictionary<string, string> s_ShaderRemaps = new()
{
{ "Standard", "Legacy Shaders/Diffuse" }, // or Particles/Standard Unlit?
{ "Universal Render Pipeline/Lit", "Universal Render Pipeline/Unlit" },
{ "HDRP/Lit", "HDRP/Unlit" }
};
private bool TryCreateUnlitCopy(Material material, out Material newMaterial, MaterialImportMode mode)
{
// No lights in the scene. Assume we don't need to tweak materials as they're using ambient colour
if (mode is MaterialImportMode.Dynamic || (m_Lights.Count == 0 && mode is MaterialImportMode.FixedSize))
{
newMaterial = material;
return false;
}
#if UNITY_EDITOR
foreach ((string originalShader, string remapShader) in s_ShaderRemaps)
{
if (!material.shader.name.Equals(originalShader))
continue;
newMaterial = mode is MaterialImportMode.FixedSize ? new(Shader.Find(remapShader)) : new(Shader.Find(originalShader));
CopyMaterialProperties_Recursive(material, newMaterial);
// Ensure colours/textures set properly
newMaterial.color = material.color;
newMaterial.mainTexture = material.mainTexture;
// Render after post-processing (mostly for HDRP)
newMaterial.renderQueue = 2501;
return true;
}
#endif
newMaterial = material;
return false;
}
[Conditional("UNITY_EDITOR")]
private static void CopyMaterialProperties_Recursive(Material from, Material to)
{
#if UNITY_EDITOR
#if UNITY_2022_1_OR_NEWER
if (from.parent)
{
// If parent, copy that one first!
CopyMaterialProperties_Recursive(from.parent, to);
}
#endif
// Copy shader keywords
to.shaderKeywords = from.shaderKeywords;
// Iterate over all properties of the material
Shader shader = from.shader;
#if UNITY_6000_5_OR_NEWER
int propertyCount = shader.GetPropertyCount();
for (int i = 0; i < propertyCount; i++)
{
// Get the property type and name
ShaderPropertyType propertyType = shader.GetPropertyType(i);
string propertyName = shader.GetPropertyName(i);
// Copy the property value based on its type
switch (propertyType)
{
case ShaderPropertyType.Color:
to.SetColor(propertyName, from.GetColor(propertyName));
break;
case ShaderPropertyType.Vector:
to.SetVector(propertyName, from.GetVector(propertyName));
break;
case ShaderPropertyType.Float:
case ShaderPropertyType.Range:
to.SetFloat(propertyName, from.GetFloat(propertyName));
break;
case ShaderPropertyType.Texture:
to.SetTexture(propertyName, from.GetTexture(propertyName));
to.SetTextureOffset(propertyName, from.GetTextureOffset(propertyName));
to.SetTextureScale(propertyName, from.GetTextureScale(propertyName));
break;
}
}
#else
int propertyCount = ShaderUtil.GetPropertyCount(shader);
for (int i = 0; i < propertyCount; i++)
{
// Get the property type and name
ShaderUtil.ShaderPropertyType propertyType = ShaderUtil.GetPropertyType(shader, i);
string propertyName = ShaderUtil.GetPropertyName(shader, i);
// Copy the property value based on its type
switch (propertyType)
{
case ShaderUtil.ShaderPropertyType.Color:
to.SetColor(propertyName, from.GetColor(propertyName));
break;
case ShaderUtil.ShaderPropertyType.Vector:
to.SetVector(propertyName, from.GetVector(propertyName));
break;
case ShaderUtil.ShaderPropertyType.Float:
case ShaderUtil.ShaderPropertyType.Range:
to.SetFloat(propertyName, from.GetFloat(propertyName));
break;
case ShaderUtil.ShaderPropertyType.TexEnv:
to.SetTexture(propertyName, from.GetTexture(propertyName));
to.SetTextureOffset(propertyName, from.GetTextureOffset(propertyName));
to.SetTextureScale(propertyName, from.GetTextureScale(propertyName));
break;
}
}
#endif
// Copy render queue and other settings
to.renderQueue = from.renderQueue;
to.enableInstancing = from.enableInstancing;
to.doubleSidedGI = from.doubleSidedGI;
#endif
}
private void SetupGameObjects(float lightIntensity, float lightAngle)
{
m_SceneRoot = new("Scene Root");
// Set up gameobjects
GameObject snapCamera = new("Camera");
snapCamera.transform.SetParent(m_SceneRoot.transform, false);
snapCamera.transform.localPosition = new(0f, 0f, -0.1f);
m_Camera = snapCamera.AddComponent<Camera>();
m_Camera.orthographic = true;
m_Camera.allowHDR = false;
m_Camera.orthographicSize = 0.5f;
m_Camera.nearClipPlane = 0.01f;
m_Camera.farClipPlane = 0.5f;
m_Camera.backgroundColor = Color.black;
m_Camera.clearFlags = CameraClearFlags.Color;
Vector3 lightDirection = Vector3.Lerp(Vector3.right, Vector3.forward, lightAngle);
GameObject snapLight = new("Light");
snapLight.transform.SetParent(m_SceneRoot.transform, false);
snapLight.transform.localPosition = new(0f, 0f, -0.1f);
snapLight.transform.rotation = Quaternion.LookRotation(lightDirection);
m_StudioLight = snapLight.AddComponent<Light>();
m_StudioLight.type = LightType.Directional;
m_StudioLight.intensity = lightIntensity;
m_StudioLight.shadows = LightShadows.None;
m_StudioLight.color = Color.white;
GameObject snapQuad = new("Quad");
snapQuad.transform.SetParent(m_SceneRoot.transform, false);
snapQuad.transform.localPosition = Vector3.zero;
MeshFilter meshFilter = snapQuad.AddComponent<MeshFilter>();
meshFilter.sharedMesh = CreateQuadMesh();
m_Renderer = snapQuad.AddComponent<MeshRenderer>();
}
private Mesh CreateQuadMesh()
{
Mesh quadMesh = new()
{
name = "Quad",
vertices = new Vector3[]
{
new(-0.5f, -0.5f, 0f),
new(-0.5f, +0.5f, 0f),
new(+0.5f, +0.5f, 0f),
new(+0.5f, -0.5f, 0f),
},
normals = new Vector3[]
{
new(0f, 0f, -1f),
new(0f, 0f, -1f),
new(0f, 0f, -1f),
new(0f, 0f, -1f),
},
uv = new Vector2[]
{
new(0f, 0f),
new(0f, 1f),
new(1f, 1f),
new(1f, 0f),
},
};
List<int> indices = new() { 0, 1, 2, 0, 2, 3 };
quadMesh.SetIndices(indices, MeshTopology.Triangles, 0);
return quadMesh;
}
}
}