using UnityEngine; using UnityEditor; 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("Grid Settings")] [Tooltip("Number of lattice points along each axis (e.g., 5 = 5x5 = 25 points)")] [Range(2, 20)] public int gridResolution = 5; [Header("Camera Settings")] [Range(1, 4096)] public int cameraResolution = 256; [Tooltip("Distance multiplier from sphere center for camera placement")] public float cameraDistance = 2f; [HideInInspector] public GameObject[] cameraObjects; [HideInInspector] public Camera[] cameras; private float radius() { return sphere_radius_ * transform.localScale.x; } void OnDrawGizmos() { // Use transform scale directly for real-time gizmo updates float currentRadius = radius(); // Draw the bounding sphere Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(transform.position, currentRadius); // Draw the camera positions and directions if (Application.isEditor && gridResolution > 0) { 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 * cameraDistance; // Draw camera position Gizmos.DrawSphere(worldPos, currentRadius * 0.05f); // Draw line to sphere center 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; float x = u; float z = v; // 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: y = 1 - |x| - |z|, clamped to hemisphere float y = Mathf.Max(0f, 1f - Mathf.Abs(x_rot) - Mathf.Abs(z_rot)); // Normalize to unit sphere Vector3 oct_pos = new Vector3(x_rot, y, z_rot); oct_pos.Normalize(); // 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 oct_pos; } public void CreateCameras() { // Clean up existing cameras DestroyExistingCameras(); 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(); Vector3 worldPos = transform.position + hemisphereDir * currentRadius * cameraDistance; // Create camera GameObject GameObject camObj = new GameObject($"ImpostorCamera_{x}_{y}"); camObj.transform.parent = transform; camObj.transform.position = worldPos; camObj.transform.LookAt(transform.position); // Add and configure camera Camera cam = camObj.AddComponent(); cam.orthographic = true; cam.orthographicSize = currentRadius; cam.nearClipPlane = 0.01f; cam.farClipPlane = currentRadius * cameraDistance * 2f; cam.enabled = false; // Only enable during baking cameraObjects[index] = camObj; cameras[index] = cam; index++; } } Debug.Log($"Created {totalCameras} impostor cameras"); } public void DestroyExistingCameras() { // Find and destroy all children with "ImpostorCamera" in name for (int i = transform.childCount - 1; i >= 0; i--) { Transform child = transform.GetChild(i); if (child.name.StartsWith("ImpostorCamera")) { DestroyImmediate(child.gameObject); } } cameraObjects = null; cameras = null; } public void BakeTexture() { // 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); 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++; } } atlasTexture.Apply(); RenderTexture.active = null; RenderTexture.ReleaseTemporary(rt); // Save texture to file byte[] bytes = atlasTexture.EncodeToPNG(); string path = Path.Combine(Application.dataPath, "yum_food/3ner/ho_bake.png"); File.WriteAllBytes(path, bytes); Debug.Log($"Baked texture saved to: {path}"); Debug.Log($"Atlas size: {texWidth}x{texHeight} ({gridResolution}x{gridResolution} grid of {cameraResolution}x{cameraResolution} images)"); // Refresh asset database AssetDatabase.Refresh(); DestroyImmediate(atlasTexture); } } [CustomEditor(typeof(Impostors))] public class ImpostorsEditor : Editor { public override void OnInspectorGUI() { 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; } }