using UdonSharp; using UnityEngine; using VRC.SDKBase; #if UNITY_EDITOR && !COMPILER_UDONSHARP using UnityEditor; using System.IO; #endif public class InstanceGrass : UdonSharpBehaviour { [SerializeField] public GameObject prefab_; // The density of instances, in instances per meter. [SerializeField] public float density_; // The extent along each cardinal axis where instances will be rendered, in // meters. I.e. render inside a cube with edges this long. [SerializeField] public Vector3 extent_meters_; [SerializeField] public float min_distance_; [SerializeField] public Vector3 angle_randomization_; [SerializeField] public float scale_randomization_; // GPU mode settings [SerializeField] public Vector3Int max_grid_size_ = new Vector3Int(512, 1, 512); // Maximum grid dimensions for GPU mode [SerializeField] public Material blit_material_; // Auto-generated in editor, set by custom editor private Mesh mesh_; private Material instance_material_; // Extracted from prefab private Transform base_transform_; private Vector3 cell_dim_; private Vector3 inv_cell_dim_; private Vector3Int count_; private int total_instances_; private bool initialized_; // Track fields to detect runtime changes. private float density_live_; private Vector3 extent_meters_live_; private float min_distance_live_; private Vector3 angle_randomization_live_; private float scale_randomization_live_; // GPU-specific resources private RenderTexture instance_data_tex_; private Matrix4x4[] identity_transforms_; // All identity matrices private float[] instance_ids_; private MaterialPropertyBlock instance_properties_; private int tex_width_; private int tex_height_; private void Init() { // Extract components from prefab. if (prefab_ != null) { MeshFilter mesh_filter = prefab_.GetComponent(); if (mesh_filter == null && prefab_.transform.childCount > 0) { mesh_filter = prefab_.GetComponentInChildren(); } if (mesh_filter != null) { mesh_ = mesh_filter.sharedMesh; if (mesh_ == null) { mesh_ = mesh_filter.mesh; } } else { Debug.LogError("[Grass::Debug] Could not find MeshFilter on prefab or children."); } MeshRenderer mesh_renderer = prefab_.GetComponent(); if (mesh_renderer == null && prefab_.transform.childCount > 0) { mesh_renderer = prefab_.GetComponentInChildren(); } if (mesh_renderer != null) { Material[] materials = mesh_renderer.sharedMaterials; if (materials != null && materials.Length > 0) { instance_material_ = materials[0]; instance_material_.enableInstancing = true; } else { Debug.LogError("[Grass::Debug] MeshRenderer has no materials."); } } else { Debug.LogError("[Grass::Debug] Could not find MeshRenderer on prefab."); } base_transform_ = prefab_.transform; Debug.Log($"[Grass::Debug] Prefab transform - pos:{base_transform_.position}, rot:{base_transform_.rotation.eulerAngles}, scale:{base_transform_.localScale}"); } else { Debug.LogError("[Grass::Debug] prefab is null in Init()."); } density_ = Mathf.Max(1e-6f, density_); extent_meters_ = Vector3.Max(Vector3.one * 1e-6f, extent_meters_); angle_randomization_ = new Vector3( Mathf.Clamp(angle_randomization_.x, 0f, 180f), Mathf.Clamp(angle_randomization_.y, 0f, 180f), Mathf.Clamp(angle_randomization_.z, 0f, 180f)); // Use max_grid_size_ directly for GPU mode count_ = Vector3Int.Min(max_grid_size_, new Vector3Int( Mathf.Max(1, Mathf.RoundToInt(density_ * extent_meters_.x)), Mathf.Max(1, Mathf.RoundToInt(density_ * extent_meters_.y)), Mathf.Max(1, Mathf.RoundToInt(density_ * extent_meters_.z)))); cell_dim_ = new Vector3( extent_meters_.x / count_.x, extent_meters_.y / count_.y, extent_meters_.z / count_.z); inv_cell_dim_ = new Vector3( 1f / cell_dim_.x, 1f / cell_dim_.y, 1f / cell_dim_.z); total_instances_ = count_.x * count_.y * count_.z; // Calculate texture dimensions (power of 2, minimum 256x256) int min_tex_size = Mathf.CeilToInt(Mathf.Sqrt(total_instances_)); tex_width_ = Mathf.Max(256, Mathf.NextPowerOfTwo(min_tex_size)); tex_height_ = tex_width_; // Create instance data texture if (instance_data_tex_ != null) { instance_data_tex_.Release(); } instance_data_tex_ = new RenderTexture(tex_width_, tex_height_, 0, RenderTextureFormat.ARGBFloat); instance_data_tex_.filterMode = FilterMode.Point; instance_data_tex_.Create(); // Create transform array and per-instance IDs. if (identity_transforms_ == null || identity_transforms_.Length != total_instances_) { identity_transforms_ = new Matrix4x4[total_instances_]; for (int i = 0; i < total_instances_; i++) { identity_transforms_[i] = Matrix4x4.identity; } } if (instance_ids_ == null || instance_ids_.Length != total_instances_) { instance_ids_ = new float[total_instances_]; for (int i = 0; i < total_instances_; i++) { instance_ids_[i] = i; } } if (mesh_ != null) { // Prevent frustum culling from clipping instanced draws that move in-shader. mesh_.bounds = new Bounds(Vector3.zero, Vector3.one * 100000f); } density_live_ = density_; extent_meters_live_ = extent_meters_; min_distance_live_ = min_distance_; angle_randomization_live_ = angle_randomization_; scale_randomization_live_ = scale_randomization_; Debug.Log($"[Grass::Debug] Init: density={density_}, extent={extent_meters_}, count={count_}, cell_dim={cell_dim_}, instances={total_instances_}, tex={tex_width_}x{tex_height_}, scale={base_transform_.localScale}, rot={base_transform_.localRotation.eulerAngles}"); Debug.Log($"[Grass::Debug] Init details: count_.x={count_.x}, count_.y={count_.y}, count_.z={count_.z}, extent.x={extent_meters_.x}, extent.z={extent_meters_.z}"); } private bool Valid() { if (prefab_ == null) { Debug.LogError("[Grass::Debug] prefab is null."); return false; } if (mesh_ == null) { Debug.LogError("[Grass::Debug] mesh is null."); return false; } if (instance_material_ == null) { Debug.LogError("[Grass::Debug] instance_material is null."); return false; } if (blit_material_ == null) { Debug.LogError("[Grass::Debug] blit_material is null (failed to generate)."); return false; } if (instance_data_tex_ == null) { Debug.LogError("[Grass::Debug] instance_data_tex is null."); return false; } return true; } void Start() { Init(); if (!Valid()) { return; } initialized_ = true; Debug.Log($"[Grass::Debug] GPU mode initialized"); } void Update() { // Reinitialize if any config changed at runtime. if (density_ != density_live_ || extent_meters_ != extent_meters_live_ || min_distance_ != min_distance_live_ || angle_randomization_ != angle_randomization_live_ || scale_randomization_ != scale_randomization_live_) { Init(); initialized_ = false; } if (!Valid()) { return; } VRCPlayerApi lcl_player = Networking.LocalPlayer; Vector3 player_pos = lcl_player.GetPosition(); // Only update grid position on axes with extent >= 1cm // For axes with smaller extent, keep grid at 0 (don't move with player) int grid_x = extent_meters_.x >= 0.01f ? Mathf.FloorToInt(player_pos.x * inv_cell_dim_.x) : 0; int grid_y = extent_meters_.y >= 0.01f ? Mathf.FloorToInt(player_pos.y * inv_cell_dim_.y) : 0; int grid_z = extent_meters_.z >= 0.01f ? Mathf.FloorToInt(player_pos.z * inv_cell_dim_.z) : 0; int half_x = count_.x / 2; int half_y = count_.y / 2; int half_z = count_.z / 2; // Update blit material properties blit_material_.SetVector("_PlayerGridPos", new Vector3(grid_x, grid_y, grid_z)); blit_material_.SetVector("_GridCount", new Vector3(count_.x, count_.y, count_.z)); blit_material_.SetVector("_GridHalf", new Vector3(half_x, half_y, half_z)); blit_material_.SetVector("_TexDimensions", new Vector2(tex_width_, tex_height_)); // Blit to generate instance data texture VRCGraphics.Blit(null, instance_data_tex_, blit_material_); // Update instance material properties instance_material_.SetTexture("_Instance_Texture_Offset_Data_Tex", instance_data_tex_); instance_material_.SetVector("_Instance_Texture_Offset_Cell_Dimensions", cell_dim_); instance_material_.SetVector("_Instance_Texture_Offset_Angle_Randomization", angle_randomization_); instance_material_.SetFloat("_Instance_Texture_Offset_Scale_Randomization", scale_randomization_); instance_material_.SetVector("_Instance_Texture_Offset_Base_Scale", base_transform_.localScale); // Pass rotation as quaternion (x, y, z, w) Quaternion rot = base_transform_.localRotation; instance_material_.SetVector("_Instance_Texture_Offset_Base_Rotation", new Vector4(rot.x, rot.y, rot.z, rot.w)); // Distance culling parameters instance_material_.SetFloat("_Instance_Distance_Culling_Min_Distance", min_distance_); float max_distance = Mathf.Max(extent_meters_.x, Mathf.Max(extent_meters_.y, extent_meters_.z)); instance_material_.SetFloat("_Instance_Distance_Culling_Max_Distance", max_distance * 0.5f); if (Time.frameCount % 300 == 0) { Debug.Log($"[Grass::Debug] Drawing {total_instances_} GPU instances"); Debug.Log($"[Grass::Debug] GridCount={count_}, TexDim={tex_width_}x{tex_height_}, CellDim={cell_dim_}"); } // Draw instances with identity transforms - GPU handles everything VRCGraphics.DrawMeshInstanced( mesh_, 0, instance_material_, identity_transforms_, total_instances_, GetInstanceProperties(), UnityEngine.Rendering.ShadowCastingMode.Off, true, 0, null, UnityEngine.Rendering.LightProbeUsage.Off, null); initialized_ = true; } private MaterialPropertyBlock GetInstanceProperties() { if (instance_properties_ == null) { instance_properties_ = new MaterialPropertyBlock(); } instance_properties_.SetFloatArray("_Instance_ID", instance_ids_); return instance_properties_; } void OnDestroy() { if (instance_data_tex_ != null) { instance_data_tex_.Release(); } } } #if UNITY_EDITOR && !COMPILER_UDONSHARP [CustomEditor(typeof(InstanceGrass))] public class InstanceGrassEditor : Editor { public override void OnInspectorGUI() { DrawDefaultInspector(); InstanceGrass script = (InstanceGrass)target; // Auto-generate blit material if missing if (script.blit_material_ == null) { if (GUILayout.Button("Generate Blit Material")) { GenerateBlitMaterial(script); } EditorGUILayout.HelpBox("Blit material is missing. Click the button above to auto-generate it.", MessageType.Warning); } else { EditorGUILayout.HelpBox("Blit material is set. GPU instancing ready.", MessageType.Info); } } private void GenerateBlitMaterial(InstanceGrass script) { Shader blitShader = Shader.Find("yum_food/GrassGridBlit"); if (blitShader == null) { EditorUtility.DisplayDialog("Error", "Could not find shader 'yum_food/GrassGridBlit'. Make sure GrassGridBlit.shader is imported.", "OK"); return; } // Get hierarchy path Transform current = script.transform; string hierarchyPath = current.name; while (current.parent != null) { current = current.parent; hierarchyPath = current.name + "/" + hierarchyPath; } string assetPath = $"Assets/yum_food/3ner/Grass_Generated/{hierarchyPath}/Grass_generated.mat"; // Create or load existing material Material blitMat = AssetDatabase.LoadAssetAtPath(assetPath); if (blitMat == null) { // Create new material blitMat = new Material(blitShader); blitMat.name = "Grass_generated"; // Ensure directory exists string directory = Path.GetDirectoryName(assetPath); if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } AssetDatabase.CreateAsset(blitMat, assetPath); Debug.Log($"[Grass::Editor] Created blit material at {assetPath}"); } else { // Update existing material shader blitMat.shader = blitShader; EditorUtility.SetDirty(blitMat); Debug.Log($"[Grass::Editor] Updated existing blit material at {assetPath}"); } script.blit_material_ = blitMat; EditorUtility.SetDirty(script); AssetDatabase.SaveAssets(); } [UnityEditor.Callbacks.DidReloadScripts] private static void OnScriptsReloaded() { // Auto-generate blit material for all InstanceGrass components when scripts reload InstanceGrass[] allGrass = FindObjectsOfType(); foreach (var grass in allGrass) { if (grass.blit_material_ == null) { var editor = CreateEditor(grass) as InstanceGrassEditor; if (editor != null) { editor.GenerateBlitMaterial(grass); DestroyImmediate(editor); } } } } } #endif