7using System.Collections.Generic;
9using UnityEditor.SceneManagement;
11using UnityEngine.Rendering;
12using UnityEngine.Rendering.HighDefinition;
36public static class ProductVisualizerSetup
39 const string ASSET_DIR =
"Assets/ProductVisualizer/Generated";
42 const string ROOT_NAME =
"=== PRODUCT VISUALIZER ===";
49 [MenuItem(
"Tools/Product Visualizer/Setup Scene", priority = 0)]
50 public static void SetupScene()
55 var old = GameObject.Find(ROOT_NAME);
56 if (old !=
null) Object.DestroyImmediate(old);
59 var sun = GameObject.Find(
"Sun") ?? GameObject.Find(
"Directional Light");
60 if (sun !=
null) sun.SetActive(
false);
62 GameObject root =
new(ROOT_NAME);
65 SetupPostProcess(root);
67 var (backdropRenderer, groundRenderer) = SetupEnvironment(root);
68 var (productRoot, swapper) = SetupProduct(root);
70 var (cam, orbit) = SetupCamera(root);
71 var bgCtrl = SetupBackgroundController(root, backdropRenderer, groundRenderer);
72 SetupUI(root, orbit, swapper, bgCtrl);
74 EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
75 Selection.activeGameObject = root;
76 Debug.Log(
"[ProductVisualizer] Scene ready. Press Play!");
83 static void EnsureDirectories()
85 string[] parts = ASSET_DIR.Split(
'/');
86 string path = parts[0];
87 for (
int i = 1; i < parts.Length; i++)
89 string next = path +
"/" + parts[i];
90 if (!AssetDatabase.IsValidFolder(next))
91 AssetDatabase.CreateFolder(path, parts[i]);
100 static string SaveAsset(Object asset,
string name)
102 string path = $
"{ASSET_DIR}/{name}";
103 AssetDatabase.CreateAsset(asset, path);
104 AssetDatabase.SaveAssets();
115 static void SetupPostProcess(GameObject root)
117 var go =
new GameObject(
"PostProcess Volume");
118 go.transform.SetParent(root.transform);
120 var volume = go.AddComponent<Volume>();
121 volume.isGlobal =
true;
122 volume.priority = 1f;
124 var profile = ScriptableObject.CreateInstance<VolumeProfile>();
126 var bloom = profile.Add<Bloom>(
true);
127 bloom.threshold.Override(0.85f);
128 bloom.intensity.Override(0.4f);
129 bloom.scatter.Override(0.55f);
131 var color = profile.Add<ColorAdjustments>(
true);
132 color.contrast.Override(12f);
133 color.saturation.Override(8f);
135 var tone = profile.Add<Tonemapping>(
true);
136 tone.mode.Override(TonemappingMode.ACES);
138 var ao = profile.Add<ScreenSpaceAmbientOcclusion>(
true);
139 ao.intensity.Override(1.5f);
140 ao.directLightingStrength.Override(0.25f);
142 var vig = profile.Add<Vignette>(
true);
143 vig.intensity.Override(0.2f);
144 vig.smoothness.Override(0.35f);
146 profile.name =
"ProductVisualizerProfile";
147 string p = SaveAsset(profile,
"ProductVisualizerProfile.asset");
148 volume.sharedProfile = AssetDatabase.LoadAssetAtPath<VolumeProfile>(p);
163 static void SetupLights(GameObject root)
165 var lightRoot =
new GameObject(
"Lights");
166 lightRoot.transform.SetParent(root.transform);
169 MakeAreaLight(lightRoot,
"Key Light",
170 pos: new Vector3(-1.6f, 2.8f, -1.2f),
171 rot: Quaternion.Euler(48f, 35f, 0f),
172 color: new Color(1f, 0.95f, 0.85f),
174 size: new Vector2(0.8f, 1.4f));
177 MakeAreaLight(lightRoot,
"Fill Light",
178 pos: new Vector3(2.2f, 1.8f, 1f),
179 rot: Quaternion.Euler(28f, -125f, 0f),
180 color: new Color(0.65f, 0.8f, 1f),
182 size: new Vector2(1.8f, 0.9f));
185 MakeAreaLight(lightRoot,
"Rim Light",
186 pos: new Vector3(0f, 2.2f, 2f),
187 rot: Quaternion.Euler(18f, 180f, 0f),
188 color: new Color(0.9f, 0.95f, 1f),
190 size: new Vector2(0.4f, 2f));
193 MakeAreaLight(lightRoot,
"Bounce Light",
194 pos: new Vector3(0f, -0.2f, 0f),
195 rot: Quaternion.Euler(-90f, 0f, 0f),
196 color: new Color(0.5f, 0.5f, 0.7f),
198 size: new Vector2(3f, 3f));
209 static void MakeAreaLight(GameObject parent,
string name,
210 Vector3 pos, Quaternion rot, Color color,
float intensity, Vector2 size)
212 var go =
new GameObject(name);
213 go.transform.SetParent(parent.transform);
214 go.transform.localPosition = pos;
215 go.transform.localRotation = rot;
217 var light = go.AddComponent<Light>();
218 light.type = LightType.Rectangle;
220 light.intensity = intensity;
221 light.shadows = LightShadows.Soft;
223 var hd = go.GetComponent<HDAdditionalLightData>();
224 if (hd ==
null) hd = go.AddComponent<HDAdditionalLightData>();
225 hd.shapeWidth = size.x;
226 hd.shapeHeight = size.y;
227 hd.lightUnit = LightUnit.Lumen;
228 hd.intensity = intensity;
230 hd.normalBias = 0.3f;
242 static (Renderer backdrop, Renderer ground) SetupEnvironment(GameObject root)
244 var envRoot =
new GameObject(
"Environment");
245 envRoot.transform.SetParent(root.transform);
248 var backdropGO =
new GameObject(
"Backdrop");
249 backdropGO.transform.SetParent(envRoot.transform);
250 backdropGO.transform.localPosition =
new Vector3(0f, 1.5f, 3.5f);
251 backdropGO.transform.localScale =
new Vector3(9f, 7f, 1f);
253 var bmf = backdropGO.AddComponent<MeshFilter>();
254 bmf.sharedMesh = Resources.GetBuiltinResource<Mesh>(
"Quad.fbx");
255 var bmr = backdropGO.AddComponent<MeshRenderer>();
257 Shader gradShader = Shader.Find(
"Custom/GradientBackground")
258 ?? Shader.Find(
"HDRP/Unlit");
260 var backdropMat =
new Material(gradShader) { name =
"Backdrop_Mat" };
261 if (gradShader.name.Contains(
"Gradient"))
263 backdropMat.SetColor(
"_TopColor",
new Color(0.06f, 0.06f, 0.18f));
264 backdropMat.SetColor(
"_BottomColor",
new Color(0.01f, 0.01f, 0.04f));
265 backdropMat.SetFloat(
"_VignetteAmount", 1.2f);
269 backdropMat.SetColor(
"_UnlitColor",
new Color(0.03f, 0.03f, 0.1f));
271 bmr.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(
272 SaveAsset(backdropMat,
"BackdropMat.mat"));
275 var groundGO =
new GameObject(
"Ground");
276 groundGO.transform.SetParent(envRoot.transform);
277 groundGO.transform.localPosition = Vector3.zero;
278 groundGO.transform.localRotation = Quaternion.Euler(90f, 0f, 0f);
279 groundGO.transform.localScale =
new Vector3(10f, 10f, 1f);
281 var gmf = groundGO.AddComponent<MeshFilter>();
282 gmf.sharedMesh = Resources.GetBuiltinResource<Mesh>(
"Quad.fbx");
283 var gmr = groundGO.AddComponent<MeshRenderer>();
285 var groundMat =
new Material(Shader.Find(
"HDRP/Lit")) { name =
"Ground_Mat" };
286 groundMat.SetColor(
"_BaseColor",
new Color(0.08f, 0.08f, 0.1f));
287 groundMat.SetFloat(
"_Metallic", 0.4f);
288 groundMat.SetFloat(
"_Smoothness", 0.9f);
289 gmr.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(
290 SaveAsset(groundMat,
"GroundMat.mat"));
298 static void SetupPedestal(GameObject root)
300 var pedestalRoot =
new GameObject(
"Pedestal");
301 pedestalRoot.transform.SetParent(root.transform);
302 pedestalRoot.transform.localPosition = Vector3.zero;
304 var mat =
new Material(Shader.Find(
"HDRP/Lit")) { name =
"Pedestal_Mat" };
305 mat.SetColor(
"_BaseColor",
new Color(0.14f, 0.14f, 0.17f));
306 mat.SetFloat(
"_Metallic", 0.85f);
307 mat.SetFloat(
"_Smoothness", 0.92f);
308 var savedMat = AssetDatabase.LoadAssetAtPath<Material>(SaveAsset(mat,
"PedestalMat.mat"));
310 PlaceSmoothCylinder(pedestalRoot.transform,
"Pedestal_Body",
311 pos: new Vector3(0f, 0.15f, 0f), scale: new Vector3(1.2f, 0.15f, 1.2f), mat: savedMat);
313 PlaceSmoothCylinder(pedestalRoot.transform,
"Pedestal_Top",
314 pos: new Vector3(0f, 0.31f, 0f), scale: new Vector3(1.32f, 0.02f, 1.32f), mat: savedMat);
326 static void PlaceSmoothCylinder(Transform parent,
string name, Vector3 pos, Vector3 scale, Material mat)
328 const int sides = 64;
329 var mesh = BuildCylinderMesh(sides);
330 mesh.name = name +
"_Mesh";
331 string meshPath = $
"{ASSET_DIR}/{name}_Mesh.asset";
332 AssetDatabase.CreateAsset(mesh, meshPath);
334 var go =
new GameObject(name);
335 go.transform.SetParent(parent);
336 go.transform.localPosition = pos;
337 go.transform.localScale = scale;
339 go.AddComponent<MeshFilter>().sharedMesh =
340 AssetDatabase.LoadAssetAtPath<Mesh>(meshPath);
341 go.AddComponent<MeshRenderer>().sharedMaterial = mat;
342 go.AddComponent<MeshCollider>().sharedMesh =
343 AssetDatabase.LoadAssetAtPath<Mesh>(meshPath);
354 static Mesh BuildCylinderMesh(
int sides)
356 var mesh =
new Mesh();
359 int vertsPerRing = sides + 1;
360 int totalVerts = vertsPerRing * rings + 2;
362 var vertices =
new Vector3[totalVerts];
363 var normals =
new Vector3[totalVerts];
364 var uvs =
new Vector2[totalVerts];
367 for (
int ring = 0; ring < rings; ring++)
369 float y = ring == 0 ? -1f : 1f;
370 for (
int i = 0; i <= sides; i++)
372 float angle = i / (float)sides * Mathf.PI * 2f;
373 float x = Mathf.Cos(angle);
374 float z = Mathf.Sin(angle);
375 int idx = ring * vertsPerRing + i;
376 vertices[idx] =
new Vector3(x, y, z);
377 normals[idx] =
new Vector3(x, 0f, z);
378 uvs[idx] =
new Vector2(i / (
float)sides, ring);
383 int bottomCentre = vertsPerRing * rings;
384 int topCentre = bottomCentre + 1;
385 vertices[bottomCentre] =
new Vector3(0f, -1f, 0f);
386 normals[bottomCentre] = Vector3.down;
387 uvs[bottomCentre] =
new Vector2(0.5f, 0f);
388 vertices[topCentre] =
new Vector3(0f, 1f, 0f);
389 normals[topCentre] = Vector3.up;
390 uvs[topCentre] =
new Vector2(0.5f, 1f);
392 mesh.vertices = vertices;
393 mesh.normals = normals;
397 var tris =
new System.Collections.Generic.List<
int>();
398 for (
int i = 0; i < sides; i++)
400 int b0 = i, b1 = i + 1;
401 int t0 = vertsPerRing + i, t1 = vertsPerRing + i + 1;
402 tris.AddRange(
new[] { b0, t0, b1, b1, t0, t1 });
406 for (
int i = 0; i < sides; i++)
407 tris.AddRange(
new[] { bottomCentre, i + 1, i });
410 for (
int i = 0; i < sides; i++)
411 tris.AddRange(
new[] { topCentre, vertsPerRing + i, vertsPerRing + i + 1 });
413 mesh.triangles = tris.ToArray();
414 mesh.RecalculateBounds();
428 static (GameObject productRoot,
CanMaterialSwapper swapper) SetupProduct(GameObject root)
430 var productRoot =
new GameObject(
"Product");
431 productRoot.transform.SetParent(root.transform);
432 productRoot.transform.localPosition =
new Vector3(0f, 0.72f, 0f);
436 var canPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(
"Assets/Resources/Prefabs/Can.prefab");
438 if (canPrefab !=
null)
440 canGO = (GameObject)PrefabUtility.InstantiatePrefab(canPrefab, productRoot.transform);
441 canGO.transform.localPosition = Vector3.zero;
442 canGO.transform.localRotation = Quaternion.identity;
443 canGO.transform.localScale = Vector3.one;
448 canGO = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
449 canGO.transform.SetParent(productRoot.transform);
450 canGO.transform.localPosition = Vector3.zero;
451 canGO.transform.localScale =
new Vector3(0.36f, 0.55f, 0.36f);
452 Debug.LogWarning(
"[ProductVisualizer] Can.prefab not found at Assets/Resources/Prefabs/Can.prefab");
456 var capMat =
new Material(Shader.Find(
"HDRP/Lit")) { name =
"Can_Cap_Mat" };
457 capMat.SetColor(
"_BaseColor",
new Color(0.76f, 0.76f, 0.80f));
458 capMat.SetFloat(
"_Metallic", 0.96f);
459 capMat.SetFloat(
"_Smoothness", 0.92f);
460 var savedCapMat = AssetDatabase.LoadAssetAtPath<Material>(SaveAsset(capMat,
"Can_Cap_Mat.mat"));
463 var brands =
new (
string name,
string texPath)[]
465 (
"Coca-Cola",
"Assets/Resources/textures/Coke Can.jpeg"),
466 (
"Pepsi",
"Assets/Resources/textures/Pepsi Can.jpeg"),
467 (
"Mountain Dew",
"Assets/Resources/textures/Mountain Dew Can.jpeg"),
468 (
"Sprite",
"Assets/Resources/textures/Spritei Can.jpeg"),
471 var litShader = Shader.Find(
"HDRP/Lit");
473 swapper.canRenderer = canGO.GetComponent<Renderer>()
474 ?? canGO.GetComponentInChildren<Renderer>();
476 foreach (var (brandName, texPath) in brands)
478 var tex = AssetDatabase.LoadAssetAtPath<Texture2D>(texPath);
481 Debug.LogWarning($
"[ProductVisualizer] Texture not found: {texPath}");
485 string safeName = brandName.Replace(
" ",
"_");
486 var labelMat =
new Material(litShader) { name = $
"Can_Label_{safeName}" };
487 labelMat.SetTexture(
"_BaseColorMap", tex);
488 labelMat.SetColor(
"_BaseColor", Color.white);
489 labelMat.SetFloat(
"_Metallic", 0.15f);
490 labelMat.SetFloat(
"_Smoothness", 0.50f);
491 var savedLabelMat = AssetDatabase.LoadAssetAtPath<Material>(
492 SaveAsset(labelMat, $
"Can_Label_{safeName}.mat"));
496 variantName = brandName,
497 labelMaterial = savedLabelMat,
498 capMaterial = savedCapMat,
502 return (productRoot, swapper);
513 static (GameObject camGO,
OrbitCamera orbit) SetupCamera(GameObject root)
516 var target =
new GameObject(
"Camera Target");
517 target.transform.SetParent(root.transform);
518 target.transform.localPosition =
new Vector3(0f, 0.7f, 0f);
521 Camera mainCam = Camera.main;
525 camGO =
new GameObject(
"Main Camera");
526 camGO.tag =
"MainCamera";
527 mainCam = camGO.AddComponent<Camera>();
528 camGO.AddComponent<AudioListener>();
532 camGO = mainCam.gameObject;
535 camGO.transform.SetParent(root.transform);
536 camGO.transform.localPosition =
new Vector3(0f, 0.7f, -3f);
537 mainCam.usePhysicalProperties =
true;
538 mainCam.nearClipPlane = 0.1f;
539 mainCam.farClipPlane = 50f;
541 var hd = camGO.GetComponent<HDAdditionalCameraData>();
542 if (hd ==
null) hd = camGO.AddComponent<HDAdditionalCameraData>();
545 orbit.target = target.transform;
547 orbit.autoRotate =
true;
548 orbit.autoRotateSpeed = 20f;
551 var probeGO =
new GameObject(
"Reflection Probe");
552 probeGO.transform.SetParent(root.transform);
553 probeGO.transform.localPosition =
new Vector3(0f, 1f, 0f);
554 var probe = probeGO.AddComponent<ReflectionProbe>();
555 probe.size =
new Vector3(10f, 6f, 10f);
556 probe.resolution = 256;
557 probe.mode = ReflectionProbeMode.Realtime;
558 probe.refreshMode = ReflectionProbeRefreshMode.EveryFrame;
560 return (camGO, orbit);
572 GameObject root, Renderer backdrop, Renderer ground)
574 var go =
new GameObject(
"Background Controller");
575 go.transform.SetParent(root.transform);
578 ctrl.backdropRenderer = backdrop;
579 ctrl.groundRenderer = ground;
581 ctrl.presets =
new List<BackgroundPreset>
583 new() { presetName =
"Dark Studio",
584 topColor =
new Color(0.06f, 0.06f, 0.18f), bottomColor =
new Color(0.01f, 0.01f, 0.04f),
585 vignette = 1.2f, groundTint =
new Color(0.08f, 0.08f, 0.10f) },
587 new() { presetName =
"Warm Amber",
588 topColor =
new Color(0.20f, 0.10f, 0.03f), bottomColor =
new Color(0.04f, 0.02f, 0.01f),
589 vignette = 1.0f, groundTint =
new Color(0.12f, 0.08f, 0.04f) },
591 new() { presetName =
"Deep Space",
592 topColor =
new Color(0.01f, 0.02f, 0.08f), bottomColor =
new Color(0.00f, 0.00f, 0.02f),
593 vignette = 1.5f, groundTint =
new Color(0.03f, 0.03f, 0.06f) },
595 new() { presetName =
"Emerald Night",
596 topColor =
new Color(0.02f, 0.10f, 0.06f), bottomColor =
new Color(0.01f, 0.03f, 0.02f),
597 vignette = 1.1f, groundTint =
new Color(0.04f, 0.08f, 0.05f) },
613 static void SetupUI(GameObject root,
617 var canvasGO =
new GameObject(
"UI Canvas");
618 canvasGO.transform.SetParent(root.transform);
619 var canvas = canvasGO.AddComponent<Canvas>();
620 canvas.renderMode = RenderMode.ScreenSpaceOverlay;
621 canvas.sortingOrder = 10;
622 var scaler = canvasGO.AddComponent<CanvasScaler>();
623 scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
624 scaler.referenceResolution =
new Vector2(1920f, 1080f);
625 scaler.matchWidthOrHeight = 0.5f;
626 canvasGO.AddComponent<GraphicRaycaster>();
629 if (Object.FindFirstObjectByType<UnityEngine.EventSystems.EventSystem>() ==
null)
631 var es =
new GameObject(
"EventSystem");
632 es.transform.SetParent(root.transform);
633 es.AddComponent<UnityEngine.EventSystems.EventSystem>();
634 es.AddComponent<UnityEngine.EventSystems.StandaloneInputModule>();
638 var bar = MakePanel(canvasGO.transform,
"Controls Bar",
639 anchorMin: new Vector2(0f, 0f), anchorMax: new Vector2(1f, 0f),
640 pivot: new Vector2(0.5f, 0f), size: new Vector2(0f, 72f), pos: Vector2.zero);
641 var barImg = bar.AddComponent<Image>();
642 barImg.color =
new Color(0f, 0f, 0f, 0.55f);
644 float bw = 150f, bh = 48f, gap = 160f;
645 float startX = -gap * 2f;
647 var btnAutoRot = MakeButton(bar.transform,
"Auto-Rotate",
new Vector2(startX, 36f),
new Vector2(bw, bh));
648 var btnPrevCol = MakeButton(bar.transform,
"< Brand",
new Vector2(startX + gap, 36f),
new Vector2(bw, bh));
649 var btnNextCol = MakeButton(bar.transform,
"Brand >",
new Vector2(startX + gap*2, 36f),
new Vector2(bw, bh));
650 var btnBg = MakeButton(bar.transform,
"Background",
new Vector2(startX + gap*3, 36f),
new Vector2(bw, bh));
651 var btnReset = MakeButton(bar.transform,
"Reset Camera",
new Vector2(startX + gap*4, 36f),
new Vector2(bw, bh));
654 var info = MakePanel(canvasGO.transform,
"Info Panel",
655 anchorMin: new Vector2(0f, 1f), anchorMax: new Vector2(0f, 1f),
656 pivot: new Vector2(0f, 1f), size: new Vector2(240f, 110f), pos: new Vector2(16f, -16f));
657 var infoImg = info.AddComponent<Image>();
658 infoImg.color =
new Color(0f, 0f, 0f, 0.45f);
660 var lblVariant = MakeLabel(info.transform,
"VariantLabel",
"Brand: Coca-Cola",
new Vector2(12f, -12f));
661 var lblBg = MakeLabel(info.transform,
"BgLabel",
"Scene: Dark Studio",
new Vector2(12f, -42f));
662 var lblRot = MakeLabel(info.transform,
"RotLabel",
"Auto-Rotate: ON",
new Vector2(12f, -72f));
666 ui.orbitCamera = orbit;
667 ui.canSwapper = swapper;
668 ui.backgroundController = bgCtrl;
669 ui.autoRotateButton = btnAutoRot;
670 ui.nextColorButton = btnNextCol;
671 ui.prevColorButton = btnPrevCol;
672 ui.nextBgButton = btnBg;
673 ui.resetCameraButton = btnReset;
674 ui.variantLabel = lblVariant;
675 ui.backgroundLabel = lblBg;
676 ui.rotationLabel = lblRot;
681 static GameObject MakePanel(Transform parent,
string name,
682 Vector2 anchorMin, Vector2 anchorMax, Vector2 pivot, Vector2 size, Vector2 pos)
684 var go =
new GameObject(name);
685 go.transform.SetParent(parent,
false);
686 var rt = go.AddComponent<RectTransform>();
687 rt.anchorMin = anchorMin;
688 rt.anchorMax = anchorMax;
691 rt.anchoredPosition = pos;
696 static Button MakeButton(Transform parent,
string label, Vector2 pos, Vector2 size)
698 var go =
new GameObject($
"Btn_{label}");
699 go.transform.SetParent(parent,
false);
700 var rt = go.AddComponent<RectTransform>();
701 rt.anchoredPosition = pos;
704 var img = go.AddComponent<Image>();
705 img.color =
new Color(0.12f, 0.12f, 0.18f, 0.92f);
707 var btn = go.AddComponent<Button>();
708 ColorBlock cb = btn.colors;
709 cb.highlightedColor =
new Color(0.22f, 0.22f, 0.35f, 1f);
710 cb.pressedColor =
new Color(0.08f, 0.08f, 0.12f, 1f);
712 btn.targetGraphic = img;
714 var textGO =
new GameObject(
"Label");
715 textGO.transform.SetParent(go.transform,
false);
716 var textRT = textGO.AddComponent<RectTransform>();
717 textRT.anchorMin = Vector2.zero;
718 textRT.anchorMax = Vector2.one;
719 textRT.offsetMin = Vector2.zero;
720 textRT.offsetMax = Vector2.zero;
722 var tmp = textGO.AddComponent<TextMeshProUGUI>();
725 tmp.alignment = TextAlignmentOptions.Center;
726 tmp.color = Color.white;
731 static TextMeshProUGUI MakeLabel(Transform parent,
string name,
string text, Vector2 pos)
733 var go =
new GameObject(name);
734 go.transform.SetParent(parent,
false);
735 var rt = go.AddComponent<RectTransform>();
736 rt.anchorMin =
new Vector2(0f, 1f);
737 rt.anchorMax =
new Vector2(1f, 1f);
738 rt.pivot =
new Vector2(0f, 1f);
739 rt.anchoredPosition = pos;
740 rt.sizeDelta =
new Vector2(-24f, 26f);
742 var tmp = go.AddComponent<TextMeshProUGUI>();
745 tmp.color = Color.white;
Cycles through BackgroundPreset entries by writing to MaterialPropertyBlocks.
Cycles through brand variants by replacing the full material array on the can renderer.
List< CanVariant > variants
Ordered list of brand variants available in the visualizer.
Data container that pairs a brand label material with an aluminium cap material.
Spherical-coordinate camera that orbits around a target Transform.
Animates a GameObject with a gentle sinusoidal float and dual-axis tilt.
Connects Unity UI buttons and TextMeshPro labels to the three scene controllers.