1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
|
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<MeshFilter>();
if (mesh_filter == null && prefab_.transform.childCount > 0) {
mesh_filter = prefab_.GetComponentInChildren<MeshFilter>();
}
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<MeshRenderer>();
if (mesh_renderer == null && prefab_.transform.childCount > 0) {
mesh_renderer = prefab_.GetComponentInChildren<MeshRenderer>();
}
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<Material>(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<InstanceGrass>();
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
|