diff options
| author | yum <yum.food.vr@gmail.com> | 2026-01-13 19:52:38 -0800 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2026-01-13 19:57:02 -0800 |
| commit | 6df5a9a34d81d1200231b974fcc85f89e80eb63e (patch) | |
| tree | 66d6388a3f94acd6929bd23858c3fc01fe8430d6 /Scripts | |
| parent | 8c3a05445f529c10ebbf5bfdc0eb220fe95c558c (diff) | |
Impostors: implement simple grid snapped impostor particle
Diffstat (limited to 'Scripts')
| -rw-r--r-- | Scripts/Impostors.cs | 432 |
1 files changed, 204 insertions, 228 deletions
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(); + } +} |
