summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Impostor.shader60
-rw-r--r--Scripts/Impostors.cs432
-rw-r--r--impostor.cginc129
3 files changed, 393 insertions, 228 deletions
diff --git a/Impostor.shader b/Impostor.shader
new file mode 100644
index 0000000..1dff6c9
--- /dev/null
+++ b/Impostor.shader
@@ -0,0 +1,60 @@
+Shader "yum_food/Gimmicks/Impostors"
+{
+ Properties
+ {
+ _ImpostorAtlas("Impostor Atlas", 2D) = "white" {}
+ _GridResolution("Grid Resolution", Int) = 5
+ _Cutoff("Alpha Cutoff", Range(0, 1)) = 0.5
+ _Color("Tint", Color) = (1, 1, 1, 1)
+
+ [Toggle] _DebugMode("Debug Mode", Float) = 0
+
+ [Enum(UnityEngine.Rendering.CullMode)] _Cull("Cull", Float) = 0
+ [Enum(Off, 0, On, 1)] _ZWrite("ZWrite", Int) = 1
+ }
+
+ SubShader
+ {
+ Tags { "RenderType" = "TransparentCutout" "Queue" = "AlphaTest" }
+
+ Pass
+ {
+ Name "FORWARD"
+ Tags { "LightMode" = "ForwardBase" }
+
+ Cull [_Cull]
+ ZWrite [_ZWrite]
+
+ CGPROGRAM
+ #pragma target 3.0
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma multi_compile_fog
+ #pragma multi_compile_instancing
+
+ #include "impostor.cginc"
+ ENDCG
+ }
+
+ Pass
+ {
+ Name "SHADOW"
+ Tags { "LightMode" = "ShadowCaster" }
+
+ Cull [_Cull]
+
+ CGPROGRAM
+ #pragma target 3.0
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma multi_compile_shadowcaster
+ #pragma multi_compile_instancing
+
+ #define IMPOSTOR_SHADOW_PASS
+ #include "impostor.cginc"
+ ENDCG
+ }
+ }
+
+ FallBack "Transparent/Cutout/Diffuse"
+}
diff --git a/Scripts/Impostors.cs b/Scripts/Impostors.cs
index 92ea581..6d8d67b 100644
--- a/Scripts/Impostors.cs
+++ b/Scripts/Impostors.cs
@@ -5,274 +5,250 @@ using System.IO;
[ExecuteInEditMode]
public class Impostors : MonoBehaviour
{
- [Header("Bounding Sphere")]
- [Tooltip("Sphere radius is controlled by the Transform scale (uses X component)")]
- public float sphere_radius_ = 1f;
+ [Header("Bounding Sphere")]
+ public float sphere_radius_ = 1f;
- [Header("Grid Settings")]
- [Tooltip("Number of lattice points along each axis (e.g., 5 = 5x5 = 25 points)")]
- [Range(2, 20)]
- public int gridResolution = 5;
+ [Header("Grid Settings")]
+ [Range(2, 20)] public int gridResolution = 5;
- [Header("Camera Settings")]
- [Range(1, 4096)]
- public int cameraResolution = 256;
+ [Header("Camera Settings")]
+ [Range(1, 4096)] public int cameraResolution = 256;
+ public float nearClippingDistance = 0.01f;
+ public LayerMask cullingMask = -1;
+ public bool renderSkybox = false;
- [Tooltip("Near clipping distance - cameras are placed this distance outside the sphere")]
- public float nearClippingDistance = 0.01f;
+ [Header("Original Mesh")]
+ public GameObject originalMesh;
- [Tooltip("Layers to render when baking")]
- public LayerMask cullingMask = -1;
+ [HideInInspector] public Camera[] cameras;
- [Tooltip("Render skybox in baked images")]
- public bool renderSkybox = false;
+ private GameObject impostorObject;
+ private Material impostorMaterial;
- [HideInInspector]
- public GameObject[] cameraObjects;
- [HideInInspector]
- public Camera[] cameras;
+ public bool HasImpostor => impostorObject != null;
+ private float Radius => sphere_radius_ * transform.lossyScale.x;
+ private const string OutputFolder = "Assets/yum_food/3ner/Impostor_Generated";
- private float radius() { return sphere_radius_ * transform.lossyScale.x; }
+ void OnEnable() => Camera.onPreRender += UpdateMainCameraPos;
+ void OnDisable() => Camera.onPreRender -= UpdateMainCameraPos;
- void OnDrawGizmos()
- {
- // Use transform scale directly for real-time gizmo updates
- float currentRadius = radius();
+ static void UpdateMainCameraPos(Camera cam)
+ {
+ if (cam.cameraType == CameraType.Game || cam.cameraType == CameraType.SceneView)
+ Shader.SetGlobalVector("_ImpostorMainCameraPos", cam.transform.position);
+ }
- // Draw the bounding sphere
- Gizmos.color = Color.cyan;
- Gizmos.DrawWireSphere(transform.position, currentRadius);
+ void OnDrawGizmos()
+ {
+ Gizmos.color = Color.cyan;
+ Gizmos.DrawWireSphere(transform.position, Radius);
- // Draw the camera positions and directions
- if (Application.isEditor && gridResolution > 0)
+ if (Application.isEditor && gridResolution > 0)
+ {
+ Gizmos.color = Color.yellow;
+ for (int y = 0; y < gridResolution; y++)
+ {
+ for (int x = 0; x < gridResolution; x++)
{
- Gizmos.color = Color.yellow;
-
- for (int y = 0; y < gridResolution; y++)
- {
- for (int x = 0; x < gridResolution; x++)
- {
- Vector3 hemispherePos = PlaneToHemiOctahedron(x, y);
- Vector3 worldPos = transform.position + hemispherePos * (currentRadius + nearClippingDistance);
-
- // Draw camera position
- Gizmos.DrawSphere(worldPos, currentRadius * 0.05f);
-
- // Draw line to sphere center
- Gizmos.DrawLine(worldPos, transform.position);
- }
- }
+ Vector3 worldPos = transform.position + PlaneToHemiOctahedron(x, y) * (Radius + nearClippingDistance);
+ Gizmos.DrawSphere(worldPos, Radius * 0.05f);
+ Gizmos.DrawLine(worldPos, transform.position);
}
+ }
}
+ }
- // Port of plane_to_hemi_octahedron from vertex_deformation.slang
- Vector3 PlaneToHemiOctahedron(int gridX, int gridY)
- {
- // Map grid indices to [-1, 1] plane coordinates
- float u = (gridX / (float)(gridResolution - 1)) * 2f - 1f;
- float v = (gridY / (float)(gridResolution - 1)) * 2f - 1f;
+ Vector3 PlaneToHemiOctahedron(int gridX, int gridY)
+ {
+ float x = (gridX / (float)(gridResolution - 1)) * 2f - 1f;
+ float z = (gridY / (float)(gridResolution - 1)) * 2f - 1f;
- float x = u;
- float z = v;
+ // Rotate 45° to fit square into diamond
+ float x_rot = (x + z) * 0.5f;
+ float z_rot = (z - x) * 0.5f;
- // Rotate 45° and scale to fit square into diamond
- float x_rot = (x + z) * 0.5f;
- float z_rot = (z - x) * 0.5f;
+ // Octahedral decode
+ float y = Mathf.Max(0f, 1f - Mathf.Abs(x_rot) - Mathf.Abs(z_rot));
- // Octahedral decode: y = 1 - |x| - |z|, clamped to hemisphere
- float y = Mathf.Max(0f, 1f - Mathf.Abs(x_rot) - Mathf.Abs(z_rot));
+ // Normalize
+ Vector3 oct_pos = new Vector3(x_rot, y, z_rot).normalized;
- // Normalize to unit sphere
- Vector3 oct_pos = new Vector3(x_rot, y, z_rot);
- oct_pos.Normalize();
+ // Rotate back by -45° around y
+ float rcp_sqrt2 = 0.70710678f;
+ float x_unrot = (oct_pos.x - oct_pos.z) * rcp_sqrt2;
+ float z_unrot = (oct_pos.x + oct_pos.z) * rcp_sqrt2;
- // Rotate back by -45° around y to undo input rotation
- float RCP_SQRT_2 = 0.70710678f;
- float x_unrot = (oct_pos.x - oct_pos.z) * RCP_SQRT_2;
- float z_unrot = (oct_pos.x + oct_pos.z) * RCP_SQRT_2;
- oct_pos = new Vector3(x_unrot, oct_pos.y, z_unrot);
+ return new Vector3(x_unrot, oct_pos.y, z_unrot);
+ }
- return oct_pos;
- }
+ public void CreateCameras()
+ {
+ DestroyExistingCameras();
- public void CreateCameras()
- {
- // Clean up existing cameras
- DestroyExistingCameras();
-
- // Create parent GameObject for all cameras
- GameObject camerasParent = new GameObject("Cameras");
- camerasParent.transform.parent = transform;
- camerasParent.transform.localPosition = Vector3.zero;
- camerasParent.transform.localRotation = Quaternion.identity;
- camerasParent.transform.localScale = Vector3.one;
-
- int totalCameras = gridResolution * gridResolution;
- cameraObjects = new GameObject[totalCameras];
- cameras = new Camera[totalCameras];
-
- int index = 0;
- for (int y = 0; y < gridResolution; y++)
- {
- for (int x = 0; x < gridResolution; x++)
- {
- // Get position on hemisphere
- Vector3 hemisphereDir = PlaneToHemiOctahedron(x, y);
- float currentRadius = radius();
-
- // Place camera offset distance outside bounding sphere surface
- Vector3 worldPos = transform.position + hemisphereDir * (currentRadius + nearClippingDistance);
-
- // Create camera GameObject
- GameObject camObj = new GameObject($"ImpostorCamera_{x}_{y}");
- camObj.transform.parent = camerasParent.transform;
- camObj.transform.position = worldPos;
- camObj.transform.LookAt(transform.position);
-
- // Add and configure camera
- Camera cam = camObj.AddComponent<Camera>();
- cam.orthographic = true;
- cam.orthographicSize = sphere_radius_;
- cam.nearClipPlane = nearClippingDistance;
- cam.farClipPlane = sphere_radius_ * 2f + nearClippingDistance;
- cam.cullingMask = cullingMask;
- if (!renderSkybox)
- {
- cam.clearFlags = CameraClearFlags.SolidColor;
- cam.backgroundColor = Color.clear;
- }
- cam.enabled = false; // Only enable during baking
-
- cameraObjects[index] = camObj;
- cameras[index] = cam;
- index++;
- }
- }
+ GameObject parent = new GameObject("Cameras");
+ parent.transform.SetParent(transform, false);
- Debug.Log($"Created {totalCameras} impostor cameras");
- }
+ cameras = new Camera[gridResolution * gridResolution];
+ int idx = 0;
- public void DestroyExistingCameras()
+ for (int y = 0; y < gridResolution; y++)
{
- // Find and destroy the "Cameras" parent GameObject
- Transform camerasTransform = transform.Find("Cameras");
- if (camerasTransform != null)
- {
- DestroyImmediate(camerasTransform.gameObject);
- }
-
- cameraObjects = null;
- cameras = null;
+ for (int x = 0; x < gridResolution; x++)
+ {
+ Vector3 localDir = PlaneToHemiOctahedron(x, y);
+ Vector3 worldPos = transform.position + (transform.rotation * localDir) * (Radius + nearClippingDistance);
+
+ GameObject camObj = new GameObject($"Camera_{x}_{y}");
+ camObj.transform.SetParent(parent.transform, false);
+ camObj.transform.position = worldPos;
+ camObj.transform.LookAt(transform.position, transform.up);
+
+ Camera cam = camObj.AddComponent<Camera>();
+ cam.orthographic = true;
+ cam.orthographicSize = sphere_radius_;
+ cam.nearClipPlane = nearClippingDistance;
+ cam.farClipPlane = sphere_radius_ * 2f + nearClippingDistance;
+ cam.cullingMask = cullingMask;
+ cam.clearFlags = renderSkybox ? CameraClearFlags.Skybox : CameraClearFlags.SolidColor;
+ cam.backgroundColor = Color.clear;
+ cam.enabled = false;
+
+ cameras[idx++] = cam;
+ }
}
-
- public void BakeTexture()
+ }
+
+ public void DestroyExistingCameras()
+ {
+ Transform camsTransform = transform.Find("Cameras");
+ if (camsTransform != null) DestroyImmediate(camsTransform.gameObject);
+ cameras = null;
+ }
+
+ public void BakeTexture()
+ {
+ SetRenderersEnabled(true);
+ if (cameras == null || cameras.Length != gridResolution * gridResolution || cameras[0] == null) CreateCameras();
+
+ // Render atlas
+ int size = cameraResolution * gridResolution;
+ Texture2D atlas = new Texture2D(size, size, TextureFormat.RGBA32, false);
+ RenderTexture rt = RenderTexture.GetTemporary(cameraResolution, cameraResolution, 24);
+
+ int idx = 0;
+ for (int y = 0; y < gridResolution; y++)
{
- // Create a texture atlas to hold all camera views
- int texWidth = cameraResolution * gridResolution;
- int texHeight = cameraResolution * gridResolution;
- Texture2D atlasTexture = new Texture2D(texWidth, texHeight, TextureFormat.RGBA32, false);
-
- // Create temporary render texture for each camera
- RenderTexture rt = RenderTexture.GetTemporary(cameraResolution, cameraResolution, 24);
+ for (int x = 0; x < gridResolution; x++)
+ {
+ cameras[idx++].targetTexture = rt;
+ cameras[idx - 1].Render();
+ RenderTexture.active = rt;
+ Texture2D temp = new Texture2D(cameraResolution, cameraResolution);
+ temp.ReadPixels(new Rect(0, 0, cameraResolution, cameraResolution), 0, 0);
+ temp.Apply();
+ atlas.SetPixels(x * cameraResolution, y * cameraResolution, cameraResolution, cameraResolution, temp.GetPixels());
+ DestroyImmediate(temp);
+ }
+ }
+ atlas.Apply();
+ RenderTexture.active = null;
+ RenderTexture.ReleaseTemporary(rt);
- int index = 0;
- for (int y = 0; y < gridResolution; y++)
- {
- for (int x = 0; x < gridResolution; x++)
- {
- Camera cam = cameras[index];
-
- // Render camera to RT
- cam.targetTexture = rt;
- cam.Render();
-
- // Read pixels from RT
- RenderTexture.active = rt;
- Texture2D temp = new Texture2D(cameraResolution, cameraResolution, TextureFormat.RGBA32, false);
- temp.ReadPixels(new Rect(0, 0, cameraResolution, cameraResolution), 0, 0);
- temp.Apply();
-
- // Copy to atlas at correct position
- int atlasX = x * cameraResolution;
- int atlasY = y * cameraResolution;
- atlasTexture.SetPixels(atlasX, atlasY, cameraResolution, cameraResolution, temp.GetPixels());
-
- DestroyImmediate(temp);
- index++;
- }
- }
+ // Save
+ if (!AssetDatabase.IsValidFolder(OutputFolder))
+ {
+ if (!AssetDatabase.IsValidFolder("Assets/yum_food/3ner"))
+ AssetDatabase.CreateFolder("Assets/yum_food", "3ner");
+ AssetDatabase.CreateFolder("Assets/yum_food/3ner", "Impostor_Generated");
+ }
- atlasTexture.Apply();
- RenderTexture.active = null;
- RenderTexture.ReleaseTemporary(rt);
+ string name = gameObject.name.Replace(" ", "_");
+ string path = Path.Combine(OutputFolder, $"{name}_atlas.png");
+ File.WriteAllBytes(Path.Combine(Application.dataPath, "..", path), atlas.EncodeToPNG());
+ DestroyImmediate(atlas);
- // Save texture to file
- byte[] bytes = atlasTexture.EncodeToPNG();
+ AssetDatabase.Refresh();
+ TextureImporter importer = AssetImporter.GetAtPath(path) as TextureImporter;
+ if (importer != null)
+ {
+ importer.mipmapEnabled = true;
+ importer.alphaIsTransparency = true;
+ importer.wrapMode = TextureWrapMode.Clamp;
+ importer.filterMode = FilterMode.Trilinear;
+ importer.SaveAndReimport();
+ }
- // Get currently selected folder in Project window
- string folder = "Assets";
- if (Selection.activeObject != null)
- {
- string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject);
- if (!string.IsNullOrEmpty(selectedPath))
- {
- if (AssetDatabase.IsValidFolder(selectedPath))
- {
- folder = selectedPath;
- }
- else
- {
- folder = Path.GetDirectoryName(selectedPath);
- }
- }
- }
+ // Create impostor
+ Texture2D tex = AssetDatabase.LoadAssetAtPath<Texture2D>(path);
+ if (tex != null)
+ {
+ DestroyExistingImpostor();
- string assetPath = Path.Combine(folder, "ho_bake.png");
- string fullPath = Path.Combine(Application.dataPath, "..", assetPath);
- File.WriteAllBytes(fullPath, bytes);
+ Shader shader = Shader.Find("yum_food/Gimmicks/Impostors");
+ if (shader == null) { Debug.LogError("Shader not found"); return; }
- Debug.Log($"Baked texture saved to: {assetPath}");
- Debug.Log($"Atlas size: {texWidth}x{texHeight} ({gridResolution}x{gridResolution} grid of {cameraResolution}x{cameraResolution} images)");
+ impostorMaterial = new Material(shader);
+ impostorMaterial.SetTexture("_ImpostorAtlas", tex);
+ impostorMaterial.SetInt("_GridResolution", gridResolution);
+ AssetDatabase.CreateAsset(impostorMaterial, Path.Combine(OutputFolder, $"{name}_mat.mat"));
- // Refresh asset database
- AssetDatabase.Refresh();
+ impostorObject = GameObject.CreatePrimitive(PrimitiveType.Quad);
+ impostorObject.name = "Impostor";
+ impostorObject.transform.SetParent(transform, false);
+ impostorObject.transform.localScale = Vector3.one * sphere_radius_ * 2f;
+ DestroyImmediate(impostorObject.GetComponent<Collider>());
+ impostorObject.GetComponent<MeshRenderer>().sharedMaterial = impostorMaterial;
- DestroyImmediate(atlasTexture);
+ SetRenderersEnabled(false);
}
+ }
+
+ public void DestroyExistingImpostor()
+ {
+ if (impostorObject != null) DestroyImmediate(impostorObject);
+ impostorObject = null;
+ impostorMaterial = null;
+ }
+
+ public void ToggleRenderers()
+ {
+ if (originalMesh == null) return;
+ bool showing = originalMesh.GetComponentInChildren<Renderer>()?.enabled ?? false;
+ SetRenderersEnabled(!showing);
+ }
+
+ void SetRenderersEnabled(bool enabled)
+ {
+ if (originalMesh == null) return;
+ foreach (Renderer r in originalMesh.GetComponentsInChildren<Renderer>(true))
+ r.enabled = enabled;
+ if (impostorObject != null) impostorObject.SetActive(!enabled);
+ }
}
[CustomEditor(typeof(Impostors))]
public class ImpostorsEditor : Editor
{
- public override void OnInspectorGUI()
+ public override void OnInspectorGUI()
+ {
+ DrawDefaultInspector();
+ Impostors s = (Impostors)target;
+
+ GUILayout.Space(10);
+ GUILayout.Label("Impostor Management", EditorStyles.boldLabel);
+
+ EditorGUILayout.BeginHorizontal();
+ if (GUILayout.Button("Create Impostor", GUILayout.Height(40))) s.BakeTexture();
+ GUI.enabled = s.HasImpostor;
+ if (GUILayout.Button("Destroy Impostor", GUILayout.Height(40)))
{
- DrawDefaultInspector();
-
- Impostors script = (Impostors)target;
-
- GUILayout.Space(10);
-
- if (GUILayout.Button("Create Cameras", GUILayout.Height(30)))
- {
- script.CreateCameras();
- }
-
- if (GUILayout.Button("Destroy Cameras", GUILayout.Height(30)))
- {
- script.DestroyExistingCameras();
- }
-
- GUILayout.Space(10);
-
- bool hasCameras = script.cameras != null && script.cameras.Length > 0;
-
- GUI.enabled = hasCameras;
- if (GUILayout.Button(new GUIContent("Bake Texture",
- hasCameras ? "" : "Create cameras first"), GUILayout.Height(40)))
- {
- script.BakeTexture();
- }
- GUI.enabled = true;
+ s.DestroyExistingImpostor();
+ s.DestroyExistingCameras();
}
-}
+ GUI.enabled = true;
+ EditorGUILayout.EndHorizontal();
+ if (s.originalMesh != null && GUILayout.Button("Toggle Visibility", GUILayout.Height(30)))
+ s.ToggleRenderers();
+ }
+}
diff --git a/impostor.cginc b/impostor.cginc
new file mode 100644
index 0000000..e265438
--- /dev/null
+++ b/impostor.cginc
@@ -0,0 +1,129 @@
+#ifndef __IMPOSTOR_INC
+#define __IMPOSTOR_INC
+
+#include "UnityCG.cginc"
+#include "vertex_deformation.hlsl"
+
+SamplerState bilinear_clamp_s;
+Texture2D _ImpostorAtlas;
+int _GridResolution;
+float _Cutoff;
+float3 _ImpostorMainCameraPos;
+
+#ifndef IMPOSTOR_SHADOW_PASS
+float4 _Color;
+float _DebugMode;
+#endif
+
+float2 HemiOctEncode(float3 N) {
+ N.y = max(N.y, 1e-4);
+ N = normalize(N);
+ float3 p = hemi_octahedron_to_plane(N, 0, float3(1,0,0), float3(0,1,0), 1);
+ return float2(p.x, p.z);
+}
+
+float3 HemiOctDecode(float2 uv) {
+ return normalize(plane_to_hemi_octahedron(float3(uv.x, 0, uv.y), 0, float3(1,0,0), float3(0,1,0), 1));
+}
+
+void BillboardBasis(float3 fwd, out float3 right, out float3 up) {
+ right = abs(fwd.y) > 0.999 ? float3(-1,0,0) : normalize(cross(float3(0,1,0), fwd));
+ up = cross(fwd, right);
+}
+
+int2 GetCell(float3 viewDir) {
+ float2 uv = HemiOctEncode(viewDir) * 0.5 + 0.5;
+ return clamp(int2(round(uv * (_GridResolution - 1))), 0, _GridResolution - 1);
+}
+
+float3 CellDir(int2 cell) {
+ float2 uv = float2(cell) / max(1.0, _GridResolution - 1) * 2.0 - 1.0;
+ return HemiOctDecode(uv);
+}
+
+float4 SampleAtlas(float2 uv, float2 cell) {
+ return _ImpostorAtlas.Sample(bilinear_clamp_s, (cell + uv) / _GridResolution);
+}
+
+// ============================================================================
+// Vertex/Fragment
+// ============================================================================
+
+struct appdata {
+ float4 vertex : POSITION;
+ UNITY_VERTEX_INPUT_INSTANCE_ID
+#ifdef IMPOSTOR_SHADOW_PASS
+ float3 normal : NORMAL;
+#endif
+};
+
+struct v2f {
+#ifdef IMPOSTOR_SHADOW_PASS
+ V2F_SHADOW_CASTER;
+#else
+ float4 pos : SV_POSITION;
+ UNITY_FOG_COORDS(3)
+#endif
+ float2 uv : TEXCOORD1;
+ float2 cell : TEXCOORD2;
+ UNITY_VERTEX_OUTPUT_STEREO
+};
+
+v2f vert(appdata v) {
+ v2f o;
+ UNITY_SETUP_INSTANCE_ID(v);
+ UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
+
+ float3 center = mul(unity_ObjectToWorld, float4(0,0,0,1)).xyz;
+ float3 scale = float3(
+ length(unity_ObjectToWorld._m00_m10_m20),
+ length(unity_ObjectToWorld._m01_m11_m21),
+ length(unity_ObjectToWorld._m02_m12_m22));
+
+#ifdef IMPOSTOR_SHADOW_PASS
+ float3 camPos = _ImpostorMainCameraPos;
+#else
+ float3 camPos = _WorldSpaceCameraPos;
+#endif
+
+ float3 viewOS = normalize(mul((float3x3)unity_WorldToObject, normalize(camPos - center)));
+ int2 cell = GetCell(viewOS);
+ float3 snapOS = CellDir(cell);
+ float3 snapWS = normalize(mul((float3x3)unity_ObjectToWorld, snapOS));
+
+ float3 right, up;
+ BillboardBasis(snapWS, right, up);
+ float3 worldPos = center + v.vertex.x * right * scale.x + v.vertex.y * up * scale.y;
+
+#ifdef IMPOSTOR_SHADOW_PASS
+ v.vertex = mul(unity_WorldToObject, float4(worldPos, 1));
+ v.normal = -snapWS;
+ TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
+#else
+ o.pos = mul(UNITY_MATRIX_VP, float4(worldPos, 1));
+ UNITY_TRANSFER_FOG(o, o.pos);
+#endif
+
+ float3 localOff = mul((float3x3)unity_WorldToObject, worldPos - center);
+ BillboardBasis(snapOS, right, up);
+ o.uv = float2(1 - (dot(localOff, right) + 0.5), dot(localOff, up) + 0.5);
+ o.cell = float2(cell);
+ return o;
+}
+
+float4 frag(v2f i) : SV_Target {
+ float4 col = SampleAtlas(i.uv, i.cell);
+#ifndef IMPOSTOR_SHADOW_PASS
+ if (_DebugMode > 0.5) return float4(1, 0, 0, 1);
+ col *= _Color;
+#endif
+ clip(col.a - _Cutoff);
+#ifdef IMPOSTOR_SHADOW_PASS
+ SHADOW_CASTER_FRAGMENT(i)
+#else
+ UNITY_APPLY_FOG(i.fogCoord, col);
+ return col;
+#endif
+}
+
+#endif