Repository: playcanvas/engine
Pull request #5851 (2023-11-25 20:01:15)

Add Entity.findScript[s] functions
Follow-up of #4603

This PR adds `Entity.findScript` and `Entity.findScripts` based on what already exists for components.

I confirm I have read the [contributing guidelines](https://github.com/playcanvas/engine/blob/master/.github/CONTRIBUTING.md) and signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform).



Patch:
diff --git a/examples/scripts/example-data.mjs b/examples/scripts/example-data.mjs
index 21e041a70..5d4e9919b 100644
--- a/examples/scripts/example-data.mjs
+++ b/examples/scripts/example-data.mjs
@@ -18,6 +18,11 @@ for (const category_ in realExamples) {
     exampleData[category] = {};
     const examples = realExamples[category_];
     for (const exampleName_ in examples) {
+        const release = process.env.NODE_ENV !== 'development';
+        if (release && examples[exampleName_].HIDDEN) {
+            console.log(`build:example:data> skip hidden example: ${category_}/${exampleName_}`);
+            continue;
+        }
         const example = toKebabCase(exampleName_).replace('-example', '');
         // turn: turn into simple array...
         exampleData[category][example] = {};
diff --git a/examples/src/examples/animation/blend-trees-1d.mjs b/examples/src/examples/animation/blend-trees-1d.mjs
index d25bdf4ae..0b3c8bfdb 100644
--- a/examples/src/examples/animation/blend-trees-1d.mjs
+++ b/examples/src/examples/animation/blend-trees-1d.mjs
@@ -200,7 +200,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, data, glsla
 }
 class BlendTrees1DExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Blend Trees 1D';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/blend-trees-2d-cartesian.mjs b/examples/src/examples/animation/blend-trees-2d-cartesian.mjs
index ff2e4e2f6..3ade7b27b 100644
--- a/examples/src/examples/animation/blend-trees-2d-cartesian.mjs
+++ b/examples/src/examples/animation/blend-trees-2d-cartesian.mjs
@@ -315,7 +315,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 }
 class BlendTrees2DCartesianExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Blend Trees 2D Cartesian';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/blend-trees-2d-directional.mjs b/examples/src/examples/animation/blend-trees-2d-directional.mjs
index 75fc60da4..502a562f1 100644
--- a/examples/src/examples/animation/blend-trees-2d-directional.mjs
+++ b/examples/src/examples/animation/blend-trees-2d-directional.mjs
@@ -276,7 +276,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 class BlendTrees2DDirectionalExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Blend Trees 2D Directional';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/component-properties.mjs b/examples/src/examples/animation/component-properties.mjs
index 13ae73bec..092638b5c 100644
--- a/examples/src/examples/animation/component-properties.mjs
+++ b/examples/src/examples/animation/component-properties.mjs
@@ -299,7 +299,6 @@ async function example({ canvas, deviceType, data, assetPath, glslangPath, twgsl
 
 class ComponentPropertiesExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Component Properties';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/events.mjs b/examples/src/examples/animation/events.mjs
index ec57d20cb..ffecbaeee 100644
--- a/examples/src/examples/animation/events.mjs
+++ b/examples/src/examples/animation/events.mjs
@@ -201,7 +201,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 }
 class EventsExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Events';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/animation/layer-masks.mjs b/examples/src/examples/animation/layer-masks.mjs
index 50b863dcd..01a9806f8 100644
--- a/examples/src/examples/animation/layer-masks.mjs
+++ b/examples/src/examples/animation/layer-masks.mjs
@@ -298,7 +298,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class LayerMasksExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Layer Masks';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/locomotion.mjs b/examples/src/examples/animation/locomotion.mjs
index afc2982a2..c3702aa6e 100644
--- a/examples/src/examples/animation/locomotion.mjs
+++ b/examples/src/examples/animation/locomotion.mjs
@@ -421,7 +421,6 @@ async function example({ canvas, deviceType, assetPath, ammoPath, glslangPath, t
 }
 class LocomotionExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Locomotion';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/animation/tween.mjs b/examples/src/examples/animation/tween.mjs
index 4a1c1e374..5f2e6172f 100644
--- a/examples/src/examples/animation/tween.mjs
+++ b/examples/src/examples/animation/tween.mjs
@@ -175,7 +175,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class TweenExample {
     static CATEGORY = 'Animation';
-    static NAME = 'Tween';
     static WEBGPU_ENABLED = true;
     static example = example;
     // added by examples/scripts/generate-standalone-files.mjs
diff --git a/examples/src/examples/camera/first-person.mjs b/examples/src/examples/camera/first-person.mjs
index 5d1bab9aa..009c5e73c 100644
--- a/examples/src/examples/camera/first-person.mjs
+++ b/examples/src/examples/camera/first-person.mjs
@@ -142,7 +142,6 @@ async function example({ canvas, assetPath, scriptsPath, ammoPath }) {
 
 class FirstPersonExample {
     static CATEGORY = 'Camera';
-    static NAME = 'First Person';
     static example = example;
 }
 
diff --git a/examples/src/examples/camera/fly.mjs b/examples/src/examples/camera/fly.mjs
index 03e8981ee..f4b025bb4 100644
--- a/examples/src/examples/camera/fly.mjs
+++ b/examples/src/examples/camera/fly.mjs
@@ -128,7 +128,6 @@ async function example({ canvas, scriptsPath }) {
 
 class FlyExample {
     static CATEGORY = 'Camera';
-    static NAME = 'Fly';
     static example = example;
 }
 
diff --git a/examples/src/examples/camera/orbit.mjs b/examples/src/examples/camera/orbit.mjs
index 79e99595c..8eb5f1e92 100644
--- a/examples/src/examples/camera/orbit.mjs
+++ b/examples/src/examples/camera/orbit.mjs
@@ -75,7 +75,6 @@ async function example({ canvas, assetPath, scriptsPath }) {
 
 class OrbitExample {
     static CATEGORY = 'Camera';
-    static NAME = 'Orbit';
     static example = example;
 }
 
diff --git a/examples/src/examples/graphics/area-lights.mjs b/examples/src/examples/graphics/area-lights.mjs
index 9e509fce8..fe1d75a84 100644
--- a/examples/src/examples/graphics/area-lights.mjs
+++ b/examples/src/examples/graphics/area-lights.mjs
@@ -258,7 +258,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class AreaLightsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Area Lights';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/area-picker.mjs b/examples/src/examples/graphics/area-picker.mjs
index 72aa19819..a70575a94 100644
--- a/examples/src/examples/graphics/area-picker.mjs
+++ b/examples/src/examples/graphics/area-picker.mjs
@@ -257,7 +257,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class AreaPickerExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Area Picker';
     static example = example;
     static WEBGPU_ENABLED = false; // device.updateBegin() is not a function
 }
diff --git a/examples/src/examples/graphics/asset-viewer.mjs b/examples/src/examples/graphics/asset-viewer.mjs
index 5d919439e..7d55f8ab3 100644
--- a/examples/src/examples/graphics/asset-viewer.mjs
+++ b/examples/src/examples/graphics/asset-viewer.mjs
@@ -267,7 +267,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class AssetViewerExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Asset Viewer';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/batching-dynamic.mjs b/examples/src/examples/graphics/batching-dynamic.mjs
index b658cd038..c1cac856d 100644
--- a/examples/src/examples/graphics/batching-dynamic.mjs
+++ b/examples/src/examples/graphics/batching-dynamic.mjs
@@ -144,7 +144,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class BatchingDynamicExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Batching Dynamic';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/clustered-area-lights.mjs b/examples/src/examples/graphics/clustered-area-lights.mjs
index 8876c339a..b75a0e78b 100644
--- a/examples/src/examples/graphics/clustered-area-lights.mjs
+++ b/examples/src/examples/graphics/clustered-area-lights.mjs
@@ -314,7 +314,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class ClusteredAreaLightsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Clustered Area Lights';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/clustered-lighting.mjs b/examples/src/examples/graphics/clustered-lighting.mjs
index 2a872fc86..bac2712ad 100644
--- a/examples/src/examples/graphics/clustered-lighting.mjs
+++ b/examples/src/examples/graphics/clustered-lighting.mjs
@@ -246,7 +246,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class ClusteredLightingExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Clustered Lighting';
     static ENGINE = 'PERFORMANCE';
     static WEBGPU_ENABLED = true;
     static example = example;
diff --git a/examples/src/examples/graphics/clustered-omni-shadows.mjs b/examples/src/examples/graphics/clustered-omni-shadows.mjs
index 33700b00e..990676787 100644
--- a/examples/src/examples/graphics/clustered-omni-shadows.mjs
+++ b/examples/src/examples/graphics/clustered-omni-shadows.mjs
@@ -311,7 +311,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class ClusteredOmniShadowsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Clustered Omni Shadows';
     static controls = controls;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/clustered-spot-shadows.mjs b/examples/src/examples/graphics/clustered-spot-shadows.mjs
index a6dc34fe4..2649a4c92 100644
--- a/examples/src/examples/graphics/clustered-spot-shadows.mjs
+++ b/examples/src/examples/graphics/clustered-spot-shadows.mjs
@@ -464,7 +464,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class ClusteredSpotShadowsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Clustered Spot Shadows';
     static ENGINE = 'DEBUG';
     static WEBGPU_ENABLED = true;
     static controls = controls;
diff --git a/examples/src/examples/graphics/contact-hardening-shadows.mjs b/examples/src/examples/graphics/contact-hardening-shadows.mjs
index 6a2e98ac4..9feab351a 100644
--- a/examples/src/examples/graphics/contact-hardening-shadows.mjs
+++ b/examples/src/examples/graphics/contact-hardening-shadows.mjs
@@ -483,7 +483,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class ContactHardeningShadowsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Contact Hardening Shadows';
     static WEBGPU_ENABLED = false;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/grab-pass.mjs b/examples/src/examples/graphics/grab-pass.mjs
index b597d5932..37a0c67d8 100644
--- a/examples/src/examples/graphics/grab-pass.mjs
+++ b/examples/src/examples/graphics/grab-pass.mjs
@@ -180,7 +180,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 export class GrabPassExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Grab Pass';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/ground-fog.mjs b/examples/src/examples/graphics/ground-fog.mjs
index 7a51e8781..54f727c47 100644
--- a/examples/src/examples/graphics/ground-fog.mjs
+++ b/examples/src/examples/graphics/ground-fog.mjs
@@ -205,7 +205,6 @@ async function example({ canvas, deviceType, files, assetPath, scriptsPath, glsl
 
 export class GroundFogExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Ground Fog';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/hardware-instancing.mjs b/examples/src/examples/graphics/hardware-instancing.mjs
index e5dc38cdf..4e3602cd3 100644
--- a/examples/src/examples/graphics/hardware-instancing.mjs
+++ b/examples/src/examples/graphics/hardware-instancing.mjs
@@ -135,7 +135,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class HardwareInstancingExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Hardware Instancing';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/hierarchy.mjs b/examples/src/examples/graphics/hierarchy.mjs
index 31f1085d9..cab1df3f6 100644
--- a/examples/src/examples/graphics/hierarchy.mjs
+++ b/examples/src/examples/graphics/hierarchy.mjs
@@ -152,7 +152,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class HierarchyExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Hierarchy';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/index.mjs b/examples/src/examples/graphics/index.mjs
index d68e1ab97..d4eaae54b 100644
--- a/examples/src/examples/graphics/index.mjs
+++ b/examples/src/examples/graphics/index.mjs
@@ -8,7 +8,7 @@ export * from "./clustered-lighting.mjs";
 export * from "./clustered-omni-shadows.mjs";
 export * from "./clustered-spot-shadows.mjs";
 export * from "./contact-hardening-shadows.mjs";
-// export * from "./lit-material";   // note that this is also excluded in read-dir.mjs
+export * from "./lit-material.mjs";
 export * from "./grab-pass.mjs";
 export * from "./ground-fog.mjs";
 export * from "./hardware-instancing.mjs";
@@ -33,7 +33,7 @@ export * from "./model-asset.mjs";
 export * from "./model-outline.mjs";
 export * from "./model-textured-box.mjs";
 export * from "./multi-view.mjs";
-export * from "./mrt.mjs";
+export * from "./multi-render-targets.mjs";
 export * from "./painter.mjs";
 export * from "./paint-mesh.mjs";
 export * from "./particles-anim-index.mjs";
diff --git a/examples/src/examples/graphics/layers.mjs b/examples/src/examples/graphics/layers.mjs
index 522e9ebcd..c26d1daef 100644
--- a/examples/src/examples/graphics/layers.mjs
+++ b/examples/src/examples/graphics/layers.mjs
@@ -125,7 +125,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class LayersExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Layers';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/light-physical-units.mjs b/examples/src/examples/graphics/light-physical-units.mjs
index 19cc95e59..f840e8ae5 100644
--- a/examples/src/examples/graphics/light-physical-units.mjs
+++ b/examples/src/examples/graphics/light-physical-units.mjs
@@ -429,7 +429,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class LightPhysicalUnitsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Light Physical Units';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/lights-baked-a-o.mjs b/examples/src/examples/graphics/lights-baked-a-o.mjs
index e9b8800e1..602ba4ff2 100644
--- a/examples/src/examples/graphics/lights-baked-a-o.mjs
+++ b/examples/src/examples/graphics/lights-baked-a-o.mjs
@@ -419,7 +419,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class LightsBakedAOExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Lights Baked AO';
     static controls = controls;
     static example = example;
     static WEBGPU_ENABLED = false; // house is just gray
diff --git a/examples/src/examples/graphics/lights-baked.mjs b/examples/src/examples/graphics/lights-baked.mjs
index 336f7bcea..a3e7a0dab 100644
--- a/examples/src/examples/graphics/lights-baked.mjs
+++ b/examples/src/examples/graphics/lights-baked.mjs
@@ -154,7 +154,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class LightsBakedExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Lights Baked';
     static example = example;
     static WEBGPU_ENABLED = false; // house is black
 }
diff --git a/examples/src/examples/graphics/lights.mjs b/examples/src/examples/graphics/lights.mjs
index a6865a39d..a5234b941 100644
--- a/examples/src/examples/graphics/lights.mjs
+++ b/examples/src/examples/graphics/lights.mjs
@@ -343,7 +343,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class LightsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Lights';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/lines.mjs b/examples/src/examples/graphics/lines.mjs
index b90bc2d13..ccf6cd8d6 100644
--- a/examples/src/examples/graphics/lines.mjs
+++ b/examples/src/examples/graphics/lines.mjs
@@ -211,7 +211,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class LinesExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Lines';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/lit-material.mjs b/examples/src/examples/graphics/lit-material.mjs
index 981f957bc..ff5346b88 100644
--- a/examples/src/examples/graphics/lit-material.mjs
+++ b/examples/src/examples/graphics/lit-material.mjs
@@ -166,7 +166,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class LitMaterialExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Lit Material';
     static HIDDEN = true;
     static WEBGPU_ENABLED = true;
     static example = example;
diff --git a/examples/src/examples/graphics/material-anisotropic.mjs b/examples/src/examples/graphics/material-anisotropic.mjs
index c7591d185..2ca130522 100644
--- a/examples/src/examples/graphics/material-anisotropic.mjs
+++ b/examples/src/examples/graphics/material-anisotropic.mjs
@@ -145,7 +145,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MaterialAnisotropicExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Material Anisotropic';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/material-basic.mjs b/examples/src/examples/graphics/material-basic.mjs
index 3bbcb8ecc..c48af89e9 100644
--- a/examples/src/examples/graphics/material-basic.mjs
+++ b/examples/src/examples/graphics/material-basic.mjs
@@ -167,7 +167,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MaterialBasicExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Material Basic';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/material-clear-coat.mjs b/examples/src/examples/graphics/material-clear-coat.mjs
index d2bb809df..1971be8bb 100644
--- a/examples/src/examples/graphics/material-clear-coat.mjs
+++ b/examples/src/examples/graphics/material-clear-coat.mjs
@@ -145,7 +145,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MaterialClearCoatExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Material Clear Coat';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/material-physical.mjs b/examples/src/examples/graphics/material-physical.mjs
index 2beff3c8d..cae8b0e76 100644
--- a/examples/src/examples/graphics/material-physical.mjs
+++ b/examples/src/examples/graphics/material-physical.mjs
@@ -143,7 +143,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MaterialPhysicalExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Material Physical';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/material-translucent-specular.mjs b/examples/src/examples/graphics/material-translucent-specular.mjs
index c478c4f7b..6bd0f9ef2 100644
--- a/examples/src/examples/graphics/material-translucent-specular.mjs
+++ b/examples/src/examples/graphics/material-translucent-specular.mjs
@@ -149,7 +149,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MaterialTranslucentSpecularExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Material Translucent Specular';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/mesh-decals.mjs b/examples/src/examples/graphics/mesh-decals.mjs
index b86e9aec7..55b7972d0 100644
--- a/examples/src/examples/graphics/mesh-decals.mjs
+++ b/examples/src/examples/graphics/mesh-decals.mjs
@@ -279,7 +279,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MeshDecalsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Mesh Decals';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/mesh-deformation.mjs b/examples/src/examples/graphics/mesh-deformation.mjs
index 358aece97..7f4008b27 100644
--- a/examples/src/examples/graphics/mesh-deformation.mjs
+++ b/examples/src/examples/graphics/mesh-deformation.mjs
@@ -141,7 +141,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MeshDeformationExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Mesh Deformation';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/mesh-generation.mjs b/examples/src/examples/graphics/mesh-generation.mjs
index 3636beb6c..8bf95414c 100644
--- a/examples/src/examples/graphics/mesh-generation.mjs
+++ b/examples/src/examples/graphics/mesh-generation.mjs
@@ -231,7 +231,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MeshGenerationExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Mesh Generation';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/mesh-morph-many.mjs b/examples/src/examples/graphics/mesh-morph-many.mjs
index 19d679718..460db79ed 100644
--- a/examples/src/examples/graphics/mesh-morph-many.mjs
+++ b/examples/src/examples/graphics/mesh-morph-many.mjs
@@ -114,7 +114,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class MeshMorphManyExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Mesh Morph Many';
     static example = example;
     static WEBGPU_ENABLED = false; // doesn't morph anything
 }
diff --git a/examples/src/examples/graphics/mesh-morph.mjs b/examples/src/examples/graphics/mesh-morph.mjs
index d34632ed6..f5811fb8c 100644
--- a/examples/src/examples/graphics/mesh-morph.mjs
+++ b/examples/src/examples/graphics/mesh-morph.mjs
@@ -218,7 +218,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class MeshMorphExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Mesh Morph';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/model-asset.mjs b/examples/src/examples/graphics/model-asset.mjs
index 083aa52e6..236765626 100644
--- a/examples/src/examples/graphics/model-asset.mjs
+++ b/examples/src/examples/graphics/model-asset.mjs
@@ -99,7 +99,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class ModelAssetExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Model Asset';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/model-outline.mjs b/examples/src/examples/graphics/model-outline.mjs
index fe9749634..b220ca7b2 100644
--- a/examples/src/examples/graphics/model-outline.mjs
+++ b/examples/src/examples/graphics/model-outline.mjs
@@ -188,7 +188,6 @@ async function example({ canvas, deviceType, scriptsPath, glslangPath, twgslPath
 
 export class ModelOutlineExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Model Outline';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/model-textured-box.mjs b/examples/src/examples/graphics/model-textured-box.mjs
index 7ed5802b1..95e1df048 100644
--- a/examples/src/examples/graphics/model-textured-box.mjs
+++ b/examples/src/examples/graphics/model-textured-box.mjs
@@ -113,7 +113,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class ModelTexturedBoxExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Model Textured Box';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/mrt.mjs b/examples/src/examples/graphics/multi-render-targets.mjs
similarity index 99%
rename from examples/src/examples/graphics/mrt.mjs
rename to examples/src/examples/graphics/multi-render-targets.mjs
index 9418d897e..17f864ee8 100644
--- a/examples/src/examples/graphics/mrt.mjs
+++ b/examples/src/examples/graphics/multi-render-targets.mjs
@@ -197,11 +197,9 @@ async function example({ canvas, deviceType, files, dracoPath, assetPath, glslan
     return app;
 }
 
-export class MrtExample {
+export class MultiRenderTargetsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Multi Render Targets';
     static WEBGPU_ENABLED = true;
-
     static FILES = {
 
         // shader chunk which outputs to multiple render targets
diff --git a/examples/src/examples/graphics/multi-view.mjs b/examples/src/examples/graphics/multi-view.mjs
index bc0ae2270..8c106da63 100644
--- a/examples/src/examples/graphics/multi-view.mjs
+++ b/examples/src/examples/graphics/multi-view.mjs
@@ -234,7 +234,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class MultiViewExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Multi View';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/paint-mesh.mjs b/examples/src/examples/graphics/paint-mesh.mjs
index e90b2379e..d94136708 100644
--- a/examples/src/examples/graphics/paint-mesh.mjs
+++ b/examples/src/examples/graphics/paint-mesh.mjs
@@ -200,7 +200,6 @@ async function example({ canvas, files, assetPath }) {
 
 export class PaintMeshExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Paint Mesh';
 
     static FILES = {
         'shader.vert': /* glsl */`
diff --git a/examples/src/examples/graphics/painter.mjs b/examples/src/examples/graphics/painter.mjs
index edf49be48..27f27d821 100644
--- a/examples/src/examples/graphics/painter.mjs
+++ b/examples/src/examples/graphics/painter.mjs
@@ -225,7 +225,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class PainterExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Painter';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/particles-anim-index.mjs b/examples/src/examples/graphics/particles-anim-index.mjs
index 036a921ab..a6b29c1f3 100644
--- a/examples/src/examples/graphics/particles-anim-index.mjs
+++ b/examples/src/examples/graphics/particles-anim-index.mjs
@@ -153,6 +153,5 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class ParticlesAnimIndexExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Particles: Anim Index';
     static example = example;
 }
diff --git a/examples/src/examples/graphics/particles-random-sprites.mjs b/examples/src/examples/graphics/particles-random-sprites.mjs
index 3ce862000..b5e3027a0 100644
--- a/examples/src/examples/graphics/particles-random-sprites.mjs
+++ b/examples/src/examples/graphics/particles-random-sprites.mjs
@@ -155,6 +155,5 @@ async function example({ canvas, assetPath }) {
 
 export class ParticlesRandomSpritesExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Particles: Random Sprites';
     static example = example;
 }
diff --git a/examples/src/examples/graphics/particles-snow.mjs b/examples/src/examples/graphics/particles-snow.mjs
index 516c53c4a..3bd04cc91 100644
--- a/examples/src/examples/graphics/particles-snow.mjs
+++ b/examples/src/examples/graphics/particles-snow.mjs
@@ -97,7 +97,6 @@ async function example({ canvas, assetPath }) {
 
 export class ParticlesSnowExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Particles: Snow';
     static example = example;
     static WEBGPU_ENABLED = false; // no particles visible after hot-reload
 }
diff --git a/examples/src/examples/graphics/particles-spark.mjs b/examples/src/examples/graphics/particles-spark.mjs
index c8bde1321..ed5e54612 100644
--- a/examples/src/examples/graphics/particles-spark.mjs
+++ b/examples/src/examples/graphics/particles-spark.mjs
@@ -143,7 +143,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class ParticlesSparkExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Particles: Spark';
     static example = example;
     static WEBGPU_ENABLED = false; // no particles visible
 }
diff --git a/examples/src/examples/graphics/point-cloud-simulation.mjs b/examples/src/examples/graphics/point-cloud-simulation.mjs
index 9686ac58a..6d1f2a9cd 100644
--- a/examples/src/examples/graphics/point-cloud-simulation.mjs
+++ b/examples/src/examples/graphics/point-cloud-simulation.mjs
@@ -144,7 +144,6 @@ async function example({ canvas, files }) {
 
 export class PointCloudSimulationExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Point Cloud Simulation';
     static FILES = {
         'shader.vert': /* glsl */`
 // Attributes per vertex: position
diff --git a/examples/src/examples/graphics/point-cloud.mjs b/examples/src/examples/graphics/point-cloud.mjs
index b8a6620b6..ba864930b 100644
--- a/examples/src/examples/graphics/point-cloud.mjs
+++ b/examples/src/examples/graphics/point-cloud.mjs
@@ -88,7 +88,6 @@ async function example({ canvas, files, assetPath }) {
 
 export class PointCloudExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Point Cloud';
     static FILES = {
         'shader.vert': /* glsl */`
 // Attributes per vertex: position
diff --git a/examples/src/examples/graphics/portal.mjs b/examples/src/examples/graphics/portal.mjs
index 052a708d3..b6c1cffd9 100644
--- a/examples/src/examples/graphics/portal.mjs
+++ b/examples/src/examples/graphics/portal.mjs
@@ -231,7 +231,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class PortalExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Portal';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/post-effects.mjs b/examples/src/examples/graphics/post-effects.mjs
index 2a5c0154e..77a66e0cc 100644
--- a/examples/src/examples/graphics/post-effects.mjs
+++ b/examples/src/examples/graphics/post-effects.mjs
@@ -433,7 +433,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class PostEffectsExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Post Effects';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/post-processing.mjs b/examples/src/examples/graphics/post-processing.mjs
index 1ae751457..6156c17b2 100644
--- a/examples/src/examples/graphics/post-processing.mjs
+++ b/examples/src/examples/graphics/post-processing.mjs
@@ -180,7 +180,7 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
         // setup skydome
         app.scene.envAtlas = assets.helipad.resource;
         app.scene.skyboxMip = 2;
-        app.scene.exposure = 0.5;
+        app.scene.exposure = 0.3;
 
         // render in HDR mode
         app.scene.toneMapping = pc.TONEMAP_LINEAR;
@@ -194,6 +194,7 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
         const createBox = (x, y, z, sx, sy, sz, r, g, b) => {
             // create material of random color
             const material = new pc.StandardMaterial();
+            material.diffuse = pc.Color.BLACK;
             material.emissive = new pc.Color(r, g, b);
             material.update();
 
@@ -254,6 +255,21 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
         });
         app.root.addChild(screen);
 
+        // add a directional light
+        const light = new pc.Entity();
+        light.addComponent("light", {
+            type: "directional",
+            color: pc.Color.WHITE,
+            intensity: 3,
+            range: 500,
+            shadowDistance: 500,
+            castShadows: true,
+            shadowBias: 0.2,
+            normalOffsetBias: 0.05
+        });
+        app.root.addChild(light);
+        light.setLocalEulerAngles(45, 30, 0);
+
         // a helper function to add a label to the screen
         const addLabel = (name, text, x, y, layer) => {
             const label = new pc.Entity(name);
@@ -289,7 +305,7 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
             // create a multi-sampled HDR render target to render the scene into
             const format = app.graphicsDevice.getRenderableHdrFormat() || pc.PIXELFORMAT_RGBA8;
             const sceneTexture = new pc.Texture(device, {
-                name: 'RTTexture',
+                name: 'SceneTexture',
                 width: 4,
                 height: 4,
                 format: format,
@@ -418,7 +434,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class PostprocessingExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Post Processing XX';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/reflection-box.mjs b/examples/src/examples/graphics/reflection-box.mjs
index 15fca810f..0feb9bfe6 100644
--- a/examples/src/examples/graphics/reflection-box.mjs
+++ b/examples/src/examples/graphics/reflection-box.mjs
@@ -429,7 +429,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 class ReflectionBoxExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Reflection Box';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/reflection-cubemap.mjs b/examples/src/examples/graphics/reflection-cubemap.mjs
index 1c9cbf40f..bc124590a 100644
--- a/examples/src/examples/graphics/reflection-cubemap.mjs
+++ b/examples/src/examples/graphics/reflection-cubemap.mjs
@@ -313,7 +313,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class ReflectionCubemapExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Reflection Cubemap';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/reflection-planar.mjs b/examples/src/examples/graphics/reflection-planar.mjs
index 6869543c8..bd5d16991 100644
--- a/examples/src/examples/graphics/reflection-planar.mjs
+++ b/examples/src/examples/graphics/reflection-planar.mjs
@@ -205,7 +205,6 @@ async function example({ canvas, deviceType, files, scriptsPath, assetPath, glsl
 
 export class ReflectionPlanarExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Reflection Planar';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/render-asset.mjs b/examples/src/examples/graphics/render-asset.mjs
index 07885d2dc..f2ff00dab 100644
--- a/examples/src/examples/graphics/render-asset.mjs
+++ b/examples/src/examples/graphics/render-asset.mjs
@@ -112,7 +112,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class RenderAssetExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Render Asset';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/render-pass.mjs b/examples/src/examples/graphics/render-pass.mjs
index b76f41e9c..624108de9 100644
--- a/examples/src/examples/graphics/render-pass.mjs
+++ b/examples/src/examples/graphics/render-pass.mjs
@@ -177,7 +177,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class RenderPassExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Render Pass';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/render-to-texture.mjs b/examples/src/examples/graphics/render-to-texture.mjs
index 985d4ea17..2ce7eb50d 100644
--- a/examples/src/examples/graphics/render-to-texture.mjs
+++ b/examples/src/examples/graphics/render-to-texture.mjs
@@ -294,7 +294,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class RenderToTextureExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Render to Texture';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/shader-burn.mjs b/examples/src/examples/graphics/shader-burn.mjs
index ea3c23e6a..37f10aa50 100644
--- a/examples/src/examples/graphics/shader-burn.mjs
+++ b/examples/src/examples/graphics/shader-burn.mjs
@@ -141,7 +141,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 export class ShaderBurnExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shader Burn';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/shader-compile.mjs b/examples/src/examples/graphics/shader-compile.mjs
index 915c3af14..55430b4da 100644
--- a/examples/src/examples/graphics/shader-compile.mjs
+++ b/examples/src/examples/graphics/shader-compile.mjs
@@ -205,7 +205,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 export class ShaderCompileExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shader Compile';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/shader-toon.mjs b/examples/src/examples/graphics/shader-toon.mjs
index a24377082..012f9c074 100644
--- a/examples/src/examples/graphics/shader-toon.mjs
+++ b/examples/src/examples/graphics/shader-toon.mjs
@@ -132,7 +132,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 export class ShaderToonExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shader Toon';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/shader-wobble.mjs b/examples/src/examples/graphics/shader-wobble.mjs
index 8ac6149ee..b8f417e5b 100644
--- a/examples/src/examples/graphics/shader-wobble.mjs
+++ b/examples/src/examples/graphics/shader-wobble.mjs
@@ -133,7 +133,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 export class ShaderWobbleExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shader Wobble';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/shadow-cascades.mjs b/examples/src/examples/graphics/shadow-cascades.mjs
index e1af83c1f..ca3009d14 100644
--- a/examples/src/examples/graphics/shadow-cascades.mjs
+++ b/examples/src/examples/graphics/shadow-cascades.mjs
@@ -293,7 +293,6 @@ async function example({ canvas, deviceType, data, assetPath, scriptsPath, glsla
 
 export class ShadowCascadesExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shadow Cascades';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/graphics/shapes.mjs b/examples/src/examples/graphics/shapes.mjs
index 082848c72..7e8778bf0 100644
--- a/examples/src/examples/graphics/shapes.mjs
+++ b/examples/src/examples/graphics/shapes.mjs
@@ -97,7 +97,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 export class ShapesExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Shapes';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/texture-array.mjs b/examples/src/examples/graphics/texture-array.mjs
index 888738cc3..8d27347ac 100644
--- a/examples/src/examples/graphics/texture-array.mjs
+++ b/examples/src/examples/graphics/texture-array.mjs
@@ -264,7 +264,6 @@ async function example({ canvas, deviceType, data, files, assetPath, scriptsPath
 
 export class TextureArrayExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Texture Array';
     static WEBGPU_ENABLED = false;
 
     static FILES = {
diff --git a/examples/src/examples/graphics/texture-basis.mjs b/examples/src/examples/graphics/texture-basis.mjs
index 94b3cfa0d..70cd02f4e 100644
--- a/examples/src/examples/graphics/texture-basis.mjs
+++ b/examples/src/examples/graphics/texture-basis.mjs
@@ -140,7 +140,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class TextureBasisExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Texture Basis';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/graphics/transform-feedback.mjs b/examples/src/examples/graphics/transform-feedback.mjs
index 2db9adf95..a7887e598 100644
--- a/examples/src/examples/graphics/transform-feedback.mjs
+++ b/examples/src/examples/graphics/transform-feedback.mjs
@@ -162,7 +162,6 @@ async function example({ canvas, files, assetPath }) {
 
 export class TransformFeedbackExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Transform Feedback';
     static FILES = {
         'shaderFeedback.vert': /* glsl */`
 // vertex shader used to move particles during transform-feedback simulation step
diff --git a/examples/src/examples/graphics/video-texture.mjs b/examples/src/examples/graphics/video-texture.mjs
index 73e5efdff..cabc29679 100644
--- a/examples/src/examples/graphics/video-texture.mjs
+++ b/examples/src/examples/graphics/video-texture.mjs
@@ -128,6 +128,5 @@ async function example({ canvas, assetPath }) {
 
 export class VideoTextureExample {
     static CATEGORY = 'Graphics';
-    static NAME = 'Video Texture';
     static example = example;
 }
diff --git a/examples/src/examples/input/gamepad.mjs b/examples/src/examples/input/gamepad.mjs
index 86aaf4be7..f62d7fa4d 100644
--- a/examples/src/examples/input/gamepad.mjs
+++ b/examples/src/examples/input/gamepad.mjs
@@ -94,7 +94,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class GamepadExample {
     static CATEGORY = 'Input';
-    static NAME = 'Gamepad';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/input/keyboard.mjs b/examples/src/examples/input/keyboard.mjs
index 33394d54c..82cfabac5 100644
--- a/examples/src/examples/input/keyboard.mjs
+++ b/examples/src/examples/input/keyboard.mjs
@@ -86,7 +86,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class KeyboardExample {
     static CATEGORY = 'Input';
-    static NAME = 'Keyboard';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/input/mouse.mjs b/examples/src/examples/input/mouse.mjs
index 6341a855a..4ddffdb08 100644
--- a/examples/src/examples/input/mouse.mjs
+++ b/examples/src/examples/input/mouse.mjs
@@ -88,7 +88,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class MouseExample {
     static CATEGORY = 'Input';
-    static NAME = 'Mouse';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/loaders/draco-glb.mjs b/examples/src/examples/loaders/draco-glb.mjs
index 68401277c..5bb28517c 100644
--- a/examples/src/examples/loaders/draco-glb.mjs
+++ b/examples/src/examples/loaders/draco-glb.mjs
@@ -97,7 +97,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class DracoGlbExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'Draco GLB';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/loaders/glb.mjs b/examples/src/examples/loaders/glb.mjs
index 15b97736d..360575b14 100644
--- a/examples/src/examples/loaders/glb.mjs
+++ b/examples/src/examples/loaders/glb.mjs
@@ -114,7 +114,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class GlbExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'GLB';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/loaders/gltf-export.mjs b/examples/src/examples/loaders/gltf-export.mjs
index 8a3901616..82e304299 100644
--- a/examples/src/examples/loaders/gltf-export.mjs
+++ b/examples/src/examples/loaders/gltf-export.mjs
@@ -174,7 +174,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class GltfExportExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'GLTF Export';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/loaders/loaders-gl.mjs b/examples/src/examples/loaders/loaders-gl.mjs
index 55ae965ca..a07741b5e 100644
--- a/examples/src/examples/loaders/loaders-gl.mjs
+++ b/examples/src/examples/loaders/loaders-gl.mjs
@@ -164,7 +164,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 class LoadersGlExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'Loaders.gl';
     static FILES = {
         'shader.vert': vshader,
         'shader.frag': fshader,
diff --git a/examples/src/examples/loaders/obj.mjs b/examples/src/examples/loaders/obj.mjs
index 6c32d6573..884963c3b 100644
--- a/examples/src/examples/loaders/obj.mjs
+++ b/examples/src/examples/loaders/obj.mjs
@@ -83,6 +83,5 @@ async function example({ canvas, assetPath, scriptsPath }) {
 
 export class ObjExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'OBJ';
     static example = example;
 }
diff --git a/examples/src/examples/loaders/splat-many.mjs b/examples/src/examples/loaders/splat-many.mjs
index 0a6eb6b8b..fa0240215 100644
--- a/examples/src/examples/loaders/splat-many.mjs
+++ b/examples/src/examples/loaders/splat-many.mjs
@@ -124,7 +124,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 export class SplatManyExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'Splat Many';
     static example = example;
 
     static FILES = {
diff --git a/examples/src/examples/loaders/splat.mjs b/examples/src/examples/loaders/splat.mjs
index fc2e80bc5..a0f6eaca6 100644
--- a/examples/src/examples/loaders/splat.mjs
+++ b/examples/src/examples/loaders/splat.mjs
@@ -106,6 +106,5 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 export class SplatExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'Splat';
     static example = example;
 }
diff --git a/examples/src/examples/loaders/usdz-export.mjs b/examples/src/examples/loaders/usdz-export.mjs
index ed9195c04..30fbec26f 100644
--- a/examples/src/examples/loaders/usdz-export.mjs
+++ b/examples/src/examples/loaders/usdz-export.mjs
@@ -127,7 +127,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath,
 
 export class UsdzExportExample {
     static CATEGORY = 'Loaders';
-    static NAME = 'USDZ Export';
     static WEBGPU_ENABLED = true;
     static controls = controls;
     static example = example;
diff --git a/examples/src/examples/misc/hello-world.mjs b/examples/src/examples/misc/hello-world.mjs
index bf7a2decd..cbb3a66c2 100644
--- a/examples/src/examples/misc/hello-world.mjs
+++ b/examples/src/examples/misc/hello-world.mjs
@@ -74,7 +74,6 @@ async function example({ canvas, deviceType, glslangPath, twgslPath }) {
 
 class HelloWorldExample {
     static CATEGORY = 'Misc';
-    static NAME = 'Hello World';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/misc/mini-stats.mjs b/examples/src/examples/misc/mini-stats.mjs
index e69e67c84..e9370bd17 100644
--- a/examples/src/examples/misc/mini-stats.mjs
+++ b/examples/src/examples/misc/mini-stats.mjs
@@ -244,7 +244,6 @@ async function example({ canvas, pcx }) {
 
 class MiniStatsExample {
     static CATEGORY = 'Misc';
-    static NAME = 'Mini Stats';
     static WEBGPU_ENABLED = true;
     static ENGINE = 'PERFORMANCE';
     static MINISTATS = true;
diff --git a/examples/src/examples/misc/spineboy.mjs b/examples/src/examples/misc/spineboy.mjs
index 3418e105e..648f46d0b 100644
--- a/examples/src/examples/misc/spineboy.mjs
+++ b/examples/src/examples/misc/spineboy.mjs
@@ -107,7 +107,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class SpineboyExample {
     static CATEGORY = 'Misc';
-    static NAME = 'Spineboy';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/physics/compound-collision.mjs b/examples/src/examples/physics/compound-collision.mjs
index 8546c1768..a871bad18 100644
--- a/examples/src/examples/physics/compound-collision.mjs
+++ b/examples/src/examples/physics/compound-collision.mjs
@@ -420,7 +420,6 @@ async function example({ canvas, deviceType, ammoPath, glslangPath, twgslPath })
 
 class CompoundCollisionExample {
     static CATEGORY = 'Physics';
-    static NAME = 'Compound Collision';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/physics/falling-shapes.mjs b/examples/src/examples/physics/falling-shapes.mjs
index f5a3c577d..54871a5fd 100644
--- a/examples/src/examples/physics/falling-shapes.mjs
+++ b/examples/src/examples/physics/falling-shapes.mjs
@@ -275,7 +275,6 @@ async function example({ canvas, deviceType, assetPath, ammoPath, glslangPath, t
 
 class FallingShapesExample {
     static CATEGORY = 'Physics';
-    static NAME = 'Falling Shapes';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/physics/offset-collision.mjs b/examples/src/examples/physics/offset-collision.mjs
index 104692673..9a179a8ec 100644
--- a/examples/src/examples/physics/offset-collision.mjs
+++ b/examples/src/examples/physics/offset-collision.mjs
@@ -273,7 +273,6 @@ async function example({ canvas, deviceType, assetPath, ammoPath, glslangPath, t
 
 class OffsetCollisionExample {
     static CATEGORY = 'Physics';
-    static NAME = 'Offset Collision';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/physics/raycast.mjs b/examples/src/examples/physics/raycast.mjs
index 529fe6b22..7119659d6 100644
--- a/examples/src/examples/physics/raycast.mjs
+++ b/examples/src/examples/physics/raycast.mjs
@@ -236,7 +236,6 @@ async function example({ canvas, deviceType, assetPath, ammoPath, glslangPath, t
 
 class RaycastExample {
     static CATEGORY = 'Physics';
-    static NAME = 'Raycast';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/physics/vehicle.mjs b/examples/src/examples/physics/vehicle.mjs
index bc73a3b30..d79a3178d 100644
--- a/examples/src/examples/physics/vehicle.mjs
+++ b/examples/src/examples/physics/vehicle.mjs
@@ -237,7 +237,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, ammoPath, g
 
 class VehicleExample {
     static CATEGORY = 'Physics';
-    static NAME = 'Vehicle';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/sound/positional.mjs b/examples/src/examples/sound/positional.mjs
index 1a62f8960..43e34fe3a 100644
--- a/examples/src/examples/sound/positional.mjs
+++ b/examples/src/examples/sound/positional.mjs
@@ -117,7 +117,6 @@ async function example({ canvas, assetPath }) {
 
 class PositionalExample {
     static CATEGORY = 'Sound';
-    static NAME = 'Positional';
     static example = example;
 }
 
diff --git a/examples/src/examples/user-interface/button-basic.mjs b/examples/src/examples/user-interface/button-basic.mjs
index a1b37f994..1c92f5476 100644
--- a/examples/src/examples/user-interface/button-basic.mjs
+++ b/examples/src/examples/user-interface/button-basic.mjs
@@ -118,7 +118,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class ButtonBasicExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Button Basic';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/button-sprite.mjs b/examples/src/examples/user-interface/button-sprite.mjs
index 738d70ee5..b307f5cf1 100644
--- a/examples/src/examples/user-interface/button-sprite.mjs
+++ b/examples/src/examples/user-interface/button-sprite.mjs
@@ -189,7 +189,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class ButtonSpriteExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Button Sprite';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/custom-shader.mjs b/examples/src/examples/user-interface/custom-shader.mjs
index f278127c5..04fc1abf0 100644
--- a/examples/src/examples/user-interface/custom-shader.mjs
+++ b/examples/src/examples/user-interface/custom-shader.mjs
@@ -120,7 +120,6 @@ async function example({ canvas, deviceType, files, assetPath, glslangPath, twgs
 
 class CustomShaderExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Custom Shader';
     static WEBGPU_ENABLED = true;
 
     static FILES = {
diff --git a/examples/src/examples/user-interface/layout-group.mjs b/examples/src/examples/user-interface/layout-group.mjs
index 3bb2f5b54..4588d118e 100644
--- a/examples/src/examples/user-interface/layout-group.mjs
+++ b/examples/src/examples/user-interface/layout-group.mjs
@@ -145,7 +145,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class LayoutGroupExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Layout Group';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/particle-system.mjs b/examples/src/examples/user-interface/particle-system.mjs
index ae47fcff2..9468e961d 100644
--- a/examples/src/examples/user-interface/particle-system.mjs
+++ b/examples/src/examples/user-interface/particle-system.mjs
@@ -181,7 +181,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class ParticleSystemExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Particle System';
     static example = example;
 }
 
diff --git a/examples/src/examples/user-interface/scroll-view.mjs b/examples/src/examples/user-interface/scroll-view.mjs
index 87a0408b6..e3a2a1856 100644
--- a/examples/src/examples/user-interface/scroll-view.mjs
+++ b/examples/src/examples/user-interface/scroll-view.mjs
@@ -255,7 +255,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class ScrollViewExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Scroll View';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/text-auto-font-size.mjs b/examples/src/examples/user-interface/text-auto-font-size.mjs
index 94eb1b5c2..a7bc155f1 100644
--- a/examples/src/examples/user-interface/text-auto-font-size.mjs
+++ b/examples/src/examples/user-interface/text-auto-font-size.mjs
@@ -128,7 +128,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class TextAutoFontSizeExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Text Auto Font Size';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/text-emojis.mjs b/examples/src/examples/user-interface/text-emojis.mjs
index aeb5aec48..e073fe600 100644
--- a/examples/src/examples/user-interface/text-emojis.mjs
+++ b/examples/src/examples/user-interface/text-emojis.mjs
@@ -189,7 +189,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class TextEmojisExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Text Emojis';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/text-localization.mjs b/examples/src/examples/user-interface/text-localization.mjs
index dbaae70b9..2775cddbe 100644
--- a/examples/src/examples/user-interface/text-localization.mjs
+++ b/examples/src/examples/user-interface/text-localization.mjs
@@ -182,7 +182,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class TextLocalizationExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Text Localization';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/text-typewriter.mjs b/examples/src/examples/user-interface/text-typewriter.mjs
index 408818978..a7c349718 100644
--- a/examples/src/examples/user-interface/text-typewriter.mjs
+++ b/examples/src/examples/user-interface/text-typewriter.mjs
@@ -113,7 +113,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class TextTypewriterExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Text Typewriter';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/text.mjs b/examples/src/examples/user-interface/text.mjs
index 817962659..8123fccdd 100644
--- a/examples/src/examples/user-interface/text.mjs
+++ b/examples/src/examples/user-interface/text.mjs
@@ -151,7 +151,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class TextExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'Text';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/world-to-screen.mjs b/examples/src/examples/user-interface/world-to-screen.mjs
index d7b7786a0..eff4ae720 100644
--- a/examples/src/examples/user-interface/world-to-screen.mjs
+++ b/examples/src/examples/user-interface/world-to-screen.mjs
@@ -247,7 +247,6 @@ async function example({ canvas, deviceType, assetPath, glslangPath, twgslPath }
 
 class WorldToScreenExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'World to Screen';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/user-interface/world-ui.mjs b/examples/src/examples/user-interface/world-ui.mjs
index 558db2d29..8adbabab8 100644
--- a/examples/src/examples/user-interface/world-ui.mjs
+++ b/examples/src/examples/user-interface/world-ui.mjs
@@ -186,7 +186,6 @@ async function example({ canvas, deviceType, assetPath, scriptsPath, glslangPath
 
 class WorldUiExample {
     static CATEGORY = 'User Interface';
-    static NAME = 'World UI';
     static WEBGPU_ENABLED = true;
     static example = example;
 }
diff --git a/examples/src/examples/xr/ar-anchors-persistence.mjs b/examples/src/examples/xr/ar-anchors-persistence.mjs
index d6a6e5246..bbfd4a248 100644
--- a/examples/src/examples/xr/ar-anchors-persistence.mjs
+++ b/examples/src/examples/xr/ar-anchors-persistence.mjs
@@ -283,7 +283,6 @@ async function example({ canvas }) {
 
 class ArAnchorsPersistentExample {
     static CATEGORY = 'XR';
-    static NAME = 'AR Anchors Persistence';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/ar-basic.mjs b/examples/src/examples/xr/ar-basic.mjs
index ad68776a5..cc8642dda 100644
--- a/examples/src/examples/xr/ar-basic.mjs
+++ b/examples/src/examples/xr/ar-basic.mjs
@@ -142,7 +142,6 @@ async function example({ canvas }) {
 
 class ArBasicExample {
     static CATEGORY = 'XR';
-    static NAME = 'AR Basic';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/ar-plane-detection.mjs b/examples/src/examples/xr/ar-plane-detection.mjs
index 8dd229fa5..47203b0f3 100644
--- a/examples/src/examples/xr/ar-plane-detection.mjs
+++ b/examples/src/examples/xr/ar-plane-detection.mjs
@@ -304,7 +304,6 @@ async function example({ canvas }) {
 
 class ArPlanesDetectionExample {
     static CATEGORY = 'XR';
-    static NAME = 'AR Plane Detection';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/vr-basic.mjs b/examples/src/examples/xr/vr-basic.mjs
index 6b71fdec6..8aa5ffead 100644
--- a/examples/src/examples/xr/vr-basic.mjs
+++ b/examples/src/examples/xr/vr-basic.mjs
@@ -140,7 +140,6 @@ async function example({ canvas }) {
 
 class VrBasicExample {
     static CATEGORY = 'XR';
-    static NAME = 'VR Basic';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/vr-controllers.mjs b/examples/src/examples/xr/vr-controllers.mjs
index cf55abbc0..dedb29039 100644
--- a/examples/src/examples/xr/vr-controllers.mjs
+++ b/examples/src/examples/xr/vr-controllers.mjs
@@ -182,7 +182,6 @@ async function example({ canvas, assetPath }) {
 
 class VrControllersExample {
     static CATEGORY = 'XR';
-    static NAME = 'VR Controllers';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/vr-hands.mjs b/examples/src/examples/xr/vr-hands.mjs
index ad256dc0a..63cc193db 100644
--- a/examples/src/examples/xr/vr-hands.mjs
+++ b/examples/src/examples/xr/vr-hands.mjs
@@ -228,7 +228,6 @@ async function example({ canvas }) {
 
 class VrHandsExample {
     static CATEGORY = 'XR';
-    static NAME = 'VR Hands';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/vr-movement.mjs b/examples/src/examples/xr/vr-movement.mjs
index 76ed3c98b..b718f16bf 100644
--- a/examples/src/examples/xr/vr-movement.mjs
+++ b/examples/src/examples/xr/vr-movement.mjs
@@ -260,7 +260,6 @@ async function example({ canvas }) {
 
 class VrMovementExample {
     static CATEGORY = 'XR';
-    static NAME = 'VR Movement';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/xr-picking.mjs b/examples/src/examples/xr/xr-picking.mjs
index 417a6f5d3..ee81643aa 100644
--- a/examples/src/examples/xr/xr-picking.mjs
+++ b/examples/src/examples/xr/xr-picking.mjs
@@ -184,7 +184,6 @@ async function example({ canvas }) {
 
 class XrPickingExample {
     static CATEGORY = 'XR';
-    static NAME = 'XR Picking';
     static example = example;
 }
 
diff --git a/src/core/event-handle.js b/src/core/event-handle.js
index c887485a5..1f5ce4068 100644
--- a/src/core/event-handle.js
+++ b/src/core/event-handle.js
@@ -90,7 +90,7 @@ class EventHandle {
     }
 
     once(name, callback, scope = this) {
-        Debug.deprecated('Using chaning with EventHandler.once is deprecated, subscribe to an event from EventHandler directly instead.');
+        Debug.deprecated('Using chaining with EventHandler.once is deprecated, subscribe to an event from EventHandler directly instead.');
         return this.handler._addCallback(name, callback, scope, true);
     }
 
diff --git a/src/framework/entity.js b/src/framework/entity.js
index 7855b535a..e2948d60d 100644
--- a/src/framework/entity.js
+++ b/src/framework/entity.js
@@ -379,6 +379,36 @@ class Entity extends GraphNode {
         });
     }
 
+    /**
+     * Search the entity and all of its descendants for the first script instance of specified type.
+     *
+     * @param {string|Class<import('./script/script-type.js').ScriptType>} nameOrType - The name or type of {@link ScriptType}.
+     * @returns {import('./script/script-type.js').ScriptType|undefined} A script instance of specified type, if the entity or any of its descendants
+     * has one. Returns undefined otherwise.
+     * @example
+     * // Get the first found "playerController" instance in the hierarchy tree that starts with this entity
+     * var controller = entity.findScript("playerController");
+     */
+    findScript(nameOrType) {
+        const entity = this.findOne(node => node.c?.script?.has(nameOrType));
+        return entity?.c.script.get(nameOrType);
+    }
+
+    /**
+     * Search the entity and all of its descendants for all script instances of specified type.
+     *
+     * @param {string|Class<import('./script/script-type.js').ScriptType>} nameOrType - The name or type of {@link ScriptType}.
+     * @returns {import('./script/script-type.js').ScriptType[]} All script instances of specified type in the entity or any of its
+     * descendants. Returns empty array if none found.
+     * @example
+     * // Get all "playerController" instances in the hierarchy tree that starts with this entity
+     * var controllers = entity.findScripts("playerController");
+     */
+    findScripts(nameOrType) {
+        const entities = this.find(node => node.c?.script?.has(nameOrType));
+        return entities.map(entity => entity.c.script.get(nameOrType));
+    }
+
     /**
      * Get the GUID value for this Entity.
      *
diff --git a/src/framework/xr/xr-manager.js b/src/framework/xr/xr-manager.js
index 808c4b52d..5c43ecfd2 100644
--- a/src/framework/xr/xr-manager.js
+++ b/src/framework/xr/xr-manager.js
@@ -238,6 +238,9 @@ class XrManager extends EventHandler {
                 this._deviceAvailabilityCheck();
             });
             this._deviceAvailabilityCheck();
+
+            this.app.graphicsDevice.on('devicelost', this._onDeviceLost, this);
+            this.app.graphicsDevice.on('devicerestored', this._onDeviceRestored, this);
         }
     }
 
@@ -672,23 +675,8 @@ class XrManager extends EventHandler {
         // so we need to calculate this based on devicePixelRatio of the dislay and what
         // we've set this in the graphics device
         Debug.assert(window, 'window is needed to scale the XR framebuffer. Are you running XR headless?');
-        const framebufferScaleFactor = this.app.graphicsDevice.maxPixelRatio / window.devicePixelRatio;
-
-        this._baseLayer = new XRWebGLLayer(session, this.app.graphicsDevice.gl, {
-            alpha: true,
-            depth: true,
-            stencil: true,
-            framebufferScaleFactor: framebufferScaleFactor,
-
-            // request a single-sampled buffer. We allocate multi-sampled buffer internally and resolve to this buffer.
-            antialias: false
-        });
 
-        session.updateRenderState({
-            baseLayer: this._baseLayer,
-            depthNear: this._depthNear,
-            depthFar: this._depthFar
-        });
+        this._createBaseLayer();
 
         // request reference space
         session.requestReferenceSpace(spaceType).then((referenceSpace) => {
@@ -731,6 +719,54 @@ class XrManager extends EventHandler {
         });
     }
 
+    _createBaseLayer() {
+        const framebufferScaleFactor = this.app.graphicsDevice.maxPixelRatio / window.devicePixelRatio;
+
+        this._baseLayer = new XRWebGLLayer(this._session, this.app.graphicsDevice.gl, {
+            alpha: true,
+            depth: true,
+            stencil: true,
+            framebufferScaleFactor: framebufferScaleFactor,
+            antialias: false
+        });
+
+        this._session.updateRenderState({
+            baseLayer: this._baseLayer,
+            depthNear: this._depthNear,
+            depthFar: this._depthFar
+        });
+    }
+
+    /** @private */
+    _onDeviceLost() {
+        if (!this._session)
+            return;
+
+        this._baseLayer = null;
+
+        this._session.updateRenderState({
+            baseLayer: this._baseLayer,
+            depthNear: this._depthNear,
+            depthFar: this._depthFar
+        });
+    }
+
+    /** @private */
+    _onDeviceRestored() {
+        if (!this._session)
+            return;
+
+        setTimeout(() => {
+            this.app.graphicsDevice.gl.makeXRCompatible()
+                .then(() => {
+                    this._createBaseLayer();
+                })
+                .catch((ex) => {
+                    this.fire('error', ex);
+                });
+        }, 0);
+    }
+
     /**
      * @param {*} frame - XRFrame from requestAnimationFrame callback.
      *
diff --git a/src/scene/composition/render-action.js b/src/scene/composition/render-action.js
index 92e04c584..720e17411 100644
--- a/src/scene/composition/render-action.js
+++ b/src/scene/composition/render-action.js
@@ -41,10 +41,6 @@ class RenderAction {
         // true if this is the last render action using this camera
         this.lastCameraUse = false;
 
-        // directional lights that needs to update their shadows for this render action. The array is
-        // filled in during light culling each frame.
-        this.directionalLights = [];
-
         // an array of view bind groups (the number of these corresponds to the number of views when XR is used)
         /** @type {import('../../platform/graphics/bind-group.js').BindGroup[]} */
         this.viewBindGroups = [];
@@ -67,10 +63,6 @@ class RenderAction {
         this.clearDepth = camera?.clearDepthBuffer || layer.clearDepthBuffer;
         this.clearStencil = camera?.clearStencilBuffer || layer.clearStencilBuffer;
     }
-
-    get hasDirectionalShadowLights() {
-        return this.directionalLights.length > 0;
-    }
 }
 
 export { RenderAction };
diff --git a/src/scene/frame-graph.js b/src/scene/frame-graph.js
index 56f84188e..d117acdb5 100644
--- a/src/scene/frame-graph.js
+++ b/src/scene/frame-graph.js
@@ -38,7 +38,6 @@ class FrameGraph {
         }
 
         if (renderPass.enabled) {
-            renderPass.frameUpdate();
             passes.push(renderPass);
         }
 
diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js
index 79191c46b..d32e3e0ca 100644
--- a/src/scene/renderer/forward-renderer.js
+++ b/src/scene/renderer/forward-renderer.js
@@ -781,11 +781,6 @@ class ForwardRenderer extends Renderer {
 
                 const isGrabPass = isDepthLayer && (camera.renderSceneColorMap || camera.renderSceneDepthMap);
 
-                // directional shadows get re-rendered for each camera
-                if (renderAction.hasDirectionalShadowLights && camera) {
-                    this._shadowRendererDirectional.buildFrameGraph(frameGraph, renderAction.directionalLights, camera);
-                }
-
                 // start of block of render actions rendering to the same render target
                 if (newStart) {
                     newStart = false;
@@ -797,10 +792,12 @@ class ForwardRenderer extends Renderer {
                 const nextRenderAction = renderActions[i + 1];
                 const isNextLayerDepth = nextRenderAction ? nextRenderAction.layer.id === LAYERID_DEPTH : false;
                 const isNextLayerGrabPass = isNextLayerDepth && (camera.renderSceneColorMap || camera.renderSceneDepthMap) && !webgl1;
+                const nextNeedDirShadows = nextRenderAction ? (nextRenderAction.firstCameraUse && this.cameraDirShadowLights.has(nextRenderAction.camera.camera)) : false;
 
-                // end of the block using the same render target
+                // end of the block using the same render target if the next render action uses a different render target, or needs directional shadows
+                // rendered before it or similar or needs other pass before it.
                 if (!nextRenderAction || nextRenderAction.renderTarget !== renderTarget ||
-                    nextRenderAction.hasDirectionalShadowLights || isNextLayerGrabPass || isGrabPass) {
+                    nextNeedDirShadows || isNextLayerGrabPass || isGrabPass) {
 
                     // render the render actions in the range
                     const isDepthOnly = isDepthLayer && startIndex === i;
diff --git a/src/scene/renderer/render-pass-render-actions.js b/src/scene/renderer/render-pass-render-actions.js
index 54d9720fb..f473d5bdb 100644
--- a/src/scene/renderer/render-pass-render-actions.js
+++ b/src/scene/renderer/render-pass-render-actions.js
@@ -47,10 +47,12 @@ class RenderPassRenderActions extends RenderPass {
     addRenderAction(renderAction) {
         this.renderActions.push(renderAction);
 
-        // first render action sets up clear params
+        // first render action
         if (this.renderActions.length === 1) {
 
             const camera = renderAction.camera;
+
+            // set up clear params
             this.fullSizeClearRect = camera.camera.fullSizeClearRect;
 
             // only if camera rendering covers the full viewport
@@ -90,6 +92,38 @@ class RenderPassRenderActions extends RenderPass {
         this.addRenderAction(ra);
     }
 
+    frameUpdate() {
+        super.frameUpdate();
+
+        // add directional shadow passes if needed for the cameras used in this render pass
+        const { renderer, renderActions } = this;
+        for (let i = 0; i < renderActions.length; i++) {
+            const renderAction = renderActions[i];
+            const cameraComp = renderAction.camera;
+            const camera = cameraComp.camera;
+
+            // if this camera uses directional shadow lights
+            const shadowDirLights = this.renderer.cameraDirShadowLights.get(camera);
+            if (shadowDirLights) {
+
+                for (let l = 0; l < shadowDirLights.length; l++) {
+                    const light = shadowDirLights[l];
+
+                    // the the shadow map is not already rendered for this light
+                    if (renderer.dirLightShadows.get(light) !== camera) {
+                        renderer.dirLightShadows.set(light, camera);
+
+                        // render the shadow before this render pass
+                        const shadowPass = renderer._shadowRendererDirectional.getLightRenderPass(light, camera);
+                        if (shadowPass) {
+                            this.beforePasses.push(shadowPass);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     before() {
         const { renderActions } = this;
         if (renderActions.length) {
@@ -121,6 +155,9 @@ class RenderPassRenderActions extends RenderPass {
                 ra.camera.onPostRender();
             }
         }
+
+        // remove shadow before-passes
+        this.beforePasses.length = 0;
     }
 
     /**
diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js
index a5c08aa1c..383a9a284 100644
--- a/src/scene/renderer/renderer.js
+++ b/src/scene/renderer/renderer.js
@@ -103,6 +103,23 @@ class Renderer {
      */
     localLights = [];
 
+    /**
+     * A list of unique directional shadow casting lights for each enabled camera. This is generated
+     * each frame during light culling.
+     *
+     * @type {Map<import('../camera.js').Camera, Array<import('../light.js').Light>>}
+     */
+    cameraDirShadowLights = new Map();
+
+    /**
+     * A mapping of a directional light to a camera, for which the shadow is currently valid. This
+     * is cleared each frame, and updated each time a directional light shadow is rendered for a
+     * camera, and allows us to manually schedule shadow passes when a new camera needs a shadow.
+     *
+     * @type {Map<import('../light.js').Light, import('../camera.js').Camera>}
+     */
+    dirLightShadows = new Map();
+
     /**
      * Create a new instance.
      *
@@ -1017,17 +1034,16 @@ class Renderer {
             }
         }
 
-        // shadow casters culling for directional lights
-        const renderActions = comp._renderActions;
-        for (let i = 0; i < renderActions.length; i++) {
-            const renderAction = renderActions[i];
-            renderAction.directionalLights.length = 0;
-            const camera = renderAction.camera.camera;
-
-            // first use of each camera renders directional shadows
-            if (renderAction.firstCameraUse)  {
+        // shadow casters culling for directional lights - start with none and collect lights for cameras
+        this.cameraDirShadowLights.clear();
+        const cameras = comp.cameras;
+        for (let i = 0; i < cameras.length; i++) {
+            const cameraComponent = cameras[i];
+            if (cameraComponent.enabled) {
+                const camera = cameraComponent.camera;
 
                 // get directional lights from all layers of the camera
+                let lightList;
                 const cameraLayers = camera.layers;
                 for (let l = 0; l < cameraLayers.length; l++) {
                     const cameraLayer = comp.getLayerById(cameraLayers[l]);
@@ -1040,7 +1056,9 @@ class Renderer {
                             // unique shadow casting lights
                             if (light.castShadows && !_tempSet.has(light)) {
                                 _tempSet.add(light);
-                                renderAction.directionalLights.push(light);
+
+                                lightList = lightList ?? [];
+                                lightList.push(light);
 
                                 // frustum culling for the directional shadow when rendering the camera
                                 this._shadowRendererDirectional.cull(light, comp, camera);
@@ -1049,6 +1067,10 @@ class Renderer {
                     }
                 }
 
+                if (lightList) {
+                    this.cameraDirShadowLights.set(camera, lightList);
+                }
+
                 _tempSet.clear();
             }
         }
@@ -1273,6 +1295,9 @@ class Renderer {
         this.clustersDebugRendered = false;
 
         this.initViewBindGroupFormat(this.scene.clusteredLightingEnabled);
+
+        // no valid shadows at the start of the frame
+        this.dirLightShadows.clear();
     }
 }
 
diff --git a/src/scene/renderer/shadow-renderer-directional.js b/src/scene/renderer/shadow-renderer-directional.js
index 389449012..25cf7d510 100644
--- a/src/scene/renderer/shadow-renderer-directional.js
+++ b/src/scene/renderer/shadow-renderer-directional.js
@@ -199,50 +199,43 @@ class ShadowRendererDirectional {
         }
     }
 
-    addLightRenderPasses(frameGraph, light, camera) {
-
-        // shadow cascades have more faces rendered within a singe render pass
-        const faceCount = light.numShadowFaces;
-        const shadowUpdateOverrides = light.shadowUpdateOverrides;
-
-        // prepare render targets / cameras for rendering
-        let allCascadesRendering = true;
-        let shadowCamera;
-        for (let face = 0; face < faceCount; face++) {
+    /**
+     * Create a render pass for directional light shadow rendering for a specified camera.
+     *
+     * @param {import('../light.js').Light} light - The directional light.
+     * @param {import('../camera.js').Camera} camera - The camera.
+     * @returns {RenderPassShadowDirectional|null} - The render pass if the shadow rendering is
+     * required, or null otherwise.
+     */
+    getLightRenderPass(light, camera) {
 
-            if (shadowUpdateOverrides?.[face] === SHADOWUPDATE_NONE)
-                allCascadesRendering = false;
+        Debug.assert(light && light._type === LIGHTTYPE_DIRECTIONAL);
 
-            shadowCamera = this.shadowRenderer.prepareFace(light, camera, face);
-        }
+        let renderPass = null;
+        if (this.shadowRenderer.needsShadowRendering(light)) {
 
-        const renderPass = new RenderPassShadowDirectional(this.device, this.shadowRenderer, light, camera, allCascadesRendering);
+            // shadow cascades have more faces rendered within a singe render pass
+            const faceCount = light.numShadowFaces;
+            const shadowUpdateOverrides = light.shadowUpdateOverrides;
 
-        // setup render pass using any of the cameras, they all have the same pass related properties
-        this.shadowRenderer.setupRenderPass(renderPass, shadowCamera, allCascadesRendering);
+            // prepare render targets / cameras for rendering
+            let allCascadesRendering = true;
+            let shadowCamera;
+            for (let face = 0; face < faceCount; face++) {
 
-        frameGraph.addRenderPass(renderPass);
-    }
+                if (shadowUpdateOverrides?.[face] === SHADOWUPDATE_NONE)
+                    allCascadesRendering = false;
 
-    /**
-     * Builds a frame graph for rendering of directional shadows for the render action.
-     *
-     * @param {import('../frame-graph.js').FrameGraph} frameGraph - The frame-graph that is built.
-     * @param {import('../light.js').Light[]} directionalLights - The
-     * directional lights.
-     * @param {import('../../framework/components/camera/component.js').CameraComponent} camera - The camera.
-     */
-    buildFrameGraph(frameGraph, directionalLights, camera) {
+                shadowCamera = this.shadowRenderer.prepareFace(light, camera, face);
+            }
 
-        // create required render passes per light
-        for (let i = 0; i < directionalLights.length; i++) {
-            const light = directionalLights[i];
-            Debug.assert(light && light._type === LIGHTTYPE_DIRECTIONAL);
+            renderPass = new RenderPassShadowDirectional(this.device, this.shadowRenderer, light, camera, allCascadesRendering);
 
-            if (this.shadowRenderer.needsShadowRendering(light)) {
-                this.addLightRenderPasses(frameGraph, light, camera.camera);
-            }
+            // setup render pass using any of the cameras, they all have the same pass related properties
+            this.shadowRenderer.setupRenderPass(renderPass, shadowCamera, allCascadesRendering);
         }
+
+        return renderPass;
     }
 }
 
diff --git a/utils/types-fixup.mjs b/utils/types-fixup.mjs
index c078be9cd..be5dff3c1 100644
--- a/utils/types-fixup.mjs
+++ b/utils/types-fixup.mjs
@@ -4,6 +4,7 @@ import fs from 'fs';
 const regex = /Class<(.*?)>/g;
 const paths = [
     './types/framework/components/script/component.d.ts',
+    './types/framework/entity.d.ts',
     './types/framework/script/script-attributes.d.ts',
     './types/framework/script/script-registry.d.ts',
     './types/framework/script/script.d.ts'
@@ -16,6 +17,10 @@ paths.forEach((path, index) => {
     if (index === 0) {
         dts += `
 import { ScriptType } from '../../script/script-type.js';
+`;
+    } else if (index === 1) {
+        dts += `
+import { ScriptType } from './script/script-type.js';
 `;
     } else {
         dts += `


Test patch:
diff --git a/examples/src/examples/xr/ar-hit-test-anchors.mjs b/examples/src/examples/xr/ar-hit-test-anchors.mjs
index c6a266b2d..ff6c57d93 100644
--- a/examples/src/examples/xr/ar-hit-test-anchors.mjs
+++ b/examples/src/examples/xr/ar-hit-test-anchors.mjs
@@ -251,7 +251,6 @@ async function example({ canvas }) {
 
 class ArHitTestAnchorsExample {
     static CATEGORY = 'XR';
-    static NAME = 'AR Hit Test Anchors';
     static example = example;
 }
 
diff --git a/examples/src/examples/xr/ar-hit-test.mjs b/examples/src/examples/xr/ar-hit-test.mjs
index d4ab73ef9..c0f8c10e7 100644
--- a/examples/src/examples/xr/ar-hit-test.mjs
+++ b/examples/src/examples/xr/ar-hit-test.mjs
@@ -183,7 +183,6 @@ async function example({ canvas }) {
 
 class ArHitTestExample {
     static CATEGORY = 'XR';
-    static NAME = 'AR Hit Test';
     static example = example;
 }
 
diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs
index 33fd834c4..4bbd61bc0 100644
--- a/test/framework/entity.test.mjs
+++ b/test/framework/entity.test.mjs
@@ -675,6 +675,151 @@ describe('Entity', function () {
 
     });
 
+    describe('#findScript', function () {
+
+        it('finds script on single entity', function () {
+            const MyScript = createScript('myScript');
+            const e = new Entity();
+            e.addComponent('script');
+            e.script.create('myScript');
+            const script = e.findScript('myScript');
+            expect(script).to.be.an.instanceof(MyScript);
+        });
+
+        it('returns undefined when script is not found', function () {
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            child.addComponent('script');
+            const script = root.findScript('myScript');
+            expect(script).to.be.undefined;
+        });
+
+        it('returns undefined when script component is not found', function () {
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            const script = root.findScript('myScript');
+            expect(script).to.be.undefined;
+        });
+
+        it('finds script on child entity', function () {
+            const MyScript = createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            child.addComponent('script');
+            child.script.create('myScript');
+            const script = root.findScript('myScript');
+            expect(script).to.be.an.instanceof(MyScript);
+        });
+
+        it('finds script on grandchild entity', function () {
+            const MyScript = createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            const grandchild = new Entity();
+            root.addChild(child);
+            child.addChild(grandchild);
+            grandchild.addComponent('script');
+            grandchild.script.create('myScript');
+            const script = root.findScript('myScript');
+            expect(script).to.be.an.instanceof(MyScript);
+        });
+
+        it('does not find script on parent entity', function () {
+            createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            root.addComponent('script');
+            root.script.create('myScript');
+            const script = child.findScript('myScript');
+            expect(script).to.be.undefined;
+        });
+
+    });
+
+    describe('#findScripts', function () {
+
+        it('finds scripts on single entity', function () {
+            const MyScript = createScript('myScript');
+            const e = new Entity();
+            e.addComponent('script');
+            e.script.create('myScript');
+            const scripts = e.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(1);
+            expect(scripts[0]).to.be.an.instanceof(MyScript);
+        });
+
+        it('returns empty array when no scripts are found', function () {
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            child.addComponent('script');
+            const scripts = root.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(0);
+        });
+
+        it('returns empty array when no script component are found', function () {
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            const scripts = root.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(0);
+        });
+
+        it('finds scripts on child entity', function () {
+            const MyScript = createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            child.addComponent('script');
+            child.script.create('myScript');
+            const scripts = root.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(1);
+            expect(scripts[0]).to.be.an.instanceof(MyScript);
+        });
+
+        it('finds scripts on 3 entity hierarchy', function () {
+            const MyScript = createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            const grandchild = new Entity();
+            root.addChild(child);
+            child.addChild(grandchild);
+            root.addComponent('script');
+            root.script.create('myScript');
+            child.addComponent('script');
+            child.script.create('myScript');
+            grandchild.addComponent('script');
+            grandchild.script.create('myScript');
+            const scripts = root.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(3);
+            expect(scripts[0]).to.be.an.instanceof(MyScript);
+            expect(scripts[1]).to.be.an.instanceof(MyScript);
+            expect(scripts[2]).to.be.an.instanceof(MyScript);
+        });
+
+        it('does not find scripts on parent entity', function () {
+            createScript('myScript');
+            const root = new Entity();
+            const child = new Entity();
+            root.addChild(child);
+            root.addComponent('script');
+            root.script.create('myScript');
+            const scripts = child.findScripts('myScript');
+            expect(scripts).to.be.an('array');
+            expect(scripts.length).to.equal(0);
+        });
+
+    });
+
     describe('#removeComponent', function () {
 
         it('removes a component from the entity', function () {