-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWinterCabinSetup.cs
More file actions
948 lines (871 loc) · 47.1 KB
/
Copy pathWinterCabinSetup.cs
File metadata and controls
948 lines (871 loc) · 47.1 KB
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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
using System.IO;
using ImageBlaster.Runtime;
using UnityEditor;
using UnityEditor.Animations;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace ImageBlaster.Editor
{
public static class WinterCabinSetup
{
const string SceneFolder = "Assets/ImageBlaster/Scenes";
const string ScenePath = SceneFolder + "/WinterCabin.unity";
const string CabinRoot = "Assets/ImageBlaster/winter-cabin";
const string MeshFolder = CabinRoot + "/Meshes";
const string WorldFolder = CabinRoot + "/World";
const string PanoramaPath = WorldFolder + "/world-panorama.png";
const string ColliderPath = WorldFolder + "/world-collider.glb";
const string SkyboxMaterialPath = WorldFolder + "/WinterCabinSkybox.mat";
[MenuItem("ImageBlaster/Diagnose Cabin Materials")]
public static void DiagnoseMaterials()
{
string[] paths = {
ColliderPath,
MeshFolder + "/0-plank-cabin-door.glb",
MeshFolder + "/0-low-daybed-bench.glb",
MeshFolder + "/0-cream-cushion.glb",
MeshFolder + "/0-long-wooden-console-bench.glb",
MeshFolder + "/0-cast-iron-firebowl.glb",
};
foreach (var p in paths)
{
if (!File.Exists(p)) { Debug.LogWarning("[ImageBlaster] missing: " + p); continue; }
var subs = AssetDatabase.LoadAllAssetsAtPath(p);
int matCount = 0;
foreach (var s in subs)
{
if (s is Material mat)
{
matCount++;
var shaderName = mat.shader != null ? mat.shader.name : "<null>";
Debug.Log($"[ImageBlaster] {Path.GetFileName(p)} mat={mat.name} shader={shaderName}");
}
}
if (matCount == 0)
Debug.LogWarning($"[ImageBlaster] {Path.GetFileName(p)} has no Material sub-assets");
}
}
[MenuItem("ImageBlaster/Force URP Shader On Cabin Materials")]
public static void ForceUrpShader()
{
// gltfast 6.x exposes a URP-flavored Lit/glTF shader. If older builds wrote Built-in shader,
// swap to URP equivalent. Try a few well-known shader names from gltfast.
string[] candidates = {
"Shader Graphs/glTF-pbrMetallicRoughness",
"glTF/PbrMetallicRoughness",
"Universal Render Pipeline/Lit"
};
Shader urpFallback = null;
foreach (var name in candidates)
{
var s = Shader.Find(name);
if (s != null) { urpFallback = s; Debug.Log("[ImageBlaster] picked replacement shader: " + s.name); break; }
}
if (urpFallback == null)
{
Debug.LogError("[ImageBlaster] no replacement shader found; gltfast/URP shaders missing from project.");
return;
}
string[] paths = {
ColliderPath,
MeshFolder + "/0-plank-cabin-door.glb",
MeshFolder + "/0-low-daybed-bench.glb",
MeshFolder + "/0-cream-cushion.glb",
MeshFolder + "/0-long-wooden-console-bench.glb",
MeshFolder + "/0-cast-iron-firebowl.glb",
};
int fixedCount = 0;
foreach (var p in paths)
{
if (!File.Exists(p)) continue;
var subs = AssetDatabase.LoadAllAssetsAtPath(p);
foreach (var s in subs)
{
if (s is Material mat)
{
var current = mat.shader != null ? mat.shader.name : "<null>";
if (current == "Hidden/InternalErrorShader" || current.StartsWith("Standard") ||
current == "<null>" || current.StartsWith("glTF/"))
{
// Preserve main texture if present
Texture baseTex = mat.HasProperty("_MainTex") ? mat.GetTexture("_MainTex") : null;
if (baseTex == null && mat.HasProperty("baseColorTexture"))
baseTex = mat.GetTexture("baseColorTexture");
mat.shader = urpFallback;
if (baseTex != null && mat.HasProperty("_BaseMap"))
mat.SetTexture("_BaseMap", baseTex);
EditorUtility.SetDirty(mat);
fixedCount++;
Debug.Log($"[ImageBlaster] swapped shader on {Path.GetFileName(p)} :: {mat.name} ({current} → {urpFallback.name})");
}
}
}
}
AssetDatabase.SaveAssets();
Debug.Log($"[ImageBlaster] fixed {fixedCount} materials.");
}
[MenuItem("ImageBlaster/Reimport Cabin GLBs (fix magenta materials)")]
public static void ReimportGlbs()
{
string[] paths = {
ColliderPath,
MeshFolder + "/0-plank-cabin-door.glb",
MeshFolder + "/0-low-daybed-bench.glb",
MeshFolder + "/0-cream-cushion.glb",
MeshFolder + "/0-long-wooden-console-bench.glb",
MeshFolder + "/0-cast-iron-firebowl.glb",
};
AssetDatabase.StartAssetEditing();
try
{
foreach (var p in paths)
{
if (File.Exists(p))
AssetDatabase.ImportAsset(p, ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate);
}
}
finally
{
AssetDatabase.StopAssetEditing();
AssetDatabase.Refresh();
}
Debug.Log("[ImageBlaster] Reimported cabin GLBs. Re-run Setup Winter Cabin Scene to rebuild.");
}
[MenuItem("ImageBlaster/Setup Winter Cabin Scene")]
public static void Setup()
{
if (!Directory.Exists(SceneFolder))
Directory.CreateDirectory(SceneFolder);
// Bump panorama Max Size so 4608x2304 stays sharp.
BumpTextureMaxSize(PanoramaPath, 8192);
var skybox = EnsureSkybox();
var scene = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
RenderSettings.skybox = skybox;
DynamicGI.UpdateEnvironment();
// Bright ambient + a soft fill light so Hunyuan/Meshy meshes don't render with black
// shaded faces — the default scene light from NewScene is too dim for these props.
RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Trilight;
RenderSettings.ambientSkyColor = new Color(0.85f, 0.9f, 1f);
RenderSettings.ambientEquatorColor = new Color(0.7f, 0.7f, 0.7f);
RenderSettings.ambientGroundColor = new Color(0.3f, 0.25f, 0.2f);
RenderSettings.ambientIntensity = 1.2f;
var existingLight = GameObject.Find("Directional Light");
if (existingLight != null)
{
var l = existingLight.GetComponent<Light>();
if (l != null)
{
l.intensity = 1.5f;
l.shadows = LightShadows.Soft;
l.transform.rotation = Quaternion.Euler(45f, 30f, 0f);
}
}
var cabinRoot = new GameObject("WinterCabin");
// AutoKickOnStart removed by default — it was useful for the first smoke test but
// tends to launch props into the ceiling box where they get stuck (especially the
// firebowl whose tight box collider doesn't match its shallow round shape).
// Re-enable manually if you want to see physics fire at Play start.
// cabinRoot.AddComponent<AutoKickOnStart>();
var cabinBounds = BuildBoxColliders(cabinRoot.transform);
var splatPlaced = PlaceGaussianSplat(cabinRoot.transform, cabinBounds);
BuildReflectionProbe(cabinRoot.transform, cabinBounds);
if (splatPlaced)
{
// Hide the low-poly visual mesh — splat is the visual now
var visual = cabinRoot.transform.Find("WorldVisual");
if (visual != null)
{
foreach (var r in visual.GetComponentsInChildren<MeshRenderer>())
r.enabled = false;
}
}
PlaceMeshes(cabinRoot.transform, cabinBounds);
// Two characters in the cabin: zombie center-back, skeleton off to the right.
// Both go through SpawnRiggedCharacter so any tweak (lighting, materials, animator) lands on both.
SpawnRiggedCharacter(cabinRoot.transform, cabinBounds, new CharacterSpec
{
id = "Zombie",
walkingGlbPath = CabinRoot + "/Characters/zombie-walking.glb",
runningGlbPath = CabinRoot + "/Characters/zombie-running.glb",
animatorPath = CabinRoot + "/Characters/ZombieAnimator.controller",
positionOffset = new Vector3(-0.8f, 0f, 0.4f),
yawDegrees = 180f,
});
SpawnRiggedCharacter(cabinRoot.transform, cabinBounds, new CharacterSpec
{
id = "SkeletonWarrior",
walkingGlbPath = CabinRoot + "/Characters/skeleton-walking.glb",
runningGlbPath = CabinRoot + "/Characters/skeleton-running.glb",
animatorPath = CabinRoot + "/Characters/SkeletonAnimator.controller",
positionOffset = new Vector3(0.8f, 0f, 0.4f),
yawDegrees = 200f,
});
BuildPlayer(cabinBounds);
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene, ScenePath);
EditorSceneManager.OpenScene(ScenePath);
Debug.Log("[ImageBlaster] Winter Cabin scene saved to " + ScenePath +
" — press Play; WASD + mouse, LMB to kick props.");
}
static void BumpTextureMaxSize(string path, int maxSize)
{
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer == null) return;
if (importer.maxTextureSize >= maxSize) return;
importer.maxTextureSize = maxSize;
importer.SaveAndReimport();
}
const string CabinVisualMaterialPath = WorldFolder + "/CabinVisualMaterial.mat";
// Build (or reuse) a plain URP Lit material to drop onto the world-collider mesh.
// The collider GLB has no UVs so we can't sample the panorama; flat white-ish is good
// enough to keep parallax working without showing magenta.
static Material EnsureCabinVisualMaterial()
{
var mat = AssetDatabase.LoadAssetAtPath<Material>(CabinVisualMaterialPath);
if (mat != null) return mat;
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
if (shader == null)
{
Debug.LogWarning("[ImageBlaster] Could not find URP Lit shader; world-collider may render magenta.");
return null;
}
mat = new Material(shader);
mat.color = new Color(0.82f, 0.74f, 0.62f); // warm wood-ish tone to blend with the panorama
AssetDatabase.CreateAsset(mat, CabinVisualMaterialPath);
return mat;
}
static Material EnsureSkybox()
{
var mat = AssetDatabase.LoadAssetAtPath<Material>(SkyboxMaterialPath);
if (mat == null)
{
var shader = Shader.Find("Skybox/Panoramic");
if (shader == null)
{
Debug.LogError("[ImageBlaster] Skybox/Panoramic shader not found.");
return null;
}
mat = new Material(shader);
AssetDatabase.CreateAsset(mat, SkyboxMaterialPath);
}
var tex = AssetDatabase.LoadAssetAtPath<Texture>(PanoramaPath);
if (tex == null)
{
Debug.LogWarning("[ImageBlaster] Panorama texture not found at " + PanoramaPath);
}
else
{
mat.SetTexture("_MainTex", tex);
mat.SetFloat("_Mapping", 1); // Latitude longitude
mat.SetFloat("_ImageType", 0); // 360 degrees
mat.SetFloat("_MirrorOnBack", 0);
EditorUtility.SetDirty(mat);
}
return mat;
}
// Build 6 BoxColliders (floor + ceiling + 4 walls) from the world-collider GLB's bbox.
// The Marble collider mesh is high-poly, has no UV/normals (Unity light-baker complains,
// and material renders magenta). Throwing away the mesh and using boxes is the simplest fix.
static Bounds BuildBoxColliders(Transform parent)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(ColliderPath);
if (prefab == null)
{
Debug.LogWarning("[ImageBlaster] Collider GLB not yet imported at " + ColliderPath +
" — wait for glTFast import to finish, then re-run setup.");
return new Bounds(Vector3.zero, new Vector3(6, 3, 6));
}
// Instantiate the world-collider mesh AND KEEP it as the visual (parallax is broken
// if we only have a panorama skybox — the player walks but the world doesn't move
// relative to them). The Marble collider mesh has no UV/normals so we have to:
// 1. swap its material to something URP-friendly so it doesn't render magenta
// 2. NOT attach a MeshCollider (its high-poly geometry triggers convex-hull warnings;
// physics goes on the 6 BoxColliders we build below from the bbox)
var visual = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
visual.name = "WorldVisual";
visual.transform.localPosition = Vector3.zero;
// glTFast on Unity 6 occasionally imports Marble's right-handed collider mesh upside-down
// relative to our scene up-axis. Quick fix: 180° around Z so floor is below, ceiling above.
// (If your render comes out correct without this, set the flag false in the const below.)
const bool FlipWorldMeshUpsideDown = true;
if (FlipWorldMeshUpsideDown)
visual.transform.localRotation = Quaternion.Euler(0f, 0f, 180f);
var renderers = visual.GetComponentsInChildren<MeshRenderer>();
var bounds = new Bounds();
bool first = true;
var fallbackMat = EnsureCabinVisualMaterial();
foreach (var r in renderers)
{
if (first) { bounds = r.bounds; first = false; }
else bounds.Encapsulate(r.bounds);
if (fallbackMat != null) r.sharedMaterial = fallbackMat;
var existingCol = r.GetComponent<MeshCollider>();
if (existingCol != null) Object.DestroyImmediate(existingCol);
// The Marble collider mesh ships without normals/tangents. Without normals, URP Lit
// gets a (0,0,0) surface direction and the shader returns black or unstable shading.
// Clone the mesh (so we don't mutate the imported asset) and regenerate.
var mf = r.GetComponent<MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
var src = mf.sharedMesh;
var clone = Object.Instantiate(src);
clone.name = src.name + "_recalc";
if (clone.normals == null || clone.normals.Length == 0)
clone.RecalculateNormals();
clone.RecalculateTangents();
clone.RecalculateBounds();
mf.sharedMesh = clone;
}
}
if (first)
{
Debug.LogWarning("[ImageBlaster] world-collider GLB has no MeshRenderers; using fallback bbox.");
bounds = new Bounds(Vector3.zero, new Vector3(6, 3, 6));
}
Debug.Log($"[ImageBlaster] cabin bbox center={bounds.center} size={bounds.size}");
var shell = new GameObject("CabinShell");
shell.transform.SetParent(parent, false);
shell.isStatic = true;
const float wall = 0.15f;
var s = bounds.size;
var c = bounds.center;
void AddBox(string name, Vector3 center, Vector3 size)
{
var box = new GameObject(name);
box.transform.SetParent(shell.transform, false);
box.transform.localPosition = center;
box.isStatic = true;
var bc = box.AddComponent<BoxCollider>();
bc.size = size;
}
// Floor
AddBox("Floor", new Vector3(c.x, bounds.min.y - wall / 2, c.z), new Vector3(s.x + 2, wall, s.z + 2));
// Ceiling
AddBox("Ceiling", new Vector3(c.x, bounds.max.y + wall / 2, c.z), new Vector3(s.x + 2, wall, s.z + 2));
// Walls
AddBox("WallN", new Vector3(c.x, c.y, bounds.max.z + wall / 2), new Vector3(s.x + 2, s.y + 1, wall));
AddBox("WallS", new Vector3(c.x, c.y, bounds.min.z - wall / 2), new Vector3(s.x + 2, s.y + 1, wall));
AddBox("WallE", new Vector3(bounds.max.x + wall / 2, c.y, c.z), new Vector3(wall, s.y + 1, s.z + 2));
AddBox("WallW", new Vector3(bounds.min.x - wall / 2, c.y, c.z), new Vector3(wall, s.y + 1, s.z + 2));
return bounds;
}
static void SetStaticRecursive(GameObject go)
{
go.isStatic = true;
foreach (Transform t in go.transform)
SetStaticRecursive(t.gameObject);
}
struct MeshSpec
{
public string file;
public string name;
public Vector3 pos;
public Vector3 euler;
public float mass;
}
static readonly MeshSpec[] Meshes = new MeshSpec[]
{
new MeshSpec{ file = "0-plank-cabin-door.glb", name = "PlankCabinDoor", pos = new Vector3(-2.4f, 0.0f, -1.5f), euler = new Vector3(0, 90, 0), mass = 30f },
new MeshSpec{ file = "0-low-daybed-bench.glb", name = "LowDaybedBench", pos = new Vector3( 0.0f, 0.0f, -1.6f), euler = new Vector3(0, 0, 0), mass = 25f },
new MeshSpec{ file = "0-cream-cushion.glb", name = "CreamCushion", pos = new Vector3(-0.6f, 0.6f, -1.6f), euler = new Vector3(0, 0, 0), mass = 2f },
new MeshSpec{ file = "0-long-wooden-console-bench.glb", name = "LongWoodenConsoleBench", pos = new Vector3( 0.0f, 0.0f, -0.2f), euler = new Vector3(0, 0, 0), mass = 25f },
new MeshSpec{ file = "0-cast-iron-firebowl.glb", name = "CastIronFirebowl", pos = new Vector3( 1.4f, 0.0f, 0.4f), euler = new Vector3(0, 0, 0), mass = 15f },
};
// Reflection probe in the cabin center. Without this, SkinnedMeshRenderers (the zombie)
// can sample an empty reflection slot and render black on the faces where ambient is low —
// visually shows up as the zombie occasionally going partially black as the camera moves.
// Realtime mode keeps it cheap; one capture per Play start is enough since the room is static.
static void BuildReflectionProbe(Transform parent, Bounds cabin)
{
var go = new GameObject("CabinReflectionProbe");
go.transform.SetParent(parent, false);
go.transform.localPosition = new Vector3(cabin.center.x, cabin.center.y, cabin.center.z);
var probe = go.AddComponent<ReflectionProbe>();
probe.mode = UnityEngine.Rendering.ReflectionProbeMode.Realtime;
probe.refreshMode = UnityEngine.Rendering.ReflectionProbeRefreshMode.OnAwake;
probe.timeSlicingMode = UnityEngine.Rendering.ReflectionProbeTimeSlicingMode.NoTimeSlicing;
probe.size = new Vector3(cabin.size.x + 4f, cabin.size.y + 4f, cabin.size.z + 4f);
probe.boxProjection = true;
probe.resolution = 128;
probe.intensity = 1f;
probe.clearFlags = UnityEngine.Rendering.ReflectionProbeClearFlags.Skybox;
}
static void PlaceMeshes(Transform parent, Bounds cabin)
{
// MeshSpec positions are relative to cabin center, on the floor.
var floor = cabin.min.y;
foreach (var spec in Meshes)
{
var path = MeshFolder + "/" + spec.file;
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab == null)
{
Debug.LogWarning("[ImageBlaster] Mesh not yet imported: " + path);
continue;
}
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
go.name = spec.name;
go.transform.localPosition = new Vector3(
cabin.center.x + spec.pos.x,
floor + spec.pos.y + 0.5f, // give them a small drop so they settle on the floor box
cabin.center.z + spec.pos.z
);
go.transform.localRotation = Quaternion.Euler(spec.euler);
var rb = go.AddComponent<Rigidbody>();
rb.mass = spec.mass;
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
rb.interpolation = RigidbodyInterpolation.Interpolate;
// Same shadow-acne diagnostic on props
foreach (var pr in go.GetComponentsInChildren<MeshRenderer>())
{
pr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
pr.receiveShadows = false;
}
// Replace MeshCollider with a single fitted BoxCollider in the GO's LOCAL frame.
// (Earlier version mixed world-bbox center with world-bbox size: when go.transform was
// rotated, the world-bbox axes weren't aligned with BoxCollider axes — the box ended up
// oversized and offset, so physics moved the GO with a phantom drift.)
var localBounds = new Bounds();
bool first = true;
foreach (var mf in go.GetComponentsInChildren<MeshFilter>())
{
if (mf.sharedMesh == null) continue;
foreach (var corner in BoundsCorners(mf.sharedMesh.bounds))
{
// mesh-local corner → world → go-local
var localCorner = go.transform.InverseTransformPoint(mf.transform.TransformPoint(corner));
if (first) { localBounds = new Bounds(localCorner, Vector3.zero); first = false; }
else localBounds.Encapsulate(localCorner);
}
}
if (!first)
{
var col = go.AddComponent<BoxCollider>();
col.center = localBounds.center;
col.size = localBounds.size;
}
}
}
static Vector3[] BoundsCorners(Bounds b)
{
var c = b.center; var e = b.extents;
return new Vector3[] {
c + new Vector3(-e.x, -e.y, -e.z), c + new Vector3( e.x, -e.y, -e.z),
c + new Vector3(-e.x, e.y, -e.z), c + new Vector3( e.x, e.y, -e.z),
c + new Vector3(-e.x, -e.y, e.z), c + new Vector3( e.x, -e.y, e.z),
c + new Vector3(-e.x, e.y, e.z), c + new Vector3( e.x, e.y, e.z),
};
}
const string SplatAssetFolder = WorldFolder + "/SplatAsset";
// Look for any GaussianSplatAsset generated via Tools > Gaussian Splats > Create GaussianSplatAsset
// and spawn a renderer for it. If the package isn't installed or the user hasn't run the asset-creator
// yet, this is a no-op and the low-poly collider mesh stays as the visual.
// Returns true if a splat was actually placed.
static bool PlaceGaussianSplat(Transform parent, Bounds cabin)
{
// Aras's renderer type lives in this namespace; look it up by reflection so this file
// compiles even when the package isn't installed.
// Aras's assembly is just "GaussianSplatting" (asmdef name), not "GaussianSplatting.Runtime"
var rendererType = System.Type.GetType("GaussianSplatting.Runtime.GaussianSplatRenderer, GaussianSplatting");
var assetType = System.Type.GetType("GaussianSplatting.Runtime.GaussianSplatAsset, GaussianSplatting");
if (rendererType == null || assetType == null)
{
Debug.Log("[ImageBlaster] GaussianSplatting package not available yet — using collider mesh as visual. " +
"Install com.aras-p.gaussian-splatting and run Tools > Gaussian Splats > Create GaussianSplatAsset.");
return false;
}
// Find a GaussianSplatAsset .asset somewhere under the world folder
UnityEngine.Object foundAsset = null;
string foundPath = null;
var guids = AssetDatabase.FindAssets("t:Object", new[] { WorldFolder });
foreach (var guid in guids)
{
var p = AssetDatabase.GUIDToAssetPath(guid);
if (!p.EndsWith(".asset")) continue;
var asset = AssetDatabase.LoadAssetAtPath(p, assetType);
if (asset != null) { foundAsset = asset; foundPath = p; break; }
}
if (foundAsset == null)
{
Debug.Log("[ImageBlaster] No GaussianSplatAsset found under " + WorldFolder +
" — run Tools > Gaussian Splats > Create GaussianSplatAsset, pick world-500k.spz, save to " +
SplatAssetFolder + "/");
return false;
}
var go = new GameObject("WorldSplat");
go.transform.SetParent(parent, false);
// Marble splats — flip and metric-scale, mirroring what we do in the web demo.
// Splat ground at physics floor: cabin.min.y + ground_plane_offset (same formula as web).
go.transform.localPosition = new Vector3(0f, cabin.min.y + 1.1290963f, 0f);
go.transform.localRotation = Quaternion.Euler(180f, 0f, 0f);
go.transform.localScale = Vector3.one * 1.4973879f;
Debug.Log($"[ImageBlaster] WorldSplat localY = {go.transform.localPosition.y:F2} (cabin floor {cabin.min.y:F2} + offset 1.13)");
var rend = go.AddComponent(rendererType);
var prop = rendererType.GetProperty("m_Asset");
if (prop != null) prop.SetValue(rend, foundAsset);
else
{
// Fallback: try public field name 'm_Asset' or 'asset' via reflection
var field = rendererType.GetField("m_Asset", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (field != null) field.SetValue(rend, foundAsset);
}
// URP requires GaussianSplatURPFeature added to the renderer asset, otherwise the
// splat draw pass is never scheduled. Auto-add if missing.
EnsureUrpFeature();
// Unity 6 URP defaults to Render-Graph-disabled "compatibility mode". Aras's pass only
// implements RecordRenderGraph (not Execute), so it gets skipped in compatibility mode.
// Force RenderGraph on so the splat actually renders.
EnsureRenderGraphEnabled();
// MSAA + HDR in the URP asset cause jagged black pixels at splat ↔ mesh boundaries.
// The composite blit doesn't resolve MSAA properly, and HDR can clamp negative splat
// samples to black. Disable both on every URP asset in the project.
DisableMsaaAndHdr();
Debug.Log("[ImageBlaster] Wired GaussianSplatRenderer to " + foundPath);
return true;
}
[MenuItem("ImageBlaster/Enable RenderGraph (disable Compatibility Mode)")]
public static void EnableRenderGraphMenu() => EnsureRenderGraphEnabled();
// Walk every project-local UniversalRenderPipelineAsset and turn off MSAA + HDR.
// (Gaussian splat composite + MSAA → jagged black edges; HDR can clamp NaN samples to black.)
static void DisableMsaaAndHdr()
{
var guids = AssetDatabase.FindAssets("t:UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset");
foreach (var guid in guids)
{
var p = AssetDatabase.GUIDToAssetPath(guid);
if (p.StartsWith("Packages/")) continue;
var asset = AssetDatabase.LoadMainAssetAtPath(p);
if (asset == null) continue;
var t = asset.GetType();
bool changed = false;
// msaaSampleCount is a property on UniversalRenderPipelineAsset; 1 = disabled
var msaaProp = t.GetProperty("msaaSampleCount", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (msaaProp != null && msaaProp.CanWrite)
{
int current = (int)msaaProp.GetValue(asset);
if (current != 1)
{
msaaProp.SetValue(asset, 1);
changed = true;
}
}
var hdrProp = t.GetProperty("supportsHDR", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (hdrProp != null && hdrProp.CanWrite)
{
bool current = (bool)hdrProp.GetValue(asset);
if (current)
{
hdrProp.SetValue(asset, false);
changed = true;
}
}
if (changed)
{
EditorUtility.SetDirty(asset);
Debug.Log($"[ImageBlaster] Disabled MSAA/HDR on URP asset {System.IO.Path.GetFileName(p)}");
}
}
AssetDatabase.SaveAssets();
}
// Flips the global URP setting so RenderGraph-based passes (like Aras's splat) actually run.
// Reflection lookup so we don't have to hard-code the namespace which moved between URP 14/15/17.
static void EnsureRenderGraphEnabled()
{
// Try the few namespaces this lives in across Unity versions
string[] candidateTypeNames = {
"UnityEngine.Rendering.RenderGraphSettings, Unity.RenderPipelines.Core.Runtime",
"UnityEngine.Rendering.RenderGraphSettings, UnityEngine.CoreModule",
"UnityEngine.Rendering.RenderGraphModule.RenderGraphSettings, Unity.RenderPipelines.Core.Runtime",
"UnityEngine.Rendering.Universal.RenderGraphSettings, Unity.RenderPipelines.Universal.Runtime",
};
System.Type t = null;
foreach (var n in candidateTypeNames)
{
t = System.Type.GetType(n);
if (t != null) break;
}
if (t == null)
{
Debug.LogWarning("[ImageBlaster] RenderGraphSettings type not found — toggle compatibility mode manually: Edit > Project Settings > Graphics > Render Graph.");
return;
}
// GraphicsSettings.GetRenderPipelineSettings<T>() is generic — call via reflection
var graphicsSettingsType = typeof(UnityEngine.Rendering.GraphicsSettings);
var getMethod = graphicsSettingsType.GetMethod("GetRenderPipelineSettings", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
if (getMethod == null)
{
Debug.LogWarning("[ImageBlaster] GraphicsSettings.GetRenderPipelineSettings not available; can't toggle RenderGraph from script.");
return;
}
var genericGet = getMethod.MakeGenericMethod(t);
var settingsObj = genericGet.Invoke(null, null);
if (settingsObj == null)
{
Debug.LogWarning($"[ImageBlaster] No active settings of type {t.Name} found.");
return;
}
// Find the bool property: enableRenderCompatibilityMode (we want to set false).
// Old API used 'enableRenderGraph' bool (set true). Try both.
var compatProp = t.GetProperty("enableRenderCompatibilityMode", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (compatProp != null && compatProp.PropertyType == typeof(bool))
{
bool current = (bool)compatProp.GetValue(settingsObj);
if (current)
{
compatProp.SetValue(settingsObj, false);
Debug.Log("[ImageBlaster] Disabled URP compatibility mode → RenderGraph active. Splat passes will now run.");
}
else
{
Debug.Log("[ImageBlaster] URP compatibility mode already disabled (RenderGraph already active).");
}
return;
}
var enableProp = t.GetProperty("enableRenderGraph", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (enableProp != null && enableProp.PropertyType == typeof(bool))
{
bool current = (bool)enableProp.GetValue(settingsObj);
if (!current)
{
enableProp.SetValue(settingsObj, true);
Debug.Log("[ImageBlaster] Enabled RenderGraph in URP settings.");
}
else
{
Debug.Log("[ImageBlaster] RenderGraph already enabled.");
}
return;
}
Debug.LogWarning("[ImageBlaster] RenderGraphSettings found but no expected bool field — toggle manually in Edit > Project Settings > Graphics > Render Graph.");
}
// Find the active URP RendererData asset and append a GaussianSplatURPFeature if not already present.
static void EnsureUrpFeature()
{
var featureType = System.Type.GetType("GaussianSplatting.Runtime.GaussianSplatURPFeature, GaussianSplatting");
if (featureType == null)
{
Debug.LogWarning("[ImageBlaster] GaussianSplatURPFeature not available — splat may not render in URP.");
return;
}
// Locate all UniversalRendererData assets in the project
var guids = AssetDatabase.FindAssets("t:UnityEngine.Rendering.Universal.UniversalRendererData");
if (guids.Length == 0)
{
Debug.LogWarning("[ImageBlaster] No UniversalRendererData found — make sure URP is the active render pipeline.");
return;
}
foreach (var guid in guids)
{
var p = AssetDatabase.GUIDToAssetPath(guid);
// Skip immutable URP defaults inside the package — only modify project-local renderers
if (p.StartsWith("Packages/")) continue;
var rendererData = AssetDatabase.LoadMainAssetAtPath(p) as ScriptableObject;
if (rendererData == null) continue;
// Use reflection to access rendererFeatures list (it's public on UniversalRendererData)
var dataType = rendererData.GetType();
var featuresField = dataType.GetField("m_RendererFeatures", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public)
?? dataType.GetField("rendererFeatures", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
if (featuresField == null) continue;
var features = featuresField.GetValue(rendererData) as System.Collections.IList;
if (features == null) continue;
bool already = false;
foreach (var f in features)
{
if (f != null && featureType.IsAssignableFrom(f.GetType())) { already = true; break; }
}
if (already)
{
Debug.Log($"[ImageBlaster] URP renderer {System.IO.Path.GetFileName(p)} already has GaussianSplatURPFeature");
continue;
}
var feature = ScriptableObject.CreateInstance(featureType);
feature.name = "GaussianSplatURPFeature";
AssetDatabase.AddObjectToAsset(feature, rendererData);
features.Add(feature);
EditorUtility.SetDirty(rendererData);
Debug.Log($"[ImageBlaster] Added GaussianSplatURPFeature to URP renderer {System.IO.Path.GetFileName(p)}");
}
AssetDatabase.SaveAssets();
}
struct CharacterSpec
{
public string id; // GameObject name in the scene + log tag
public string walkingGlbPath; // GLB with skinned mesh + walk clip
public string runningGlbPath; // GLB with run clip (mesh re-used, only clip read)
public string animatorPath; // .controller asset path (one per character — different rigs don't share)
public Vector3 positionOffset;// relative to (cabin.center.x, cabin.min.y, cabin.center.z)
public float yawDegrees;
}
// Drop a Meshy auto-rigged + auto-animated humanoid in the scene. Generic so any number of
// characters spawn from the same code path. Each rig gets its own AnimatorController
// because Meshy bone naming varies between characters and can't be shared across rigs.
static void SpawnRiggedCharacter(Transform parent, Bounds cabin, CharacterSpec spec)
{
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(spec.walkingGlbPath);
if (prefab == null)
{
Debug.LogWarning($"[ImageBlaster] {spec.id}: GLB not yet imported at " + spec.walkingGlbPath);
return;
}
AnimationClip walkClip = FindFirstClip(spec.walkingGlbPath);
AnimationClip runClip = FindFirstClip(spec.runningGlbPath);
if (walkClip == null) Debug.LogWarning($"[ImageBlaster] {spec.id}: no walk clip in " + spec.walkingGlbPath);
if (runClip == null) Debug.LogWarning($"[ImageBlaster] {spec.id}: no run clip in " + spec.runningGlbPath);
var controller = EnsureWalkRunController(spec.animatorPath, walkClip, runClip);
var go = (GameObject)PrefabUtility.InstantiatePrefab(prefab, parent);
go.name = spec.id;
// Render-stability flags fixed during testing — see DEVELOPMENT.md "compatibility notes"
foreach (var smr in go.GetComponentsInChildren<SkinnedMeshRenderer>())
{
smr.updateWhenOffscreen = true;
smr.allowOcclusionWhenDynamic = false;
smr.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Simple;
smr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.BlendProbes;
smr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
smr.receiveShadows = false;
var mats = smr.sharedMaterials;
for (int i = 0; i < mats.Length; i++)
{
if (mats[i] == null) continue;
SetMaterialDoubleSided(mats[i]);
}
}
// Measure → scale to 1.7m tall → re-measure → plant feet on floor
var rbounds = new Bounds();
bool first = true;
foreach (var r in go.GetComponentsInChildren<Renderer>())
{
if (first) { rbounds = r.bounds; first = false; }
else rbounds.Encapsulate(r.bounds);
}
float currentHeight = first ? 1.7f : rbounds.size.y;
float scale = 1.7f / Mathf.Max(currentHeight, 0.001f);
go.transform.localScale = Vector3.one * scale;
rbounds = new Bounds();
first = true;
foreach (var r in go.GetComponentsInChildren<Renderer>())
{
if (first) { rbounds = r.bounds; first = false; }
else rbounds.Encapsulate(r.bounds);
}
float bottomY = first ? 0f : rbounds.min.y;
go.transform.localPosition = new Vector3(
cabin.center.x + spec.positionOffset.x,
cabin.min.y + spec.positionOffset.y - bottomY + go.transform.localPosition.y,
cabin.center.z + spec.positionOffset.z
);
go.transform.localRotation = Quaternion.Euler(0f, spec.yawDegrees, 0f);
Debug.Log($"[ImageBlaster] {spec.id} placed at {go.transform.position} height={rbounds.size.y:F2}m feet={rbounds.min.y:F2}");
var animator = go.GetComponent<Animator>();
if (animator == null) animator = go.AddComponent<Animator>();
animator.runtimeAnimatorController = controller;
animator.applyRootMotion = false;
var ai = go.GetComponent<ZombieAI>();
if (ai == null) ai = go.AddComponent<ZombieAI>();
}
// URP Lit + most glTFast shaders expose a "_Cull" or "_CullMode" int (0=Off, 1=Front, 2=Back).
// Some also have a "_DoubleSided" toggle. Set whichever the shader exposes so back faces draw.
static void SetMaterialDoubleSided(Material m)
{
if (m.HasProperty("_Cull")) m.SetFloat("_Cull", 0f);
if (m.HasProperty("_CullMode")) m.SetFloat("_CullMode", 0f);
if (m.HasProperty("_DoubleSided")) m.SetFloat("_DoubleSided", 1f);
if (m.HasProperty("_DoubleSidedEnable")) m.SetFloat("_DoubleSidedEnable", 1f);
EditorUtility.SetDirty(m);
}
static AnimationClip FindFirstClip(string assetPath)
{
foreach (var sub in AssetDatabase.LoadAllAssetsAtPath(assetPath))
{
if (sub is AnimationClip clip && !clip.name.StartsWith("__"))
return clip;
}
return null;
}
// Build an AnimatorController with two motion states (Walk + Run) and a Speed float param.
// The transitions: Walk ↔ Run gated by Speed >= 1.5 (run) vs Speed < 1.5 (walk).
// ZombieAI.cs flips Speed between 1 and 2 based on distance to the player.
static AnimatorController EnsureWalkRunController(string path, AnimationClip walkClip, AnimationClip runClip)
{
var controller = AssetDatabase.LoadAssetAtPath<AnimatorController>(path);
if (controller == null)
controller = AnimatorController.CreateAnimatorControllerAtPath(path);
// Reset
var rootLayer = controller.layers[0];
var rootSm = rootLayer.stateMachine;
for (int i = rootSm.states.Length - 1; i >= 0; i--)
rootSm.RemoveState(rootSm.states[i].state);
// Drop existing parameters to keep things clean
for (int i = controller.parameters.Length - 1; i >= 0; i--)
controller.RemoveParameter(i);
controller.AddParameter("Speed", AnimatorControllerParameterType.Float);
AnimatorState walk = null;
AnimatorState run = null;
if (walkClip != null)
{
walk = rootSm.AddState("Walk");
walk.motion = walkClip;
rootSm.defaultState = walk;
}
if (runClip != null)
{
run = rootSm.AddState("Run");
run.motion = runClip;
if (walk == null) rootSm.defaultState = run;
}
if (walk != null && run != null)
{
var wToR = walk.AddTransition(run);
wToR.hasExitTime = false;
wToR.duration = 0.15f;
wToR.AddCondition(AnimatorConditionMode.Greater, 1.5f, "Speed");
var rToW = run.AddTransition(walk);
rToW.hasExitTime = false;
rToW.duration = 0.15f;
rToW.AddCondition(AnimatorConditionMode.Less, 1.5f, "Speed");
}
EditorUtility.SetDirty(controller);
AssetDatabase.SaveAssets();
return controller;
}
static void BuildPlayer(Bounds cabin)
{
var player = new GameObject("Player");
// Spawn safely inside the bbox, just above the floor box
player.transform.position = new Vector3(
cabin.center.x,
cabin.min.y + 0.5f, // a tiny bit above floor so CharacterController doesn't intersect floor box
cabin.center.z
);
var cc = player.AddComponent<CharacterController>();
cc.height = 1.8f;
cc.radius = 0.35f;
cc.center = new Vector3(0f, 0.9f, 0f);
var camGo = new GameObject("PlayerCamera");
camGo.transform.SetParent(player.transform, false);
camGo.transform.localPosition = new Vector3(0f, 1.65f, 0f);
var cam = camGo.AddComponent<Camera>();
cam.tag = "MainCamera";
cam.clearFlags = CameraClearFlags.Skybox;
// 0.3 instead of 0.1 — Gaussian splats produce floating line artifacts when near plane is too tight
cam.nearClipPlane = 0.3f;
cam.farClipPlane = 200f;
var urpCamData = camGo.AddComponent<UniversalAdditionalCameraData>();
// SMAA + Gaussian splats = jagged black artifacts on splat ↔ opaque mesh edges where the
// composite shader and AA pass disagree on sub-pixel coverage. Disable both AA and post
// processing for now — splats already produce a soft photographic look on their own.
urpCamData.antialiasing = UnityEngine.Rendering.Universal.AntialiasingMode.None;
urpCamData.renderPostProcessing = false;
urpCamData.allowHDROutput = false;
camGo.AddComponent<AudioListener>();
var fps = player.AddComponent<SimpleFpsController>();
fps.pitchTarget = camGo.transform;
var kick = player.AddComponent<KickInteraction>();
kick.kickCamera = cam;
// Remove the default Main Camera that NewScene adds, since we have our own.
var defaultCam = GameObject.Find("Main Camera");
if (defaultCam != null && defaultCam != camGo) Object.DestroyImmediate(defaultCam);
}
}
}