Repository: playcanvas/engine
Pull request #6354 (2024-05-10 17:00:27)

Fix buildUrl relative path support
`buildUrl` never worked with relative paths as it would always replace the path in the url, instead of adding to it.
Using the other mode of the URL constructor we can use it to always give us a correct url:
```
  `new URL('glslang/glslang.js', 'https://example.com/foo/bar.html')` is `https://example.com/foo/glslang/glslang.js`
  `new URL('/glslang/glslang.js', 'https://example.com/foo/bar.html')` is `https://example.com/glslang/glslang.js`
  `new URL('glslang/glslang.js', 'https://example.com/')` is `https://example.com/glslang/glslang.js`
  `new URL('/glslang/glslang.js', 'https://example.com/')` is `https://example.com/glslang/glslang.js`
```
This bug currently prevents Playcanvas Editor exports from being run on a non root path with WebGPU. The editor export will pass just `glslang.js` as path.
So on a url like https://94176748-9ef8-42c9-a44e-a95b70ec5680.poki-gdn.com/7a8f1fc6-7162-473a-b1dc-4479ed5d60cf/index.html 
the old code would turn it into https://94176748-9ef8-42c9-a44e-a95b70ec5680.poki-gdn.com/glslang.js 
while the actual paths should be https://94176748-9ef8-42c9-a44e-a95b70ec5680.poki-gdn.com/7a8f1fc6-7162-473a-b1dc-4479ed5d60cf/glslang.js

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/src/examples/compute/particles.example.mjs b/examples/src/examples/compute/particles.example.mjs
index deb05191a..ddb2175f7 100644
--- a/examples/src/examples/compute/particles.example.mjs
+++ b/examples/src/examples/compute/particles.example.mjs
@@ -220,11 +220,6 @@ assetListLoader.load(() => {
             new pc.UniformFormat('matrix_model', pc.UNIFORMTYPE_MAT4)
         ]),
         meshBindGroupFormat: new pc.BindGroupFormat(app.graphicsDevice, [
-            // uniforms
-            new pc.BindUniformBufferFormat(
-                pc.UNIFORM_BUFFER_DEFAULT_SLOT_NAME,
-                pc.SHADERSTAGE_VERTEX | pc.SHADERSTAGE_FRAGMENT
-            ),
             // particle storage buffer in read-only mode
             new pc.BindStorageBufferFormat('particles', pc.SHADERSTAGE_VERTEX | pc.SHADERSTAGE_FRAGMENT, true)
         ])
@@ -232,6 +227,7 @@ assetListLoader.load(() => {
 
     // material to render the particles
     const material = new pc.Material();
+    material.name = 'ParticleRenderingMaterial';
     material.shader = new pc.Shader(app.graphicsDevice, shaderDefinition);
 
     // index buffer - two triangles (6 indices) per particle using 4 vertices
@@ -254,7 +250,7 @@ assetListLoader.load(() => {
     const meshInstance = new pc.MeshInstance(mesh, material);
     meshInstance.cull = false; // disable culling as we did not supply custom aabb for the mesh instance
 
-    const entity = new pc.Entity();
+    const entity = new pc.Entity('ParticleRenderingEntity');
     entity.addComponent('render', {
         meshInstances: [meshInstance]
     });
diff --git a/examples/src/examples/compute/particles.shader-rendering.wgsl b/examples/src/examples/compute/particles.shader-rendering.wgsl
index 4f6da1619..36c564279 100644
--- a/examples/src/examples/compute/particles.shader-rendering.wgsl
+++ b/examples/src/examples/compute/particles.shader-rendering.wgsl
@@ -8,9 +8,9 @@ struct ub_view {
     matrix_viewProjection : mat4x4f
 }
 
-@group(0) @binding(0) var<uniform> uvMesh : ub_mesh;
-@group(0) @binding(1) var<storage, read> particles: array<Particle>;
-@group(1) @binding(0) var<uniform> ubView : ub_view;
+@group(2) @binding(0) var<uniform> ubMesh : ub_mesh;
+@group(1) @binding(0) var<storage, read> particles: array<Particle>;
+@group(0) @binding(0) var<uniform> ubView : ub_view;
 
 // quad vertices - used to expand the particles into quads
 var<private> pos : array<vec2f, 4> = array<vec2f, 4>(
diff --git a/examples/src/examples/graphics/wgsl-shader.example.mjs b/examples/src/examples/graphics/wgsl-shader.example.mjs
index 9b23bb66b..76162178a 100644
--- a/examples/src/examples/graphics/wgsl-shader.example.mjs
+++ b/examples/src/examples/graphics/wgsl-shader.example.mjs
@@ -56,12 +56,7 @@ const shaderDefinition = {
         new pc.UniformFormat('matrix_model', pc.UNIFORMTYPE_MAT4),
         new pc.UniformFormat('amount', pc.UNIFORMTYPE_FLOAT)
     ]),
-    meshBindGroupFormat: new pc.BindGroupFormat(app.graphicsDevice, [
-        new pc.BindUniformBufferFormat(
-            pc.UNIFORM_BUFFER_DEFAULT_SLOT_NAME,
-            pc.SHADERSTAGE_VERTEX | pc.SHADERSTAGE_FRAGMENT
-        )
-    ])
+    meshBindGroupFormat: new pc.BindGroupFormat(app.graphicsDevice, [])
 };
 const shader = new pc.Shader(app.graphicsDevice, shaderDefinition);
 
diff --git a/examples/src/examples/graphics/wgsl-shader.shader.wgsl b/examples/src/examples/graphics/wgsl-shader.shader.wgsl
index d15612a3a..63953a6c3 100644
--- a/examples/src/examples/graphics/wgsl-shader.shader.wgsl
+++ b/examples/src/examples/graphics/wgsl-shader.shader.wgsl
@@ -12,8 +12,8 @@ struct VertexOutput {
     @location(0) fragPosition: vec4f,
 }
 
-@group(0) @binding(0) var<uniform> uvMesh : ub_mesh;
-@group(1) @binding(0) var<uniform> ubView : ub_view;
+@group(2) @binding(0) var<uniform> uvMesh : ub_mesh;
+@group(0) @binding(0) var<uniform> ubView : ub_view;
 
 @vertex
 fn vertexMain(@location(0) position : vec4f) -> VertexOutput {
diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js
index 95d601edf..910c0d805 100644
--- a/src/deprecated/deprecated.js
+++ b/src/deprecated/deprecated.js
@@ -126,9 +126,6 @@ import { basisInitialize } from '../framework/handlers/basis.js';
 import { LitShader } from '../scene/shader-lib/programs/lit-shader.js';
 import { Geometry } from '../scene/geometry/geometry.js';
 
-// ScriptType alias
-export { Script as ScriptType } from '../framework/script/script.js';
-
 // CORE
 export const LINEBATCH_WORLD = 0;
 export const LINEBATCH_OVERLAY = 1;
diff --git a/src/framework/components/script/component.js b/src/framework/components/script/component.js
index cc918e721..c182759bf 100644
--- a/src/framework/components/script/component.js
+++ b/src/framework/components/script/component.js
@@ -19,7 +19,7 @@ import { Entity } from '../../entity.js';
  */
 class ScriptComponent extends Component {
     /**
-     * Fired when a {@link Script} instance is created and attached to the script component.
+     * Fired when a {@link ScriptType} instance is created and attached to the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `create` - Fired when a script instance is created. The name of the script type and the
@@ -40,7 +40,7 @@ class ScriptComponent extends Component {
     static EVENT_CREATE = 'create';
 
     /**
-     * Fired when a {@link Script} instance is destroyed and removed from the script component.
+     * Fired when a {@link ScriptType} instance is destroyed and removed from the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `destroy` - Fired when a script instance is destroyed. The name of the script type and
@@ -109,7 +109,7 @@ class ScriptComponent extends Component {
     static EVENT_STATE = 'state';
 
     /**
-     * Fired when the index of a {@link Script} instance is changed in the script component.
+     * Fired when the index of a {@link ScriptType} instance is changed in the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `move` - Fired when a script instance is moved. The name of the script type, the script
@@ -130,7 +130,7 @@ class ScriptComponent extends Component {
     static EVENT_MOVE = 'move';
 
     /**
-     * Fired when a {@link Script} instance had an exception. The handler is passed the script
+     * Fired when a {@link ScriptType} instance had an exception. The handler is passed the script
      * instance, the exception and the method name that the exception originated from.
      *
      * @event
@@ -154,7 +154,7 @@ class ScriptComponent extends Component {
         /**
          * Holds all script instances for this component.
          *
-         * @type {import('../../script/script.js').Script[]}
+         * @type {import('../../script/script-type.js').ScriptType[]}
          * @private
          */
         this._scripts = [];
@@ -191,7 +191,7 @@ class ScriptComponent extends Component {
      * An array of all script instances attached to an entity. This array is read-only and should
      * not be modified by developer.
      *
-     * @type {import('../../script/script.js').Script[]}
+     * @type {import('../../script/script-type.js').ScriptType[]}
      */
     set scripts(value) {
         this._scriptsData = value;
@@ -546,8 +546,8 @@ class ScriptComponent extends Component {
     /**
      * Detect if script is attached to an entity.
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
      * @returns {boolean} If script is attached to an entity.
      * @example
      * if (entity.script.has('playerController')) {
@@ -570,9 +570,9 @@ class ScriptComponent extends Component {
     /**
      * Get a script instance (if attached).
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
-     * @returns {import('../../script/script.js').Script|null} If script is attached, the
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
+     * @returns {import('../../script/script-type.js').ScriptType|null} If script is attached, the
      * instance is returned. Otherwise null is returned.
      * @example
      * const controller = entity.script.get('playerController');
@@ -594,8 +594,8 @@ class ScriptComponent extends Component {
     /**
      * Create a script instance and attach to an entity script component.
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
      * @param {object} [args] - Object with arguments for a script.
      * @param {boolean} [args.enabled] - If script instance is enabled after creation. Defaults to
      * true.
@@ -605,9 +605,9 @@ class ScriptComponent extends Component {
      * script and attributes must be initialized manually. Defaults to false.
      * @param {number} [args.ind] - The index where to insert the script instance at. Defaults to
      * -1, which means append it at the end.
-     * @returns {import('../../script/script.js').Script|null} Returns an instance of a
-     * {@link Script} if successfully attached to an entity, or null if it failed because a
-     * script with a same name has already been added or if the {@link Script} cannot be found
+     * @returns {import('../../script/script-type.js').ScriptType|null} Returns an instance of a
+     * {@link ScriptType} if successfully attached to an entity, or null if it failed because a
+     * script with a same name has already been added or if the {@link ScriptType} cannot be found
      * by name in the {@link ScriptRegistry}.
      * @example
      * entity.script.create('playerController', {
@@ -699,8 +699,8 @@ class ScriptComponent extends Component {
     /**
      * Destroy the script instance that is attached to an entity.
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
      * @returns {boolean} If it was successfully destroyed.
      * @example
      * entity.script.destroy('playerController');
@@ -756,8 +756,8 @@ class ScriptComponent extends Component {
     /**
      * Swap the script instance.
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
      * @returns {boolean} If it was successfully swapped.
      * @private
      */
@@ -927,8 +927,8 @@ class ScriptComponent extends Component {
     /**
      * Move script instance to different position to alter update order of scripts within entity.
      *
-     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
-     * name or type of {@link Script}.
+     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
+     * name or type of {@link ScriptType}.
      * @param {number} ind - New position index.
      * @returns {boolean} If it was successfully moved.
      * @example
diff --git a/src/framework/entity.js b/src/framework/entity.js
index e4ca2cb4b..08b808127 100644
--- a/src/framework/entity.js
+++ b/src/framework/entity.js
@@ -400,8 +400,8 @@ class Entity extends GraphNode {
     /**
      * Search the entity and all of its descendants for the first script instance of specified type.
      *
-     * @param {string|typeof import('./script/script.js').Script} nameOrType - The name or type of {@link Script}.
-     * @returns {import('./script/script.js').Script|undefined} A script instance of specified type, if the entity or any of its descendants
+     * @param {string|typeof 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
@@ -415,8 +415,8 @@ class Entity extends GraphNode {
     /**
      * Search the entity and all of its descendants for all script instances of specified type.
      *
-     * @param {string|typeof import('./script/script.js').Script} nameOrType - The name or type of {@link Script}.
-     * @returns {import('./script/script.js').Script[]} All script instances of specified type in the entity or any of its
+     * @param {string|typeof 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
diff --git a/src/framework/handlers/cubemap.js b/src/framework/handlers/cubemap.js
index 9978f4842..bfd6a15aa 100644
--- a/src/framework/handlers/cubemap.js
+++ b/src/framework/handlers/cubemap.js
@@ -121,7 +121,6 @@ class CubemapHandler extends ResourceHandler {
                             height: tex.height >> i,
                             format: tex.format,
                             levels: [tex._levels[i]],
-                            fixCubemapSeams: true,
                             addressU: ADDRESS_CLAMP_TO_EDGE,
                             addressV: ADDRESS_CLAMP_TO_EDGE,
                             // generate cubemaps on the top level only
@@ -180,8 +179,7 @@ class CubemapHandler extends ResourceHandler {
                     magFilter: assetData.hasOwnProperty('magFilter') ? assetData.magFilter : faceTextures[0].magFilter,
                     anisotropy: assetData.hasOwnProperty('anisotropy') ? assetData.anisotropy : 1,
                     addressU: ADDRESS_CLAMP_TO_EDGE,
-                    addressV: ADDRESS_CLAMP_TO_EDGE,
-                    fixCubemapSeams: !!assets[0]
+                    addressV: ADDRESS_CLAMP_TO_EDGE
                 });
 
                 resources[0] = faces;
diff --git a/src/framework/handlers/script.js b/src/framework/handlers/script.js
index c63caab4e..d77f0f71f 100644
--- a/src/framework/handlers/script.js
+++ b/src/framework/handlers/script.js
@@ -1,8 +1,8 @@
 import { platform } from '../../core/platform.js';
 import { script } from '../script.js';
-import { Script } from '../script/script.js';
+import { ScriptType } from '../script/script-type.js';
 import { ScriptTypes } from '../script/script-types.js';
-import { registerScript } from '../script/script-create.js';
+import { registerScript } from '../script/script.js';
 import { ResourceLoader } from './loader.js';
 
 import { ResourceHandler } from './handler.js';
@@ -154,7 +154,7 @@ class ScriptHandler extends ResourceHandler {
 
             for (const key in module) {
                 const scriptClass = module[key];
-                const extendsScriptType = scriptClass.prototype instanceof Script;
+                const extendsScriptType = scriptClass.prototype instanceof ScriptType;
 
                 if (extendsScriptType) {
 
diff --git a/src/framework/script/script-attributes.js b/src/framework/script/script-attributes.js
index 7a718b401..0d0dc1d90 100644
--- a/src/framework/script/script-attributes.js
+++ b/src/framework/script/script-attributes.js
@@ -150,8 +150,8 @@ function rawToValue(app, args, value, old) {
 
 /**
  * Container of Script Attribute definitions. Implements an interface to add/remove attributes and
- * store their definition for a {@link Script}. Note: An instance of ScriptAttributes is
- * created automatically by each {@link Script}.
+ * store their definition for a {@link ScriptType}. Note: An instance of ScriptAttributes is
+ * created automatically by each {@link ScriptType}.
  *
  * @category Script
  */
@@ -159,7 +159,7 @@ class ScriptAttributes {
     /**
      * Create a new ScriptAttributes instance.
      *
-     * @param {typeof import('./script.js').Script} scriptType - Script Type that attributes relate to.
+     * @param {typeof import('./script-type.js').ScriptType} scriptType - Script Type that attributes relate to.
      */
     constructor(scriptType) {
         this.scriptType = scriptType;
diff --git a/src/framework/script/script-create.js b/src/framework/script/script-create.js
deleted file mode 100644
index b463156e2..000000000
--- a/src/framework/script/script-create.js
+++ /dev/null
@@ -1,148 +0,0 @@
-import { Debug } from '../../core/debug.js';
-
-import { script } from '../script.js';
-import { AppBase } from '../app-base.js';
-
-import { ScriptAttributes } from './script-attributes.js';
-import { Script } from './script.js';
-import { ScriptTypes } from './script-types.js';
-
-const reservedScriptNames = new Set([
-    'system', 'entity', 'create', 'destroy', 'swap', 'move', 'data',
-    'scripts', '_scripts', '_scriptsIndex', '_scriptsData',
-    'enabled', '_oldState', 'onEnable', 'onDisable', 'onPostStateChange',
-    '_onSetEnabled', '_checkState', '_onBeforeRemove',
-    '_onInitializeAttributes', '_onInitialize', '_onPostInitialize',
-    '_onUpdate', '_onPostUpdate',
-    '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent'
-]);
-
-function getReservedScriptNames() {
-    return reservedScriptNames;
-}
-
-/**
- * Create and register a new {@link Script}. It returns new class type (constructor function),
- * which is auto-registered to {@link ScriptRegistry} using its name. This is the main interface to
- * create Script Types, to define custom logic using JavaScript, that is used to create interaction
- * for entities.
- *
- * @param {string} name - Unique Name of a Script Type. If a Script Type with the same name has
- * already been registered and the new one has a `swap` method defined in its prototype, then it
- * will perform hot swapping of existing Script Instances on entities using this new Script Type.
- * Note: There is a reserved list of names that cannot be used, such as list below as well as some
- * starting from `_` (underscore): system, entity, create, destroy, swap, move, scripts, onEnable,
- * onDisable, onPostStateChange, has, on, off, fire, once, hasEvent.
- * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
- * to add a script to. By default it will use `Application.getApplication()` to get current
- * {@link AppBase}.
- * @returns {typeof Script|null} A class type (constructor function) that inherits {@link Script},
- * which the developer is meant to further extend by adding attributes and prototype methods.
- * Returns null if there was an error.
- * @example
- * var Turning = pc.createScript('turn');
- *
- * // define 'speed' attribute that is available in Editor UI
- * Turning.attributes.add('speed', {
- *     type: 'number',
- *     default: 180,
- *     placeholder: 'deg/s'
- * });
- *
- * // runs every tick
- * Turning.prototype.update = function (dt) {
- *     this.entity.rotate(0, this.speed * dt, 0);
- * };
- * @category Script
- */
-function createScript(name, app) {
-    if (script.legacy) {
-        Debug.error('This project is using the legacy script system. You cannot call pc.createScript().');
-        return null;
-    }
-
-    if (reservedScriptNames.has(name))
-        throw new Error(`Script name '${name}' is reserved, please rename the script`);
-
-    class ScriptWithAttributes extends Script {
-        attributes = new ScriptAttributes(ScriptWithAttributes);
-    }
-
-    registerScript(ScriptWithAttributes, name, app);
-    return ScriptWithAttributes;
-}
-
-// Editor uses this - migrate to ScriptAttributes.reservedNames and delete this
-const reservedAttributes = {};
-ScriptAttributes.reservedNames.forEach((value, value2, set) => {
-    reservedAttributes[value] = 1;
-});
-createScript.reservedAttributes = reservedAttributes;
-
-/* eslint-disable jsdoc/check-examples */
-/**
- * Register a existing class type as a Script Type to {@link ScriptRegistry}. Useful when defining
- * a ES6 script class that extends {@link Script} (see example).
- *
- * @param {typeof Script} script - The existing class type (constructor function) to be
- * registered as a Script Type. Class must extend {@link Script} (see example). Please note: A
- * class created using {@link createScript} is auto-registered, and should therefore not be pass
- * into {@link registerScript} (which would result in swapping out all related script instances).
- * @param {string} [name] - Optional unique name of the Script Type. By default it will use the
- * same name as the existing class. If a Script Type with the same name has already been registered
- * and the new one has a `swap` method defined in its prototype, then it will perform hot swapping
- * of existing Script Instances on entities using this new Script Type. Note: There is a reserved
- * list of names that cannot be used, such as list below as well as some starting from `_`
- * (underscore): system, entity, create, destroy, swap, move, scripts, onEnable, onDisable,
- * onPostStateChange, has, on, off, fire, once, hasEvent.
- * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
- * to register the script type to. By default it will use `Application.getApplication()` to get
- * current {@link AppBase}.
- * @example
- * // define a ES6 script class
- * class PlayerController extends pc.Script {
- *
- *     initialize() {
- *         // called once on initialize
- *     }
- *
- *     update(dt) {
- *         // called each tick
- *     }
- * }
- *
- * // register the class as a script
- * pc.registerScript(PlayerController);
- *
- * // declare script attributes (Must be after pc.registerScript())
- * PlayerController.attributes.add('attribute1', {type: 'number'});
- * @category Script
- */
-function registerScript(script, name, app) {
-    if (script.legacy) {
-        Debug.error('This project is using the legacy script system. You cannot call pc.registerScript().');
-        return;
-    }
-
-    if (typeof script !== 'function')
-        throw new Error(`script class: '${script}' must be a constructor function (i.e. class).`);
-
-    if (!(script.prototype instanceof Script))
-        throw new Error(`script class: '${Script.__getScriptName(script)}' does not extend pc.Script.`);
-
-    name = name || script.__name || Script.__getScriptName(script);
-
-    if (reservedScriptNames.has(name))
-        throw new Error(`script name: '${name}' is reserved, please change script name`);
-
-    script.__name = name;
-
-    // add to scripts registry
-    const registry = app ? app.scripts : AppBase.getApplication().scripts;
-    registry.add(script);
-
-    ScriptTypes.push(script, script.legacy);
-}
-/* eslint-enable jsdoc/check-examples */
-
-export { createScript, registerScript, getReservedScriptNames };
diff --git a/src/framework/script/script-registry.js b/src/framework/script/script-registry.js
index 78db4b531..12f08201c 100644
--- a/src/framework/script/script-registry.js
+++ b/src/framework/script/script-registry.js
@@ -1,7 +1,7 @@
 import { EventHandler } from '../../core/event-handler.js';
 
 /**
- * Container for all {@link Script}s that are available to this application. Note that
+ * Container for all {@link ScriptType}s that are available to this application. Note that
  * PlayCanvas scripts can access the Script Registry from inside the application with
  * {@link AppBase#scripts}.
  *
@@ -9,13 +9,13 @@ import { EventHandler } from '../../core/event-handler.js';
  */
 class ScriptRegistry extends EventHandler {
     /**
-     * @type {Object<string, typeof import('./script.js').Script>}
+     * @type {Object<string, typeof import('./script-type.js').ScriptType>}
      * @private
      */
     _scripts = {};
 
     /**
-     * @type {typeof import('./script.js').Script[]}
+     * @type {typeof import('./script-type.js').ScriptType[]}
      * @private
      */
     _list = [];
@@ -37,12 +37,12 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Add {@link Script} to registry. Note: when {@link createScript} is called, it will add
-     * the {@link Script} to the registry automatically. If a script already exists in
+     * Add {@link ScriptType} to registry. Note: when {@link createScript} is called, it will add
+     * the {@link ScriptType} to the registry automatically. If a script already exists in
      * registry, and the new script has a `swap` method defined, it will perform code hot swapping
      * automatically in async manner.
      *
-     * @param {typeof import('./script.js').Script} script - Script Type that is created
+     * @param {typeof import('./script-type.js').ScriptType} script - Script Type that is created
      * using {@link createScript}.
      * @returns {boolean} True if added for the first time or false if script already exists.
      * @example
@@ -146,10 +146,10 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Remove {@link Script}.
+     * Remove {@link ScriptType}.
      *
-     * @param {string|typeof import('./script.js').Script} nameOrType - The name or type
-     * of {@link Script}.
+     * @param {string|typeof import('./script-type.js').ScriptType} nameOrType - The name or type
+     * of {@link ScriptType}.
      * @returns {boolean} True if removed or False if already not in registry.
      * @example
      * app.scripts.remove('playerController');
@@ -178,10 +178,10 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Get {@link Script} by name.
+     * Get {@link ScriptType} by name.
      *
-     * @param {string} name - Name of a {@link Script}.
-     * @returns {typeof import('./script.js').Script} The Script Type if it exists in the
+     * @param {string} name - Name of a {@link ScriptType}.
+     * @returns {typeof import('./script-type.js').ScriptType} The Script Type if it exists in the
      * registry or null otherwise.
      * @example
      * var PlayerController = app.scripts.get('playerController');
@@ -191,11 +191,11 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Check if a {@link Script} with the specified name is in the registry.
+     * Check if a {@link ScriptType} with the specified name is in the registry.
      *
-     * @param {string|typeof import('./script.js').Script} nameOrType - The name or type
-     * of {@link Script}.
-     * @returns {boolean} True if {@link Script} is in registry.
+     * @param {string|typeof import('./script-type.js').ScriptType} nameOrType - The name or type
+     * of {@link ScriptType}.
+     * @returns {boolean} True if {@link ScriptType} is in registry.
      * @example
      * if (app.scripts.has('playerController')) {
      *     // playerController is in pc.ScriptRegistry
@@ -212,9 +212,9 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Get list of all {@link Script}s from registry.
+     * Get list of all {@link ScriptType}s from registry.
      *
-     * @returns {Array<typeof import('./script.js').Script>} list of all {@link Script}s
+     * @returns {Array<typeof import('./script-type.js').ScriptType>} list of all {@link ScriptType}s
      * in registry.
      * @example
      * // logs array of all Script Type names available in registry
diff --git a/src/framework/script/script-type.js b/src/framework/script/script-type.js
new file mode 100644
index 000000000..3a1ff14a3
--- /dev/null
+++ b/src/framework/script/script-type.js
@@ -0,0 +1,393 @@
+import { Debug } from '../../core/debug.js';
+import { EventHandler } from '../../core/event-handler.js';
+
+import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js';
+import { ScriptAttributes } from './script-attributes.js';
+
+const funcNameRegex = new RegExp('^\\s*function(?:\\s|\\s*\\/\\*.*\\*\\/\\s*)+([^\\(\\s\\/]*)\\s*');
+
+/**
+ * Represents the type of a script. It is returned by {@link createScript}. Also referred to as
+ * Script Type.
+ *
+ * The type is to be extended using its JavaScript prototype. There is a list of methods that will
+ * be executed by the engine on instances of this type, such as:
+ *
+ * - `initialize`
+ * - `postInitialize`
+ * - `update`
+ * - `postUpdate`
+ * - `swap`
+ *
+ * `initialize` and `postInitialize` - are called (if defined) when a script is about to run for
+ * the first time - `postInitialize` will run after all `initialize` methods are executed in the
+ * same tick or enabling chain of actions.
+ *
+ * `update` and `postUpdate` - are called (if defined) for enabled (running state) scripts on each
+ * tick.
+ *
+ * `swap` - is called when a ScriptType that already exists in the registry gets redefined. If the
+ * new ScriptType has a `swap` method in its prototype, then it will be executed to perform hot-
+ * reload at runtime.
+ *
+ * @category Script
+ */
+class ScriptType extends EventHandler {
+    /**
+     * Fired when a script instance becomes enabled.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('enable', () => {
+     *         // Script Instance is now enabled
+     *     });
+     * };
+     */
+    static EVENT_ENABLE = 'enable';
+
+    /**
+     * Fired when a script instance becomes disabled.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('disable', () => {
+     *         // Script Instance is now disabled
+     *     });
+     * };
+     */
+    static EVENT_DISABLE = 'disable';
+
+    /**
+     * Fired when a script instance changes state to enabled or disabled. The handler is passed a
+     * boolean parameter that states whether the script instance is now enabled or disabled.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('state', (enabled) => {
+     *         console.log(`Script Instance is now ${enabled ? 'enabled' : 'disabled'}`);
+     *     });
+     * };
+     */
+    static EVENT_STATE = 'state';
+
+    /**
+     * Fired when a script instance is destroyed and removed from component.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('destroy', () => {
+     *         // no longer part of the entity
+     *         // this is a good place to clean up allocated resources used by the script
+     *     });
+     * };
+     */
+    static EVENT_DESTROY = 'destroy';
+
+    /**
+     * Fired when script attributes have changed. This event is available in two forms. They are as follows:
+     *
+     * 1. `attr` - Fired for any attribute change. The handler is passed the name of the attribute
+     * that changed, the value of the attribute before the change and the value of the attribute
+     * after the change.
+     * 2. `attr:[name]` - Fired for a specific attribute change. The handler is passed the value of
+     * the attribute before the change and the value of the attribute after the change.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('attr', (name, newValue, oldValue) => {
+     *         console.log(`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`);
+     *     });
+     * };
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('attr:speed', (newValue, oldValue) => {
+     *         console.log(`Attribute 'speed' changed from '${oldValue}' to '${newValue}'`);
+     *     });
+     * };
+     */
+    static EVENT_ATTR = 'attr';
+
+    /**
+     * Fired when a script instance had an exception. The script instance will be automatically
+     * disabled. The handler is passed an {@link Error} object containing the details of the
+     * exception and the name of the method that threw the exception.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('error', (err, method) => {
+     *         // caught an exception
+     *         console.log(err.stack);
+     *     });
+     * };
+     */
+    static EVENT_ERROR = 'error';
+
+    /**
+     * The {@link AppBase} that the instance of this type belongs to.
+     *
+     * @type {import('../app-base.js').AppBase}
+     */
+    app;
+
+    /**
+     * The {@link Entity} that the instance of this type belongs to.
+     *
+     * @type {import('../entity.js').Entity}
+     */
+    entity;
+
+    /** @private */
+    _enabled;
+
+    /** @private */
+    _enabledOld;
+
+    /** @private */
+    _initialized;
+
+    /** @private */
+    _postInitialized;
+
+    /** @private */
+    __destroyed;
+
+    /** @private */
+    __attributes;
+
+    /** @private */
+    __attributesRaw;
+
+    /** @private */
+    __scriptType;
+
+    /**
+     * The order in the script component that the methods of this script instance will run
+     * relative to other script instances in the component.
+     *
+     * @type {number}
+     * @private
+     */
+    __executionOrder;
+
+    /**
+     * Create a new ScriptType instance.
+     *
+     * @param {object} args - The input arguments object.
+     * @param {import('../app-base.js').AppBase} args.app - The {@link AppBase} that is running the
+     * script.
+     * @param {import('../entity.js').Entity} args.entity - The {@link Entity} that the script is
+     * attached to.
+     */
+    constructor(args) {
+        super();
+        this.initScriptType(args);
+    }
+
+    /**
+     * True if the instance of this type is in running state. False when script is not running,
+     * because the Entity or any of its parents are disabled or the {@link ScriptComponent} is
+     * disabled or the Script Instance is disabled. When disabled no update methods will be called
+     * on each tick. initialize and postInitialize methods will run once when the script instance
+     * is in `enabled` state during app tick.
+     *
+     * @type {boolean}
+     */
+    set enabled(value) {
+        this._enabled = !!value;
+
+        if (this.enabled === this._enabledOld) return;
+
+        this._enabledOld = this.enabled;
+        this.fire(this.enabled ? 'enable' : 'disable');
+        this.fire('state', this.enabled);
+
+        // initialize script if not initialized yet and script is enabled
+        if (!this._initialized && this.enabled) {
+            this._initialized = true;
+
+            this.__initializeAttributes(true);
+
+            if (this.initialize)
+                this.entity.script._scriptMethod(this, SCRIPT_INITIALIZE);
+        }
+
+        // post initialize script if not post initialized yet and still enabled
+        // (initialize might have disabled the script so check this.enabled again)
+        // Warning: Do not do this if the script component is currently being enabled
+        // because in this case post initialize must be called after all the scripts
+        // in the script component have been initialized first
+        if (this._initialized && !this._postInitialized && this.enabled && !this.entity.script._beingEnabled) {
+            this._postInitialized = true;
+
+            if (this.postInitialize)
+                this.entity.script._scriptMethod(this, SCRIPT_POST_INITIALIZE);
+        }
+    }
+
+    get enabled() {
+        return this._enabled && !this._destroyed && this.entity.script.enabled && this.entity.enabled;
+    }
+
+    /**
+     * @param {{entity: import('../entity.js').Entity, app: import('../app-base.js').AppBase}} args -
+     * The entity and app.
+     * @private
+     */
+    initScriptType(args) {
+        const script = this.constructor; // get script type, i.e. function (class)
+        Debug.assert(args && args.app && args.entity, `script [${script.__name}] has missing arguments in constructor`);
+
+        this.app = args.app;
+        this.entity = args.entity;
+
+        this._enabled = typeof args.enabled === 'boolean' ? args.enabled : true;
+        this._enabledOld = this.enabled;
+
+        this.__destroyed = false;
+        this.__attributes = { };
+        this.__attributesRaw = args.attributes || { }; // need at least an empty object to make sure default attributes are initialized
+        this.__scriptType = script;
+        this.__executionOrder = -1;
+    }
+
+    /**
+     * Name of a Script Type.
+     *
+     * @type {string}
+     * @private
+     */
+    static __name = null; // Will be assigned when calling createScript or registerScript.
+
+    /**
+     * @param {*} constructorFn - The constructor function of the script type.
+     * @returns {string} The script name.
+     * @private
+     */
+    static __getScriptName(constructorFn) {
+        if (typeof constructorFn !== 'function') return undefined;
+        if ('name' in Function.prototype) return constructorFn.name;
+        if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function';
+        const match = ('' + constructorFn).match(funcNameRegex);
+        return match ? match[1] : undefined;
+    }
+
+    /**
+     * Name of a Script Type.
+     *
+     * @type {string|null}
+     */
+    static get scriptName() {
+        return this.__name;
+    }
+
+    /**
+     * The interface to define attributes for Script Types. Refer to {@link ScriptAttributes}.
+     *
+     * @type {ScriptAttributes}
+     * @example
+     * var PlayerController = pc.createScript('playerController');
+     *
+     * PlayerController.attributes.add('speed', {
+     *     type: 'number',
+     *     title: 'Speed',
+     *     placeholder: 'km/h',
+     *     default: 22.2
+     * });
+     */
+    static get attributes() {
+        if (!this.hasOwnProperty('__attributes')) this.__attributes = new ScriptAttributes(this);
+        return this.__attributes;
+    }
+
+    /**
+     * @param {boolean} [force] - Set to true to force initialization of the attributes.
+     * @private
+     */
+    __initializeAttributes(force) {
+        if (!force && !this.__attributesRaw)
+            return;
+
+        // set attributes values
+        for (const key in this.__scriptType.attributes.index) {
+            if (this.__attributesRaw && this.__attributesRaw.hasOwnProperty(key)) {
+                this[key] = this.__attributesRaw[key];
+            } else if (!this.__attributes.hasOwnProperty(key)) {
+                if (this.__scriptType.attributes.index[key].hasOwnProperty('default')) {
+                    this[key] = this.__scriptType.attributes.index[key].default;
+                } else {
+                    this[key] = null;
+                }
+            }
+        }
+
+        this.__attributesRaw = null;
+    }
+
+    /**
+     * Shorthand function to extend Script Type prototype with list of methods.
+     *
+     * @param {object} methods - Object with methods, where key - is name of method, and value - is function.
+     * @example
+     * var PlayerController = pc.createScript('playerController');
+     *
+     * PlayerController.extend({
+     *     initialize: function () {
+     *         // called once on initialize
+     *     },
+     *     update: function (dt) {
+     *         // called each tick
+     *     }
+     * });
+     */
+    static extend(methods) {
+        for (const key in methods) {
+            if (!methods.hasOwnProperty(key))
+                continue;
+
+            this.prototype[key] = methods[key];
+        }
+    }
+
+    /**
+     * @function
+     * @name ScriptType#[initialize]
+     * @description Called when script is about to run for the first time.
+     */
+
+    /**
+     * @function
+     * @name ScriptType#[postInitialize]
+     * @description Called after all initialize methods are executed in the same tick or enabling chain of actions.
+     */
+
+    /**
+     * @function
+     * @name ScriptType#[update]
+     * @description Called for enabled (running state) scripts on each tick.
+     * @param {number} dt - The delta time in seconds since the last frame.
+     */
+
+    /**
+     * @function
+     * @name ScriptType#[postUpdate]
+     * @description Called for enabled (running state) scripts on each tick, after update.
+     * @param {number} dt - The delta time in seconds since the last frame.
+     */
+
+    /**
+     * @function
+     * @name ScriptType#[swap]
+     * @description Called when a ScriptType that already exists in the registry
+     * gets redefined. If the new ScriptType has a `swap` method in its prototype,
+     * then it will be executed to perform hot-reload at runtime.
+     * @param {ScriptType} old - Old instance of the scriptType to copy data to the new instance.
+     */
+}
+
+export { ScriptType };
diff --git a/src/framework/script/script.js b/src/framework/script/script.js
index a136619f1..b25939d38 100644
--- a/src/framework/script/script.js
+++ b/src/framework/script/script.js
@@ -1,404 +1,156 @@
 import { Debug } from '../../core/debug.js';
 import { EventHandler } from '../../core/event-handler.js';
 
-import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js';
-import { ScriptAttributes } from './script-attributes.js';
+import { script } from '../script.js';
+import { AppBase } from '../app-base.js';
 
-const funcNameRegex = new RegExp('^\\s*function(?:\\s|\\s*\\/\\*.*\\*\\/\\s*)+([^\\(\\s\\/]*)\\s*');
+import { ScriptAttributes } from './script-attributes.js';
+import { ScriptType } from './script-type.js';
+import { ScriptTypes } from './script-types.js';
+
+const reservedScriptNames = new Set([
+    'system', 'entity', 'create', 'destroy', 'swap', 'move', 'data',
+    'scripts', '_scripts', '_scriptsIndex', '_scriptsData',
+    'enabled', '_oldState', 'onEnable', 'onDisable', 'onPostStateChange',
+    '_onSetEnabled', '_checkState', '_onBeforeRemove',
+    '_onInitializeAttributes', '_onInitialize', '_onPostInitialize',
+    '_onUpdate', '_onPostUpdate',
+    '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent'
+]);
+
+function getReservedScriptNames() {
+    return reservedScriptNames;
+}
 
 /**
- * The Script class is a base class that you must extend to receive
- * {@link https://developer.playcanvas.com/user-manual/scripting/anatomy/ various lifecycle updates}
- * from the engine.
- *
- * You can create a Script using either {@link createScript} or by extending the class directly.
- *
- * ```javascript
- * import { Script } from 'playcanvas';
- * class Rotator extends Script {
- *     update() {
- *         this.entity.rotate(0, 0.1, 0);
- *     }
- * }
- * ```
- * The following methods are called if they exist on the Script instance:
- *
- * - `initialize`
- * - `postInitialize`
- * - `update`
- * - `postUpdate`
- * - `swap`
+ * Create and register a new {@link ScriptType}. It returns new class type (constructor function),
+ * which is auto-registered to {@link ScriptRegistry} using its name. This is the main interface to
+ * create Script Types, to define custom logic using JavaScript, that is used to create interaction
+ * for entities.
  *
- * `initialize` and `postInitialize` - are called (if defined) when a script is about to run for
- * the first time - `postInitialize` will run after all `initialize` methods are executed in the
- * same tick or enabling chain of actions.
+ * @param {string} name - Unique Name of a Script Type. If a Script Type with the same name has
+ * already been registered and the new one has a `swap` method defined in its prototype, then it
+ * will perform hot swapping of existing Script Instances on entities using this new Script Type.
+ * Note: There is a reserved list of names that cannot be used, such as list below as well as some
+ * starting from `_` (underscore): system, entity, create, destroy, swap, move, scripts, onEnable,
+ * onDisable, onPostStateChange, has, on, off, fire, once, hasEvent.
+ * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
+ * to add a script to. By default it will use `Application.getApplication()` to get current
+ * {@link AppBase}.
+ * @returns {typeof ScriptType|null} A class type (constructor function) that inherits {@link ScriptType},
+ * which the developer is meant to further extend by adding attributes and prototype methods.
+ * Returns null if there was an error.
+ * @example
+ * var Turning = pc.createScript('turn');
  *
- * `update` and `postUpdate` - are called (if defined) for enabled (running state) scripts on each
- * tick.
+ * // define 'speed' attribute that is available in Editor UI
+ * Turning.attributes.add('speed', {
+ *     type: 'number',
+ *     default: 180,
+ *     placeholder: 'deg/s'
+ * });
  *
- * `swap` - is called when a Script that already exists in the registry gets redefined. If the
- * new Script has a `swap` method in its prototype, then it will be executed to perform hot-
- * reload at runtime.
- *
- * @see {@link https://developer.playcanvas.com/user-manual/scripting/anatomy/} for more information.
+ * // runs every tick
+ * Turning.prototype.update = function (dt) {
+ *     this.entity.rotate(0, this.speed * dt, 0);
+ * };
  * @category Script
  */
-class Script extends EventHandler {
-    /**
-     * Fired when a script instance becomes enabled.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('enable', () => {
-     *         // Script Instance is now enabled
-     *     });
-     * };
-     */
-    static EVENT_ENABLE = 'enable';
-
-    /**
-     * Fired when a script instance becomes disabled.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('disable', () => {
-     *         // Script Instance is now disabled
-     *     });
-     * };
-     */
-    static EVENT_DISABLE = 'disable';
-
-    /**
-     * Fired when a script instance changes state to enabled or disabled. The handler is passed a
-     * boolean parameter that states whether the script instance is now enabled or disabled.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('state', (enabled) => {
-     *         console.log(`Script Instance is now ${enabled ? 'enabled' : 'disabled'}`);
-     *     });
-     * };
-     */
-    static EVENT_STATE = 'state';
-
-    /**
-     * Fired when a script instance is destroyed and removed from component.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('destroy', () => {
-     *         // no longer part of the entity
-     *         // this is a good place to clean up allocated resources used by the script
-     *     });
-     * };
-     */
-    static EVENT_DESTROY = 'destroy';
-
-    /**
-     * Fired when script attributes have changed. This event is available in two forms. They are as follows:
-     *
-     * 1. `attr` - Fired for any attribute change. The handler is passed the name of the attribute
-     * that changed, the value of the attribute before the change and the value of the attribute
-     * after the change.
-     * 2. `attr:[name]` - Fired for a specific attribute change. The handler is passed the value of
-     * the attribute before the change and the value of the attribute after the change.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('attr', (name, newValue, oldValue) => {
-     *         console.log(`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`);
-     *     });
-     * };
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('attr:speed', (newValue, oldValue) => {
-     *         console.log(`Attribute 'speed' changed from '${oldValue}' to '${newValue}'`);
-     *     });
-     * };
-     */
-    static EVENT_ATTR = 'attr';
-
-    /**
-     * Fired when a script instance had an exception. The script instance will be automatically
-     * disabled. The handler is passed an {@link Error} object containing the details of the
-     * exception and the name of the method that threw the exception.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('error', (err, method) => {
-     *         // caught an exception
-     *         console.log(err.stack);
-     *     });
-     * };
-     */
-    static EVENT_ERROR = 'error';
-
-    /**
-     * The {@link AppBase} that the instance of this type belongs to.
-     *
-     * @type {import('../app-base.js').AppBase}
-     */
-    app;
-
-    /**
-     * The {@link Entity} that the instance of this type belongs to.
-     *
-     * @type {import('../entity.js').Entity}
-     */
-    entity;
-
-    /** @private */
-    _enabled;
-
-    /** @private */
-    _enabledOld;
-
-    /** @private */
-    _initialized;
-
-    /** @private */
-    _postInitialized;
-
-    /** @private */
-    __destroyed;
-
-    /** @private */
-    __attributes;
-
-    /** @private */
-    __attributesRaw;
-
-    /** @private */
-    __scriptType;
-
-    /**
-     * The order in the script component that the methods of this script instance will run
-     * relative to other script instances in the component.
-     *
-     * @type {number}
-     * @private
-     */
-    __executionOrder;
-
-    /**
-     * Create a new Script instance.
-     *
-     * @param {object} args - The input arguments object.
-     * @param {import('../app-base.js').AppBase} args.app - The {@link AppBase} that is running the
-     * script.
-     * @param {import('../entity.js').Entity} args.entity - The {@link Entity} that the script is
-     * attached to.
-     */
-    constructor(args) {
-        super();
-        this.initScriptType(args);
-    }
-
-    /**
-     * True if the instance of this type is in running state. False when script is not running,
-     * because the Entity or any of its parents are disabled or the {@link ScriptComponent} is
-     * disabled or the Script Instance is disabled. When disabled no update methods will be called
-     * on each tick. initialize and postInitialize methods will run once when the script instance
-     * is in `enabled` state during app tick.
-     *
-     * @type {boolean}
-     */
-    set enabled(value) {
-        this._enabled = !!value;
-
-        if (this.enabled === this._enabledOld) return;
-
-        this._enabledOld = this.enabled;
-        this.fire(this.enabled ? 'enable' : 'disable');
-        this.fire('state', this.enabled);
-
-        // initialize script if not initialized yet and script is enabled
-        if (!this._initialized && this.enabled) {
-            this._initialized = true;
-
-            this.__initializeAttributes(true);
-
-            if (this.initialize)
-                this.entity.script._scriptMethod(this, SCRIPT_INITIALIZE);
-        }
-
-        // post initialize script if not post initialized yet and still enabled
-        // (initialize might have disabled the script so check this.enabled again)
-        // Warning: Do not do this if the script component is currently being enabled
-        // because in this case post initialize must be called after all the scripts
-        // in the script component have been initialized first
-        if (this._initialized && !this._postInitialized && this.enabled && !this.entity.script._beingEnabled) {
-            this._postInitialized = true;
-
-            if (this.postInitialize)
-                this.entity.script._scriptMethod(this, SCRIPT_POST_INITIALIZE);
-        }
+function createScript(name, app) {
+    if (script.legacy) {
+        Debug.error('This project is using the legacy script system. You cannot call pc.createScript().');
+        return null;
     }
 
-    get enabled() {
-        return this._enabled && !this._destroyed && this.entity.script.enabled && this.entity.enabled;
-    }
+    if (reservedScriptNames.has(name))
+        throw new Error(`Script name '${name}' is reserved, please rename the script`);
 
-    /**
-     * @param {{entity: import('../entity.js').Entity, app: import('../app-base.js').AppBase}} args -
-     * The entity and app.
-     * @private
-     */
-    initScriptType(args) {
-        const script = this.constructor; // get script type, i.e. function (class)
-        Debug.assert(args && args.app && args.entity, `script [${script.__name}] has missing arguments in constructor`);
+    const scriptType = function (args) {
+        EventHandler.prototype.initEventHandler.call(this);
+        ScriptType.prototype.initScriptType.call(this, args);
+    };
 
-        this.app = args.app;
-        this.entity = args.entity;
+    scriptType.prototype = Object.create(ScriptType.prototype);
+    scriptType.prototype.constructor = scriptType;
 
-        this._enabled = typeof args.enabled === 'boolean' ? args.enabled : true;
-        this._enabledOld = this.enabled;
+    scriptType.extend = ScriptType.extend;
+    scriptType.attributes = new ScriptAttributes(scriptType);
 
-        this.__destroyed = false;
-        this.__attributes = { };
-        this.__attributesRaw = args.attributes || { }; // need at least an empty object to make sure default attributes are initialized
-        this.__scriptType = script;
-        this.__executionOrder = -1;
-    }
-
-    /**
-     * Name of a Script.
-     *
-     * @type {string}
-     * @private
-     */
-    static __name = null; // Will be assigned when calling createScript or registerScript.
-
-    /**
-     * @param {*} constructorFn - The constructor function of the script type.
-     * @returns {string} The script name.
-     * @private
-     */
-    static __getScriptName(constructorFn) {
-        if (typeof constructorFn !== 'function') return undefined;
-        if ('name' in Function.prototype) return constructorFn.name;
-        if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function';
-        const match = ('' + constructorFn).match(funcNameRegex);
-        return match ? match[1] : undefined;
-    }
-
-    /**
-     * Name of a Script Type.
-     *
-     * @type {string|null}
-     */
-    static get scriptName() {
-        return this.__name;
-    }
-
-    /**
-     * The interface to define attributes for Script Types. Refer to {@link ScriptAttributes}.
-     *
-     * @type {ScriptAttributes}
-     * @example
-     * var PlayerController = pc.createScript('playerController');
-     *
-     * PlayerController.attributes.add('speed', {
-     *     type: 'number',
-     *     title: 'Speed',
-     *     placeholder: 'km/h',
-     *     default: 22.2
-     * });
-     */
-    static get attributes() {
-        if (!this.hasOwnProperty('__attributes')) this.__attributes = new ScriptAttributes(this);
-        return this.__attributes;
-    }
-
-    /**
-     * @param {boolean} [force] - Set to true to force initialization of the attributes.
-     * @private
-     */
-    __initializeAttributes(force) {
-        if (!force && !this.__attributesRaw)
-            return;
+    registerScript(scriptType, name, app);
+    return scriptType;
+}
 
-        // set attributes values
-        for (const key in this.__scriptType.attributes.index) {
-            if (this.__attributesRaw && this.__attributesRaw.hasOwnProperty(key)) {
-                this[key] = this.__attributesRaw[key];
-            } else if (!this.__attributes.hasOwnProperty(key)) {
-                if (this.__scriptType.attributes.index[key].hasOwnProperty('default')) {
-                    this[key] = this.__scriptType.attributes.index[key].default;
-                } else {
-                    this[key] = null;
-                }
-            }
-        }
+// Editor uses this - migrate to ScriptAttributes.reservedNames and delete this
+const reservedAttributes = {};
+ScriptAttributes.reservedNames.forEach((value, value2, set) => {
+    reservedAttributes[value] = 1;
+});
+createScript.reservedAttributes = reservedAttributes;
 
-        this.__attributesRaw = null;
+/* eslint-disable jsdoc/check-examples */
+/**
+ * Register a existing class type as a Script Type to {@link ScriptRegistry}. Useful when defining
+ * a ES6 script class that extends {@link ScriptType} (see example).
+ *
+ * @param {typeof ScriptType} script - The existing class type (constructor function) to be
+ * registered as a Script Type. Class must extend {@link ScriptType} (see example). Please note: A
+ * class created using {@link createScript} is auto-registered, and should therefore not be pass
+ * into {@link registerScript} (which would result in swapping out all related script instances).
+ * @param {string} [name] - Optional unique name of the Script Type. By default it will use the
+ * same name as the existing class. If a Script Type with the same name has already been registered
+ * and the new one has a `swap` method defined in its prototype, then it will perform hot swapping
+ * of existing Script Instances on entities using this new Script Type. Note: There is a reserved
+ * list of names that cannot be used, such as list below as well as some starting from `_`
+ * (underscore): system, entity, create, destroy, swap, move, scripts, onEnable, onDisable,
+ * onPostStateChange, has, on, off, fire, once, hasEvent.
+ * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
+ * to register the script type to. By default it will use `Application.getApplication()` to get
+ * current {@link AppBase}.
+ * @example
+ * // define a ES6 script class
+ * class PlayerController extends pc.ScriptType {
+ *
+ *     initialize() {
+ *         // called once on initialize
+ *     }
+ *
+ *     update(dt) {
+ *         // called each tick
+ *     }
+ * }
+ *
+ * // register the class as a script
+ * pc.registerScript(PlayerController);
+ *
+ * // declare script attributes (Must be after pc.registerScript())
+ * PlayerController.attributes.add('attribute1', {type: 'number'});
+ * @category Script
+ */
+function registerScript(script, name, app) {
+    if (script.legacy) {
+        Debug.error('This project is using the legacy script system. You cannot call pc.registerScript().');
+        return;
     }
 
-    /**
-     * Shorthand function to extend Script Type prototype with list of methods.
-     *
-     * @param {object} methods - Object with methods, where key - is name of method, and value - is function.
-     * @example
-     * var PlayerController = pc.createScript('playerController');
-     *
-     * PlayerController.extend({
-     *     initialize: function () {
-     *         // called once on initialize
-     *     },
-     *     update: function (dt) {
-     *         // called each tick
-     *     }
-     * });
-     */
-    static extend(methods) {
-        for (const key in methods) {
-            if (!methods.hasOwnProperty(key))
-                continue;
+    if (typeof script !== 'function')
+        throw new Error(`script class: '${script}' must be a constructor function (i.e. class).`);
 
-            this.prototype[key] = methods[key];
-        }
-    }
+    if (!(script.prototype instanceof ScriptType))
+        throw new Error(`script class: '${ScriptType.__getScriptName(script)}' does not extend pc.ScriptType.`);
 
-    /**
-     * @function
-     * @name Script#[initialize]
-     * @description Called when script is about to run for the first time.
-     */
+    name = name || script.__name || ScriptType.__getScriptName(script);
 
-    /**
-     * @function
-     * @name Script#[postInitialize]
-     * @description Called after all initialize methods are executed in the same tick or enabling chain of actions.
-     */
+    if (reservedScriptNames.has(name))
+        throw new Error(`script name: '${name}' is reserved, please change script name`);
 
-    /**
-     * @function
-     * @name Script#[update]
-     * @description Called for enabled (running state) scripts on each tick.
-     * @param {number} dt - The delta time in seconds since the last frame.
-     */
+    script.__name = name;
 
-    /**
-     * @function
-     * @name Script#[postUpdate]
-     * @description Called for enabled (running state) scripts on each tick, after update.
-     * @param {number} dt - The delta time in seconds since the last frame.
-     */
+    // add to scripts registry
+    const registry = app ? app.scripts : AppBase.getApplication().scripts;
+    registry.add(script);
 
-    /**
-     * @function
-     * @name Script#[swap]
-     * @description Called when a Script that already exists in the registry
-     * gets redefined. If the new Script has a `swap` method in its prototype,
-     * then it will be executed to perform hot-reload at runtime.
-     * @param {Script} old - Old instance of the script to copy data to the new instance.
-     */
+    ScriptTypes.push(script, script.legacy);
 }
+/* eslint-enable jsdoc/check-examples */
 
-export { Script };
+export { createScript, registerScript, getReservedScriptNames };
diff --git a/src/index.js b/src/index.js
index 10c89ac5a..ee4274508 100644
--- a/src/index.js
+++ b/src/index.js
@@ -360,10 +360,10 @@ export { ElementInput, ElementInputEvent, ElementMouseEvent, ElementSelectEvent,
 export { JsonStandardMaterialParser } from './framework/parsers/material/json-standard-material.js';
 
 // FRAMEWORK /SCRIPTS
-export { createScript, registerScript, getReservedScriptNames } from './framework/script/script-create.js';
+export { createScript, registerScript, getReservedScriptNames } from './framework/script/script.js';
 export { ScriptAttributes } from './framework/script/script-attributes.js';
 export { ScriptRegistry } from './framework/script/script-registry.js';
-export { Script } from './framework/script/script.js';
+export { ScriptType } from './framework/script/script-type.js';
 
 // FRAMEWORK / LOCALIZATION
 export { I18n } from './framework/i18n/i18n.js';
diff --git a/src/platform/graphics/constants.js b/src/platform/graphics/constants.js
index 9709b973f..6199cdb15 100644
--- a/src/platform/graphics/constants.js
+++ b/src/platform/graphics/constants.js
@@ -2054,13 +2054,13 @@ export const SHADERSTAGE_FRAGMENT = 2;
  */
 export const SHADERSTAGE_COMPUTE = 4;
 
-// indices of commonly used bind groups
-// sorted in a way that any trailing bind groups can be unused in any render pass
-export const BINDGROUP_MESH = 0;
-export const BINDGROUP_VIEW = 1;
+// indices of commonly used bind groups, sorted from the least commonly changing to avoid internal rebinding
+export const BINDGROUP_VIEW = 0;        // view bind group, textures, samplers and uniforms
+export const BINDGROUP_MESH = 1;        // mesh bind group - textures and samplers
+export const BINDGROUP_MESH_UB = 2;     // mesh bind group - a single uniform buffer
 
 // names of bind groups
-export const bindGroupNames = ['mesh', 'view'];
+export const bindGroupNames = ['view', 'mesh', 'mesh_ub'];
 
 // name of the default uniform buffer slot in a bind group
 export const UNIFORM_BUFFER_DEFAULT_SLOT_NAME = 'default';
diff --git a/src/platform/graphics/graphics-device.js b/src/platform/graphics/graphics-device.js
index bdfd928c1..ee57634c2 100644
--- a/src/platform/graphics/graphics-device.js
+++ b/src/platform/graphics/graphics-device.js
@@ -264,6 +264,15 @@ class GraphicsDevice extends EventHandler {
       */
     textureHalfFloatRenderable;
 
+    /**
+     * True if small-float textures with format {@link PIXELFORMAT_111110F} can be used as a frame
+     * buffer. This is always true on WebGL2, but optional on WebGPU device.
+     *
+     * @type {boolean}
+     * @readonly
+     */
+    textureRG11B10Renderable = false;
+
      /**
       * True if filtering can be applied when sampling float textures.
       *
@@ -855,6 +864,12 @@ class GraphicsDevice extends EventHandler {
     /**
      * Get a renderable HDR pixel format supported by the graphics device.
      *
+     * Note:
+     * - When the `filterable` parameter is set to false, this function returns one of the supported
+     * formats on the majority of devices apart from some very old iOS and Android devices (99%).
+     * - When the `filterable` parameter is set to true, the function returns a format on a
+     * considerably lower number of devices (70%).
+     *
      * @param {number[]} [formats] - An array of pixel formats to check for support. Can contain:
      *
      * - {@link PIXELFORMAT_111110F}
diff --git a/src/platform/graphics/shader-processor.js b/src/platform/graphics/shader-processor.js
index 9319e7d19..205118a53 100644
--- a/src/platform/graphics/shader-processor.js
+++ b/src/platform/graphics/shader-processor.js
@@ -2,13 +2,13 @@ import { Debug } from '../../core/debug.js';
 import {
     BINDGROUP_MESH, uniformTypeToName, semanticToLocation,
     SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT,
-    UNIFORM_BUFFER_DEFAULT_SLOT_NAME,
     SAMPLETYPE_FLOAT, SAMPLETYPE_DEPTH, SAMPLETYPE_UNFILTERABLE_FLOAT,
     TEXTUREDIMENSION_2D, TEXTUREDIMENSION_2D_ARRAY, TEXTUREDIMENSION_CUBE, TEXTUREDIMENSION_3D,
-    TYPE_FLOAT32, TYPE_INT8, TYPE_INT16, TYPE_INT32, TYPE_FLOAT16, SAMPLETYPE_INT, SAMPLETYPE_UINT
+    TYPE_FLOAT32, TYPE_INT8, TYPE_INT16, TYPE_INT32, TYPE_FLOAT16, SAMPLETYPE_INT, SAMPLETYPE_UINT,
+    BINDGROUP_MESH_UB
 } from './constants.js';
 import { UniformFormat, UniformBufferFormat } from './uniform-buffer-format.js';
-import { BindGroupFormat, BindUniformBufferFormat, BindTextureFormat } from './bind-group-format.js';
+import { BindGroupFormat, BindTextureFormat } from './bind-group-format.js';
 
 // accepted keywords
 // TODO: 'out' keyword is not in the list, as handling it is more complicated due
@@ -269,14 +269,7 @@ class ShaderProcessor {
         });
         const meshUniformBufferFormat = meshUniforms.length ? new UniformBufferFormat(device, meshUniforms) : null;
 
-        // build mesh bind group format - start with uniform buffer
-        const uniformBufferFormats = [];
-        if (meshUniformBufferFormat) {
-            // TODO: we could optimize visibility to only stages that use any of the data
-            uniformBufferFormats.push(new BindUniformBufferFormat(UNIFORM_BUFFER_DEFAULT_SLOT_NAME, SHADERSTAGE_VERTEX | SHADERSTAGE_FRAGMENT));
-        }
-
-        // add textures uniforms
+        // build mesh bind group format - this contains the textures, but not the uniform buffer as that is a separate binding
         const textureFormats = [];
         uniformLinesSamplers.forEach((uniform) => {
             // unmatched texture uniforms go to mesh block
@@ -307,7 +300,7 @@ class ShaderProcessor {
             // validate types in else
 
         });
-        const meshBindGroupFormat = new BindGroupFormat(device, [...uniformBufferFormats, ...textureFormats]);
+        const meshBindGroupFormat = new BindGroupFormat(device, textureFormats);
 
         // generate code for uniform buffers
         let code = '';
@@ -319,7 +312,7 @@ class ShaderProcessor {
 
         // and also for generated mesh format, which is at the slot 0 of the bind group
         if (meshUniformBufferFormat) {
-            code += meshUniformBufferFormat.getShaderDeclaration(BINDGROUP_MESH, 0);
+            code += meshUniformBufferFormat.getShaderDeclaration(BINDGROUP_MESH_UB, 0);
         }
 
         // generate code for textures
diff --git a/src/platform/graphics/texture.js b/src/platform/graphics/texture.js
index 96bca13b7..877c57b4a 100644
--- a/src/platform/graphics/texture.js
+++ b/src/platform/graphics/texture.js
@@ -25,6 +25,27 @@ let id = 0;
  * A texture is a container for texel data that can be utilized in a fragment shader. Typically,
  * the texel data represents an image that is mapped over geometry.
  *
+ * Note on **HDR texture format** support:
+ * 1. **As textures**:
+ *     - float (i.e. {@link PIXELFORMAT_RGBA32F}), half-float (i.e. {@link PIXELFORMAT_RGBA16F}) and
+ * small-float ({@link PIXELFORMAT_111110F}) formats are always supported on both WebGL2 and WebGPU
+ * with point sampling.
+ *     - half-float and small-float formats are always supported on WebGL2 and WebGPU with linear
+ * sampling.
+ *     - float formats are supported on WebGL2 and WebGPU with linear sampling only if
+ * {@link GraphicsDevice#textureFloatFilterable} is true.
+ *
+ * 2. **As renderable textures** that can be used as color buffers in a {@link RenderTarget}:
+ *     - on WebGPU, rendering to float and half-float formats is always supported.
+ *     - on WebGPU, rendering to small-float format is supported only if
+ * {@link GraphicsDevice#textureRG11B10Renderable} is true.
+ *     - on WebGL2, rendering to these 3 formats formats is supported only if
+ * {@link GraphicsDevice#textureFloatRenderable} is true.
+ *     - on WebGL2, if {@link GraphicsDevice#textureFloatRenderable} is false, but
+ * {@link GraphicsDevice#textureHalfFloatRenderable} is true, rendering to half-float formats only
+ * is supported. This is the case of many mobile iOS devices.
+ *     - you can determine available renderable HDR format using
+ * {@link GraphicsDevice#getRenderableHdrFormat}.
  * @category Graphics
  */
 class Texture {
@@ -140,8 +161,6 @@ class Texture {
      * - {@link TEXTURETYPE_SWIZZLEGGGR}
      *
      * Defaults to {@link TEXTURETYPE_DEFAULT}.
-     * @param {boolean} [options.fixCubemapSeams] - Specifies whether this cubemap texture requires
-     * special seam fixing shader code to look right. Defaults to false.
      * @param {boolean} [options.flipY] - Specifies whether the texture should be flipped in the
      * Y-direction. Only affects textures with a source that is an image, canvas or video element.
      * Does not affect cubemaps, compressed textures or textures set from raw pixel data. Defaults
@@ -215,7 +234,6 @@ class Texture {
 
         this._storage = options.storage ?? false;
         this._cubemap = options.cubemap ?? false;
-        this.fixCubemapSeams = options.fixCubemapSeams ?? false;
         this._flipY = options.flipY ?? false;
         this._premultiplyAlpha = options.premultiplyAlpha ?? false;
 
diff --git a/src/platform/graphics/webgpu/webgpu-clear-renderer.js b/src/platform/graphics/webgpu/webgpu-clear-renderer.js
index 176acb2ba..6cd1c7943 100644
--- a/src/platform/graphics/webgpu/webgpu-clear-renderer.js
+++ b/src/platform/graphics/webgpu/webgpu-clear-renderer.js
@@ -4,7 +4,8 @@ import { BlendState } from "../blend-state.js";
 import {
     CULLFACE_NONE,
     PRIMITIVE_TRISTRIP, SHADERLANGUAGE_WGSL,
-    UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC4, BINDGROUP_MESH, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL
+    UNIFORMTYPE_FLOAT, UNIFORMTYPE_VEC4, BINDGROUP_MESH, CLEARFLAG_COLOR, CLEARFLAG_DEPTH, CLEARFLAG_STENCIL,
+    BINDGROUP_MESH_UB
 } from "../constants.js";
 import { Shader } from "../shader.js";
 import { DynamicBindGroup } from "../bind-group.js";
@@ -39,7 +40,7 @@ class WebgpuClearRenderer {
                 depth: f32
             }
 
-            @group(0) @binding(0) var<uniform> ubMesh : ub_mesh;
+            @group(2) @binding(0) var<uniform> ubMesh : ub_mesh;
 
             var<private> pos : array<vec2f, 4> = array<vec2f, 4>(
                 vec2(-1.0, 1.0), vec2(1.0, 1.0),
@@ -98,10 +99,13 @@ class WebgpuClearRenderer {
 
             DebugGraphics.pushGpuMarker(device, 'CLEAR-RENDERER');
 
-            // dynamic bind group for this UB
+            // dynamic bind group for the UB
             const { uniformBuffer, dynamicBindGroup } = this;
             uniformBuffer.startUpdate(dynamicBindGroup);
-            device.setBindGroup(BINDGROUP_MESH, dynamicBindGroup.bindGroup, dynamicBindGroup.offsets);
+            device.setBindGroup(BINDGROUP_MESH_UB, dynamicBindGroup.bindGroup, dynamicBindGroup.offsets);
+
+            // not using mesh bind group
+            device.setBindGroup(BINDGROUP_MESH, device.emptyBindGroup);
 
             // setup clear color
             if ((flags & CLEARFLAG_COLOR) && (renderTarget.colorBuffer || renderTarget.impl.assignedColorTexture)) {
@@ -136,7 +140,6 @@ class WebgpuClearRenderer {
 
             // render 4 vertices without vertex buffer
             device.setShader(this.shader);
-
             device.draw(primitive);
 
             DebugGraphics.popGpuMarker(device);
diff --git a/src/platform/graphics/webgpu/webgpu-graphics-device.js b/src/platform/graphics/webgpu/webgpu-graphics-device.js
index 4214f3b6e..bd96638c0 100644
--- a/src/platform/graphics/webgpu/webgpu-graphics-device.js
+++ b/src/platform/graphics/webgpu/webgpu-graphics-device.js
@@ -1,6 +1,5 @@
 import { TRACEID_RENDER_QUEUE } from '../../../core/constants.js';
 import { Debug, DebugHelper } from '../../../core/debug.js';
-import { path } from '../../../core/path.js';
 
 import {
     PIXELFORMAT_RGBA8, PIXELFORMAT_BGRA8, DEVICETYPE_WEBGPU,
@@ -29,6 +28,8 @@ import { WebgpuGpuProfiler } from './webgpu-gpu-profiler.js';
 import { WebgpuResolver } from './webgpu-resolver.js';
 import { WebgpuCompute } from './webgpu-compute.js';
 import { WebgpuBuffer } from './webgpu-buffer.js';
+import { BindGroupFormat } from '../bind-group-format.js';
+import { BindGroup } from '../bind-group.js';
 
 const _uniqueLocations = new Map();
 
@@ -72,6 +73,14 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
      */
     bindGroupFormats = [];
 
+    /**
+     * An empty bind group, used when the draw call is using a typical bind group layout based on
+     * BINDGROUP_*** constants but some bind groups are not needed, for example clear renderer.
+     *
+     * @type {BindGroup}
+     */
+    emptyBindGroup;
+
     /**
      * Current command buffer encoder.
      *
@@ -167,16 +176,9 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
         // temporary message to confirm Webgpu is being used
         Debug.log("WebgpuGraphicsDevice initialization ..");
 
-        // build a full URL from a relative path
+        // build a full URL from a relative or absolute path
         const buildUrl = (srcPath) => {
-            if (!path.isRelativePath(srcPath)) {
-                return srcPath;
-            }
-
-            const url = new URL(window.location.href);
-            url.pathname = srcPath;
-            url.search = '';
-            return url.toString();
+            return new URL(srcPath, window.location.href).toString();
         };
 
         const results = await Promise.all([
@@ -310,6 +312,10 @@ class WebgpuGraphicsDevice extends GraphicsDevice {
 
         // init dynamic buffer using 1MB allocation
         this.dynamicBuffers = new WebgpuDynamicBuffers(this, 1024 * 1024, this.limits.minUniformBufferOffsetAlignment);
+
+        // empty bind group
+        this.emptyBindGroup = new BindGroup(this, new BindGroupFormat(this, []));
+        this.emptyBindGroup.update();
     }
 
     createBackbuffer() {
diff --git a/src/platform/graphics/webgpu/webgpu-render-pipeline.js b/src/platform/graphics/webgpu/webgpu-render-pipeline.js
index 8ff449f8a..80945f1a3 100644
--- a/src/platform/graphics/webgpu/webgpu-render-pipeline.js
+++ b/src/platform/graphics/webgpu/webgpu-render-pipeline.js
@@ -7,6 +7,7 @@ import { WebgpuVertexBufferLayout } from "./webgpu-vertex-buffer-layout.js";
 import { WebgpuDebug } from "./webgpu-debug.js";
 import { WebgpuPipeline } from "./webgpu-pipeline.js";
 import { DebugGraphics } from "../debug-graphics.js";
+import { bindGroupNames } from "../constants.js";
 
 let _pipelineId = 0;
 
@@ -120,6 +121,12 @@ class WebgpuRenderPipeline extends WebgpuPipeline {
 
         Debug.assert(bindGroupFormats.length <= 3);
 
+        // all bind groups must be set as the WebGPU layout cannot have skipped indices. Not having a bind
+        // group would assign incorrect slots to the following bind groups, causing a validation errors.
+        Debug.assert(bindGroupFormats[0], `BindGroup with index 0 [${bindGroupNames[0]}] is not set.`);
+        Debug.assert(bindGroupFormats[1], `BindGroup with index 1 [${bindGroupNames[1]}] is not set.`);
+        Debug.assert(bindGroupFormats[2], `BindGroup with index 2 [${bindGroupNames[2]}] is not set.`);
+
         // render pipeline unique hash
         const lookupHashes = this.lookupHashes;
         lookupHashes[0] = primitive.type;
diff --git a/src/scene/graphics/env-lighting.js b/src/scene/graphics/env-lighting.js
index 4df3f4839..dd6d99566 100644
--- a/src/scene/graphics/env-lighting.js
+++ b/src/scene/graphics/env-lighting.js
@@ -9,8 +9,6 @@ import {
 } from '../../platform/graphics/constants.js';
 import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
 
-const fixCubemapSeams = true;
-
 // calculate the number of mipmap levels given texture dimensions
 const calcLevels = (width, height = 0) => {
     return 1 + Math.floor(Math.log2(Math.max(width, height)));
@@ -46,7 +44,6 @@ const createCubemap = (device, size, format, mipmaps) => {
         type: format === PIXELFORMAT_RGBA8 ? RGBA8_TYPE : TEXTURETYPE_DEFAULT,
         addressU: ADDRESS_CLAMP_TO_EDGE,
         addressV: ADDRESS_CLAMP_TO_EDGE,
-        fixCubemapSeams: fixCubemapSeams,
         mipmaps: !!mipmaps
     });
 };
@@ -108,7 +105,6 @@ class EnvLighting {
             type: format === PIXELFORMAT_RGBA8 ? RGBA8_TYPE : TEXTURETYPE_DEFAULT,
             addressU: ADDRESS_CLAMP_TO_EDGE,
             addressV: ADDRESS_CLAMP_TO_EDGE,
-            fixCubemapSeams: false,
             mipmaps: true
         });
 
diff --git a/src/scene/graphics/quad-render.js b/src/scene/graphics/quad-render.js
index b8f363f64..ab7693fd8 100644
--- a/src/scene/graphics/quad-render.js
+++ b/src/scene/graphics/quad-render.js
@@ -1,7 +1,7 @@
 import { Debug, DebugHelper } from "../../core/debug.js";
 import { Vec4 } from "../../core/math/vec4.js";
-import { BindGroup } from "../../platform/graphics/bind-group.js";
-import { BINDGROUP_MESH, PRIMITIVE_TRISTRIP } from "../../platform/graphics/constants.js";
+import { BindGroup, DynamicBindGroup } from "../../platform/graphics/bind-group.js";
+import { BINDGROUP_MESH, BINDGROUP_MESH_UB, BINDGROUP_VIEW, PRIMITIVE_TRISTRIP } from "../../platform/graphics/constants.js";
 import { DebugGraphics } from "../../platform/graphics/debug-graphics.js";
 import { ShaderProcessorOptions } from "../../platform/graphics/shader-processor-options.js";
 import { UniformBuffer } from "../../platform/graphics/uniform-buffer.js";
@@ -16,6 +16,7 @@ const _quadPrimitive = {
 
 const _tempViewport = new Vec4();
 const _tempScissor = new Vec4();
+const _dynamicBindGroup = new DynamicBindGroup();
 
 /**
  * An object that renders a quad using a {@link Shader}.
@@ -70,7 +71,7 @@ class QuadRender {
             // bind group
             const bindGroupFormat = this.shader.meshBindGroupFormat;
             Debug.assert(bindGroupFormat);
-            this.bindGroup = new BindGroup(device, bindGroupFormat, this.uniformBuffer);
+            this.bindGroup = new BindGroup(device, bindGroupFormat);
             DebugHelper.setName(this.bindGroup, `QuadRender-MeshBindGroup_${this.bindGroup.id}`);
         }
     }
@@ -120,10 +121,22 @@ class QuadRender {
 
         if (device.supportsUniformBuffers) {
 
+            // not using view bind group
+            device.setBindGroup(BINDGROUP_VIEW, device.emptyBindGroup);
+
+            // mesh bind group
             const bindGroup = this.bindGroup;
-            bindGroup.defaultUniformBuffer?.update();
             bindGroup.update();
             device.setBindGroup(BINDGROUP_MESH, bindGroup);
+
+            // dynamic uniform buffer bind group
+            const uniformBuffer = this.uniformBuffer;
+            if (uniformBuffer) {
+                uniformBuffer.update(_dynamicBindGroup);
+                device.setBindGroup(BINDGROUP_MESH_UB, _dynamicBindGroup.bindGroup, _dynamicBindGroup.offsets);
+            } else {
+                device.setBindGroup(BINDGROUP_MESH_UB, device.emptyBindGroup);
+            }
         }
 
         device.draw(_quadPrimitive);
diff --git a/src/scene/graphics/reproject-texture.js b/src/scene/graphics/reproject-texture.js
index 1c8fc4a1a..4680b6d99 100644
--- a/src/scene/graphics/reproject-texture.js
+++ b/src/scene/graphics/reproject-texture.js
@@ -483,7 +483,6 @@ function reprojectTexture(source, target, options = {}) {
     constantSource.setValue(source);
 
     const constantParams = device.scope.resolve("params");
-    const constantParams2 = device.scope.resolve("params2");
 
     const uvModParam = device.scope.resolve("uvMod");
     if (seamPixels > 0) {
@@ -500,11 +499,6 @@ function reprojectTexture(source, target, options = {}) {
     const params = [
         0,
         specularPower,
-        source.fixCubemapSeams ? 1.0 / source.width : 0.0,          // source seam scale
-        target.fixCubemapSeams ? 1.0 / target.width : 0.0           // target seam scale
-    ];
-
-    const params2 = [
         target.width * target.height * (target.cubemap ? 6 : 1),
         source.width * source.height * (source.cubemap ? 6 : 1)
     ];
@@ -530,7 +524,6 @@ function reprojectTexture(source, target, options = {}) {
             });
             params[0] = f;
             constantParams.setValue(params);
-            constantParams2.setValue(params2);
 
             drawQuadWithShader(device, renderTarget, shader, options?.rect);
 
diff --git a/src/scene/immediate/immediate-batch.js b/src/scene/immediate/immediate-batch.js
index e0bed82aa..26113dbde 100644
--- a/src/scene/immediate/immediate-batch.js
+++ b/src/scene/immediate/immediate-batch.js
@@ -91,14 +91,16 @@ class ImmediateBatch {
                 this.meshInstance = new MeshInstance(this.mesh, this.material, identityGraphNode);
             }
 
-            // clear lines when after they were rendered as their lifetime is one frame
-            this.positions.length = 0;
-            this.colors.length = 0;
-
             // inject mesh instance into visible list to be rendered
             visibleList.push(this.meshInstance);
         }
     }
+
+    clear() {
+        // clear lines after they are rendered as their lifetime is one frame
+        this.positions.length = 0;
+        this.colors.length = 0;
+    }
 }
 
 export { ImmediateBatch };
diff --git a/src/scene/immediate/immediate-batches.js b/src/scene/immediate/immediate-batches.js
index e564c8218..cd6e3716a 100644
--- a/src/scene/immediate/immediate-batches.js
+++ b/src/scene/immediate/immediate-batches.js
@@ -23,6 +23,10 @@ class ImmediateBatches {
             batch.onPreRender(visibleList, transparent);
         });
     }
+
+    clear() {
+        this.map.forEach(batch => batch.clear());
+    }
 }
 
 export { ImmediateBatches };
diff --git a/src/scene/immediate/immediate.js b/src/scene/immediate/immediate.js
index d2d9ee559..5203ffc8c 100644
--- a/src/scene/immediate/immediate.js
+++ b/src/scene/immediate/immediate.js
@@ -275,6 +275,7 @@ class Immediate {
     onPostRender() {
 
         // clean up line batches
+        this.allBatches.forEach(batch => batch.clear());
         this.allBatches.clear();
 
         // all batches need updating next frame
diff --git a/src/scene/materials/lit-material-options-builder.js b/src/scene/materials/lit-material-options-builder.js
index 78b8df0e7..c8ff4ea4f 100644
--- a/src/scene/materials/lit-material-options-builder.js
+++ b/src/scene/materials/lit-material-options-builder.js
@@ -99,7 +99,6 @@ class LitMaterialOptionsBuilder {
         litOptions.fog = material.useFog ? scene.fog : 'none';
         litOptions.gamma = material.useGammaTonemap ? scene.gammaCorrection : GAMMA_NONE;
         litOptions.toneMap = material.useGammaTonemap ? scene.toneMapping : -1;
-        litOptions.fixSeams = false;
 
         // source of reflections
         if (material.useSkybox && scene.envAtlas && scene.skybox) {
diff --git a/src/scene/materials/standard-material-options-builder.js b/src/scene/materials/standard-material-options-builder.js
index b4e35897a..03ce42a87 100644
--- a/src/scene/materials/standard-material-options-builder.js
+++ b/src/scene/materials/standard-material-options-builder.js
@@ -301,7 +301,6 @@ class StandardMaterialOptionsBuilder {
         options.litOptions.fog = stdMat.useFog ? scene.fog : 'none';
         options.litOptions.gamma = stdMat.useGammaTonemap ? scene.gammaCorrection : GAMMA_NONE;
         options.litOptions.toneMap = stdMat.useGammaTonemap ? scene.toneMapping : -1;
-        options.litOptions.fixSeams = (stdMat.cubeMap ? stdMat.cubeMap.fixCubemapSeams : false);
 
         const isPhong = stdMat.shadingModel === SPECULAR_PHONG;
 
diff --git a/src/scene/materials/standard-material.js b/src/scene/materials/standard-material.js
index 287343992..027a2bb1a 100644
--- a/src/scene/materials/standard-material.js
+++ b/src/scene/materials/standard-material.js
@@ -993,9 +993,7 @@ function _defineTex2D(name, channel = "rgb", vertexColor = true, uv = 0) {
         defaultValue: null,
         dirtyShaderFunc: (oldValue, newValue) => {
             return !!oldValue !== !!newValue ||
-                oldValue && (oldValue.type !== newValue.type ||
-                             oldValue.fixCubemapSeams !== newValue.fixCubemapSeams ||
-                             oldValue.format !== newValue.format);
+                oldValue && (oldValue.type !== newValue.type || oldValue.format !== newValue.format);
         }
     });
 
diff --git a/src/scene/mesh-instance.js b/src/scene/mesh-instance.js
index 24df3a06a..1162c9faa 100644
--- a/src/scene/mesh-instance.js
+++ b/src/scene/mesh-instance.js
@@ -59,12 +59,19 @@ class ShaderInstance {
     shader;
 
     /**
-     * A bind group storing mesh uniforms for the shader.
+     * A bind group storing mesh textures / samplers for the shader. but not the uniform buffer.
      *
      * @type {BindGroup|null}
      */
     bindGroup = null;
 
+    /**
+     * A uniform buffer storing mesh uniforms for the shader.
+     *
+     * @type {UniformBuffer|null}
+     */
+    uniformBuffer = null;
+
     /**
      * Returns the mesh bind group for the shader.
      *
@@ -79,28 +86,43 @@ class ShaderInstance {
             const shader = this.shader;
             Debug.assert(shader);
 
-            // mesh uniform buffer
-            const ubFormat = shader.meshUniformBufferFormat;
-            Debug.assert(ubFormat);
-            const uniformBuffer = new UniformBuffer(device, ubFormat, false);
-
-            // mesh bind group
             const bindGroupFormat = shader.meshBindGroupFormat;
             Debug.assert(bindGroupFormat);
-            this.bindGroup = new BindGroup(device, bindGroupFormat, uniformBuffer);
+            this.bindGroup = new BindGroup(device, bindGroupFormat);
             DebugHelper.setName(this.bindGroup, `MeshBindGroup_${this.bindGroup.id}`);
         }
 
         return this.bindGroup;
     }
 
-    destroy() {
-        const group = this.bindGroup;
-        if (group) {
-            group.defaultUniformBuffer?.destroy();
-            group.destroy();
-            this.bindGroup = null;
+    /**
+     * Returns the uniform buffer for the shader.
+     *
+     * @param {import('../platform/graphics/graphics-device.js').GraphicsDevice} device - The
+     * graphics device.
+     * @returns {UniformBuffer} - The uniform buffer.
+     */
+    getUniformBuffer(device) {
+
+        // create uniform buffer
+        if (!this.uniformBuffer) {
+            const shader = this.shader;
+            Debug.assert(shader);
+
+            const ubFormat = shader.meshUniformBufferFormat;
+            Debug.assert(ubFormat);
+            this.uniformBuffer = new UniformBuffer(device, ubFormat, false);
         }
+
+        return this.uniformBuffer;
+    }
+
+    destroy() {
+        this.bindGroup?.destroy();
+        this.bindGroup = null;
+
+        this.uniformBuffer?.destroy();
+        this.uniformBuffer = null;
     }
 }
 
diff --git a/src/scene/renderer/forward-renderer.js b/src/scene/renderer/forward-renderer.js
index e41d455cf..d386fdad5 100644
--- a/src/scene/renderer/forward-renderer.js
+++ b/src/scene/renderer/forward-renderer.js
@@ -685,14 +685,6 @@ class ForwardRenderer extends Renderer {
 
         this.setupViewport(camera, renderTarget);
 
-        // clearing
-        const clearColor = options.clearColor ?? false;
-        const clearDepth = options.clearDepth ?? false;
-        const clearStencil = options.clearStencil ?? false;
-        if (clearColor || clearDepth || clearStencil) {
-            this.clear(camera, clearColor, clearDepth, clearStencil);
-        }
-
         let visible, splitLights;
         if (layer) {
             // #if _PROFILER
@@ -748,6 +740,14 @@ class ForwardRenderer extends Renderer {
             this.setupViewUniformBuffers(viewBindGroups, this.viewUniformFormat, this.viewBindGroupFormat, viewCount);
         }
 
+        // clearing - do it after the view bind groups are set up, to avoid overriding those
+        const clearColor = options.clearColor ?? false;
+        const clearDepth = options.clearDepth ?? false;
+        const clearStencil = options.clearStencil ?? false;
+        if (clearColor || clearDepth || clearStencil) {
+            this.clear(camera, clearColor, clearDepth, clearStencil);
+        }
+
         // enable flip faces if either the camera has _flipFaces enabled or the render target has flipY enabled
         const flipFaces = !!(camera._flipFaces ^ renderTarget?.flipY);
 
diff --git a/src/scene/renderer/renderer.js b/src/scene/renderer/renderer.js
index eabdcc8fe..7f82522a9 100644
--- a/src/scene/renderer/renderer.js
+++ b/src/scene/renderer/renderer.js
@@ -24,11 +24,12 @@ import {
     SHADERSTAGE_VERTEX, SHADERSTAGE_FRAGMENT,
     SEMANTIC_ATTR,
     CULLFACE_BACK, CULLFACE_FRONT, CULLFACE_NONE,
-    TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_FLOAT, SAMPLETYPE_DEPTH
+    TEXTUREDIMENSION_2D, SAMPLETYPE_UNFILTERABLE_FLOAT, SAMPLETYPE_FLOAT, SAMPLETYPE_DEPTH,
+    BINDGROUP_MESH_UB
 } from '../../platform/graphics/constants.js';
 import { DebugGraphics } from '../../platform/graphics/debug-graphics.js';
 import { UniformBuffer } from '../../platform/graphics/uniform-buffer.js';
-import { BindGroup } from '../../platform/graphics/bind-group.js';
+import { BindGroup, DynamicBindGroup } from '../../platform/graphics/bind-group.js';
 import { UniformFormat, UniformBufferFormat } from '../../platform/graphics/uniform-buffer-format.js';
 import { BindGroupFormat, BindUniformBufferFormat, BindTextureFormat } from '../../platform/graphics/bind-group-format.js';
 
@@ -50,6 +51,7 @@ const tempSphere = new BoundingSphere();
 const _flipYMat = new Mat4().setScale(1, -1, 1);
 const _tempLightSet = new Set();
 const _tempLayerSet = new Set();
+const _dynamicBindGroup = new DynamicBindGroup();
 
 // Converts a projection matrix in OpenGL style (depth range of -1..1) to a DirectX style (depth range of 0..1).
 const _fixProjRangeMat = new Mat4().set([
@@ -866,10 +868,12 @@ class Renderer {
 
             // update mesh bind group / uniform buffer
             const meshBindGroup = shaderInstance.getBindGroup(device);
-
-            meshBindGroup.defaultUniformBuffer.update();
             meshBindGroup.update();
             device.setBindGroup(BINDGROUP_MESH, meshBindGroup);
+
+            const meshUniformBuffer = shaderInstance.getUniformBuffer(device);
+            meshUniformBuffer.update(_dynamicBindGroup);
+            device.setBindGroup(BINDGROUP_MESH_UB, _dynamicBindGroup.bindGroup, _dynamicBindGroup.offsets);
         }
     }
 
diff --git a/src/scene/shader-lib/chunks/chunks.js b/src/scene/shader-lib/chunks/chunks.js
index 9057c4288..7270c0677 100644
--- a/src/scene/shader-lib/chunks/chunks.js
+++ b/src/scene/shader-lib/chunks/chunks.js
@@ -45,8 +45,6 @@ import extensionPS from './lit/frag/extension.js';
 import extensionVS from './lit/vert/extension.js';
 import falloffInvSquaredPS from './lit/frag/falloffInvSquared.js';
 import falloffLinearPS from './lit/frag/falloffLinear.js';
-import fixCubemapSeamsNonePS from './common/frag/fixCubemapSeamsNone.js';
-import fixCubemapSeamsStretchPS from './common/frag/fixCubemapSeamsStretch.js';
 import floatUnpackingPS from './lit/frag/float-unpacking.js';
 import fogExpPS from './lit/frag/fogExp.js';
 import fogExp2PS from './lit/frag/fogExp2.js';
@@ -255,8 +253,6 @@ const shaderChunks = {
     extensionVS,
     falloffInvSquaredPS,
     falloffLinearPS,
-    fixCubemapSeamsNonePS,
-    fixCubemapSeamsStretchPS,
     floatUnpackingPS,
     fogExpPS,
     fogExp2PS,
diff --git a/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsNone.js b/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsNone.js
deleted file mode 100644
index e3e735f48..000000000
--- a/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsNone.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export default /* glsl */`
-vec3 fixSeams(vec3 vec, float mipmapIndex) {
-    return vec;
-}
-
-vec3 fixSeams(vec3 vec) {
-    return vec;
-}
-
-vec3 fixSeamsStatic(vec3 vec, float invRecMipSize) {
-    return vec;
-}
-
-vec3 calcSeam(vec3 vec) {
-    return vec3(0);
-}
-
-vec3 applySeam(vec3 vec, vec3 seam, float scale) {
-    return vec;
-}
-`;
diff --git a/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsStretch.js b/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsStretch.js
deleted file mode 100644
index c7ff02d01..000000000
--- a/src/scene/shader-lib/chunks/common/frag/fixCubemapSeamsStretch.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export default /* glsl */`
-vec3 fixSeams(vec3 vec, float mipmapIndex) {
-    vec3 avec = abs(vec);
-    float scale = 1.0 - exp2(mipmapIndex) / 128.0;
-    float M = max(max(avec.x, avec.y), avec.z);
-    if (avec.x != M) vec.x *= scale;
-    if (avec.y != M) vec.y *= scale;
-    if (avec.z != M) vec.z *= scale;
-    return vec;
-}
-
-vec3 fixSeams(vec3 vec) {
-    vec3 avec = abs(vec);
-    float scale = 1.0 - 1.0 / 128.0;
-    float M = max(max(avec.x, avec.y), avec.z);
-    if (avec.x != M) vec.x *= scale;
-    if (avec.y != M) vec.y *= scale;
-    if (avec.z != M) vec.z *= scale;
-    return vec;
-}
-
-vec3 fixSeamsStatic(vec3 vec, float invRecMipSize) {
-    vec3 avec = abs(vec);
-    float scale = invRecMipSize;
-    float M = max(max(avec.x, avec.y), avec.z);
-    if (avec.x != M) vec.x *= scale;
-    if (avec.y != M) vec.y *= scale;
-    if (avec.z != M) vec.z *= scale;
-    return vec;
-}
-
-vec3 calcSeam(vec3 vec) {
-    vec3 avec = abs(vec);
-    float M = max(avec.x, max(avec.y, avec.z));
-    return vec3(avec.x != M ? 1.0 : 0.0,
-                avec.y != M ? 1.0 : 0.0,
-                avec.z != M ? 1.0 : 0.0);
-}
-
-vec3 applySeam(vec3 vec, vec3 seam, float scale) {
-    return vec * (seam * -scale + vec3(1.0));
-}
-`;
diff --git a/src/scene/shader-lib/chunks/common/frag/reproject.js b/src/scene/shader-lib/chunks/common/frag/reproject.js
index a0963738c..d89548ac9 100644
--- a/src/scene/shader-lib/chunks/common/frag/reproject.js
+++ b/src/scene/shader-lib/chunks/common/frag/reproject.js
@@ -32,22 +32,14 @@ varying vec2 vUv0;
 // params:
 // x - target cubemap face 0..6
 // y - specular power (when prefiltering)
-// z - source cubemap seam scale (0 to disable)
-// w - target cubemap size for seam calc (0 to disable)
+// z - target image total pixels
+// w - source cubemap size
 uniform vec4 params;
 
-// params2:
-// x - target image total pixels
-// y - source cubemap size
-uniform vec2 params2;
-
 float targetFace() { return params.x; }
 float specularPower() { return params.y; }
-float sourceCubeSeamScale() { return params.z; }
-float targetCubeSeamScale() { return params.w; }
-
-float targetTotalPixels() { return params2.x; }
-float sourceTotalPixels() { return params2.y; }
+float targetTotalPixels() { return params.z; }
+float sourceTotalPixels() { return params.w; }
 
 float PI = 3.141592653589793;
 
@@ -122,7 +114,7 @@ vec2 octEncode(in vec3 v) {
 
 #ifdef CUBEMAP_SOURCE
     vec4 sampleCubemap(vec3 dir) {
-        return textureCube(sourceCube, modifySeams(dir, 1.0 - sourceCubeSeamScale()));
+        return textureCube(sourceCube, modifySeams(dir, 1.0));
     }
 
     vec4 sampleCubemap(vec2 sph) {
@@ -130,7 +122,7 @@ vec2 octEncode(in vec3 v) {
 }
 
     vec4 sampleCubemap(vec3 dir, float mipLevel) {
-        return textureCubeLodEXT(sourceCube, modifySeams(dir, 1.0 - exp2(mipLevel) * sourceCubeSeamScale()), mipLevel);
+        return textureCubeLodEXT(sourceCube, modifySeams(dir, 1.0), mipLevel);
     }
 
     vec4 sampleCubemap(vec2 sph, float mipLevel) {
@@ -195,7 +187,7 @@ vec3 getDirectionCubemap() {
         vec = vec3(-st.x, -st.y, -1);
     }
 
-    return normalize(modifySeams(vec, 1.0 / (1.0 - targetCubeSeamScale())));
+    return normalize(modifySeams(vec, 1.0));
 }
 
 mat3 matrixFromVector(vec3 n) { // frisvad
diff --git a/src/scene/shader-lib/chunks/lit/frag/reflectionCube.js b/src/scene/shader-lib/chunks/lit/frag/reflectionCube.js
index a188064ee..37e7f98ab 100644
--- a/src/scene/shader-lib/chunks/lit/frag/reflectionCube.js
+++ b/src/scene/shader-lib/chunks/lit/frag/reflectionCube.js
@@ -3,7 +3,7 @@ uniform samplerCube texture_cubeMap;
 uniform float material_reflectivity;
 
 vec3 calcReflection(vec3 reflDir, float gloss) {
-    vec3 lookupVec = fixSeams(cubeMapProject(reflDir));
+    vec3 lookupVec = cubeMapProject(reflDir);
     lookupVec.x *= -1.0;
     return $DECODE(textureCube(texture_cubeMap, lookupVec));
 }
diff --git a/src/scene/shader-lib/chunks/lit/frag/reflectionEnvHQ.js b/src/scene/shader-lib/chunks/lit/frag/reflectionEnvHQ.js
index 1782a2660..10e513c28 100644
--- a/src/scene/shader-lib/chunks/lit/frag/reflectionEnvHQ.js
+++ b/src/scene/shader-lib/chunks/lit/frag/reflectionEnvHQ.js
@@ -15,7 +15,7 @@ vec3 calcReflection(vec3 reflDir, float gloss) {
     float ilevel = floor(level);
     float flevel = level - ilevel;
 
-    vec3 sharp = $DECODE_CUBEMAP(textureCube(texture_cubeMap, fixSeams(dir)));
+    vec3 sharp = $DECODE_CUBEMAP(textureCube(texture_cubeMap, dir));
     vec3 roughA = $DECODE(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel)));
     vec3 roughB = $DECODE(texture2D(texture_envAtlas, mapRoughnessUv(uv, ilevel + 1.0)));
 
diff --git a/src/scene/shader-lib/chunks/skybox/frag/skyboxHDR.js b/src/scene/shader-lib/chunks/skybox/frag/skyboxHDR.js
index a08c59c46..288ed91a2 100644
--- a/src/scene/shader-lib/chunks/skybox/frag/skyboxHDR.js
+++ b/src/scene/shader-lib/chunks/skybox/frag/skyboxHDR.js
@@ -26,7 +26,7 @@ void main(void) {
     #endif
 
     dir.x *= -1.0;
-    vec3 linear = SKYBOX_DECODE_FNC(textureCube(texture_cubeMap, fixSeamsStatic(dir, SKYBOX_MIP)));
+    vec3 linear = SKYBOX_DECODE_FNC(textureCube(texture_cubeMap, dir));
     gl_FragColor = vec4(gammaCorrectOutput(toneMap(processEnvironment(linear))), 1.0);
 }
 `;
diff --git a/src/scene/shader-lib/programs/lit-shader-options.js b/src/scene/shader-lib/programs/lit-shader-options.js
index 85b20ca67..c44e4014c 100644
--- a/src/scene/shader-lib/programs/lit-shader-options.js
+++ b/src/scene/shader-lib/programs/lit-shader-options.js
@@ -266,14 +266,6 @@ class LitShaderOptions {
      */
     toneMap = -1;
 
-    /**
-     * If cubemaps require seam fixing (see the `fixCubemapSeams` property of the options object
-     * passed to the {@link Texture} constructor).
-     *
-     * @type {boolean}
-     */
-    fixSeams = false;
-
     /**
      * One of "envAtlasHQ", "envAtlas", "cubeMap", "sphereMap".
      *
diff --git a/src/scene/shader-lib/programs/lit-shader.js b/src/scene/shader-lib/programs/lit-shader.js
index 028dc6a85..24d02b8e7 100644
--- a/src/scene/shader-lib/programs/lit-shader.js
+++ b/src/scene/shader-lib/programs/lit-shader.js
@@ -780,7 +780,6 @@ class LitShader {
         }
 
         if (options.reflectionSource === 'envAtlasHQ') {
-            func.append(options.fixSeams ? chunks.fixCubemapSeamsStretchPS : chunks.fixCubemapSeamsNonePS);
             func.append(chunks.envAtlasPS);
             func.append(chunks.reflectionEnvHQPS
                 .replace(/\$DECODE_CUBEMAP/g, ChunkUtils.decodeFunc(options.reflectionCubemapEncoding))
@@ -790,7 +789,6 @@ class LitShader {
             func.append(chunks.envAtlasPS);
             func.append(chunks.reflectionEnvPS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
         } else if (options.reflectionSource === 'cubeMap') {
-            func.append(options.fixSeams ? chunks.fixCubemapSeamsStretchPS : chunks.fixCubemapSeamsNonePS);
             func.append(chunks.reflectionCubePS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
         } else if (options.reflectionSource === 'sphereMap') {
             func.append(chunks.reflectionSpherePS.replace(/\$DECODE/g, ChunkUtils.decodeFunc(options.reflectionEncoding)));
diff --git a/src/scene/shader-lib/programs/skybox.js b/src/scene/shader-lib/programs/skybox.js
index fbe7141b0..6b32c7f40 100644
--- a/src/scene/shader-lib/programs/skybox.js
+++ b/src/scene/shader-lib/programs/skybox.js
@@ -6,8 +6,6 @@ import { ShaderUtils } from '../../../platform/graphics/shader-utils.js';
 import { ShaderGenerator } from './shader-generator.js';
 import { SKYTYPE_INFINITE } from '../../constants.js';
 
-const mip2size = [128, 64, /* 32 */ 16, 8, 4, 2];
-
 const fShader = `
     #include "decodePS"
     #include "gamma"
@@ -15,7 +13,6 @@ const fShader = `
     #include "envMultiplyPS"
 
     #ifdef SKY_CUBEMAP
-        #include "cubemapSeams"
         #include "skyboxHDRPS"
     #else
         #include "sphericalPS"
@@ -38,7 +35,6 @@ class ShaderGeneratorSkybox extends ShaderGenerator {
         if (options.skymesh !== SKYTYPE_INFINITE) defines.set('SKYMESH', '');
         if (options.type === 'cubemap') {
             defines.set('SKY_CUBEMAP', '');
-            defines.set('SKYBOX_MIP', (1 - 1 / mip2size[options.mip]).toString());
         }
 
         // includes
@@ -49,7 +45,6 @@ class ShaderGeneratorSkybox extends ShaderGenerator {
         includes.set('envMultiplyPS', shaderChunks.envMultiplyPS);
 
         if (options.type === 'cubemap') {
-            includes.set('cubemapSeams', options.mip ? shaderChunks.fixCubemapSeamsStretchPS : shaderChunks.fixCubemapSeamsNonePS);
             includes.set('skyboxHDRPS', shaderChunks.skyboxHDRPS);
         } else {
             includes.set('sphericalPS', shaderChunks.sphericalPS);
diff --git a/src/scene/skybox/sky-mesh.js b/src/scene/skybox/sky-mesh.js
index 3b1152671..1a5149fd3 100644
--- a/src/scene/skybox/sky-mesh.js
+++ b/src/scene/skybox/sky-mesh.js
@@ -45,8 +45,7 @@ class SkyMesh {
 
             if (texture.cubemap) {
                 options.type = 'cubemap';
-                options.mip = texture.fixCubemapSeams ? scene.skyboxMip : 0;
-                options.fixSeams = texture.fixCubemapSeams;
+                options.mip = scene.skyboxMip;
             } else {
                 options.type = 'envAtlas';
             }
diff --git a/utils/plugins/rollup-types-fixup.mjs b/utils/plugins/rollup-types-fixup.mjs
index 37d9d181a..8bc60ebd7 100644
--- a/utils/plugins/rollup-types-fixup.mjs
+++ b/utils/plugins/rollup-types-fixup.mjs
@@ -209,7 +209,7 @@ import { Texture } from '../../platform/graphics/texture.js';
 `
     }
 }, {
-    path: `${TYPES_PATH}/framework/script/script.d.ts`,
+    path: `${TYPES_PATH}/framework/script/script-type.d.ts`,
     replacement: {
         from: 'get enabled(): boolean;',
         to: `get enabled(): boolean;
@@ -232,12 +232,12 @@ import { Texture } from '../../platform/graphics/texture.js';
      */
     postUpdate?(dt: number): void;
     /**
-     * Called when a Script that already exists in the registry gets redefined. If the new
-     * Script has a \`swap\` method in its prototype, then it will be executed to perform
+     * Called when a ScriptType that already exists in the registry gets redefined. If the new
+     * ScriptType has a \`swap\` method in its prototype, then it will be executed to perform
      * hot-reload at runtime.
-     * @param old - Old instance of the script to copy data to the new instance.
+     * @param old - Old instance of the scriptType to copy data to the new instance.
      */
-    swap?(old: Script): void;
+    swap?(old: ScriptType): void;
 `
     }
 }];


Test patch:
diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs
index a761962c7..4cd5a2a55 100644
--- a/test/framework/entity.test.mjs
+++ b/test/framework/entity.test.mjs
@@ -1,4 +1,4 @@
-import { createScript } from '../../src/framework/script/script-create.js';
+import { createScript } from '../../src/framework/script/script.js';
 import { Color } from '../../src/core/math/color.js';
 
 import { AnimComponent } from '../../src/framework/components/anim/component.js';