Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"vite-plugin-static-copy": "^3.0.0"
},
"peerDependencies": {
"three": ">=0.159.0"
"three": ">=0.177.0"
},
"dependencies": {
"bvh.js": "^0.0.13"
Expand Down
2 changes: 1 addition & 1 deletion src/core/InstancedEntity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Color, ColorRepresentation, Euler, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from 'three';
import { InstancedMesh2 } from './InstancedMesh2.js';
import { InstancedMesh2 } from './InstancedMesh2.common.js';
import { UniformValue, UniformValueObj } from './utils/SquareDataTexture.js';

// TODO add other object3D methods
Expand Down
101 changes: 31 additions & 70 deletions src/core/InstancedMesh2.ts → src/core/InstancedMesh2.common.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { AttachedBindMode, BindMode, Box3, BufferAttribute, BufferGeometry, Camera, Color, ColorManagement, ColorRepresentation, DataTexture, DetachedBindMode, InstancedBufferAttribute, Material, Matrix4, Mesh, Object3D, Object3DEventMap, Scene, Skeleton, Sphere, TypedArray, Vector3, WebGLProgramParametersWithUniforms, WebGLRenderer } from 'three';
import { CustomSortCallback, OnFrustumEnterCallback } from './feature/FrustumCulling.js';

import { Entity } from './feature/Instances.js';
import { LODInfo } from './feature/LOD.js';
import { InstancedEntity } from './InstancedEntity.js';
import { BVHParams, InstancedMeshBVH } from './InstancedMeshBVH.js';
import { GLInstancedBufferAttribute } from './utils/GLInstancedBufferAttribute.js';
import { SquareDataTexture } from './utils/SquareDataTexture.js';
import { CustomSortCallback, OnFrustumEnterCallback } from './feature/FrustumCulling.js';

// TODO: Add check to not update partial texture if needsuupdate already true
// TODO: if bvh present, can override?
Expand Down Expand Up @@ -63,7 +64,7 @@ export class InstancedMesh2<
/**
* @defaultValue `InstancedMesh2`
*/
public override readonly type = 'InstancedMesh2';
public override type = 'InstancedMesh2';
/**
* Indicates if this is an `InstancedMesh2`.
*/
Expand All @@ -80,23 +81,23 @@ export class InstancedMesh2<
/**
* Texture storing matrices for instances.
*/
public matricesTexture: SquareDataTexture;
public matricesTexture: SquareDataTexture; // initialized in renderer-specific prototype
/**
* Texture storing colors for instances.
*/
public colorsTexture: SquareDataTexture = null;
public colorsTexture: SquareDataTexture = null; // initialized in renderer-specific prototype
/**
* Texture storing morph target influences for instances.
*/
public morphTexture: DataTexture = null;
/**
* Texture storing bones for instances.
*/
public boneTexture: SquareDataTexture = null;
public boneTexture: SquareDataTexture = null; // initialized in renderer-specific prototype
/**
* Texture storing custom uniforms per instance.
*/
public uniformsTexture: SquareDataTexture = null;
public uniformsTexture: SquareDataTexture = null; // initialized in renderer-specific prototype
/**
* This bounding box encloses all instances, which can be calculated with `computeBoundingBox` method.
* Bounding box isn't computed by default. It needs to be explicitly computed, otherwise it's `null`.
Expand Down Expand Up @@ -160,7 +161,7 @@ export class InstancedMesh2<
* Callback function called if an instance is inside the frustum.
*/
public onFrustumEnter: OnFrustumEnterCallback = null;
/** @internal */ _renderer: WebGLRenderer = null;
/** @internal */ _renderer: WebGLRenderer = null; // initialized in renderer-specific prototype
/** @internal */ _instancesCount = 0;
/** @internal */ _instancesArrayCount = 0;
/** @internal */ _perObjectFrustumCulled = true;
Expand All @@ -182,9 +183,16 @@ export class InstancedMesh2<
protected _createEntities: boolean;

// HACK TO MAKE IT WORK WITHOUT UPDATE CORE
/** @internal */ isInstancedMesh = true; // must be set to use instancing rendering
/** @internal */ instanceMatrix = new InstancedBufferAttribute(new Float32Array(0), 16); // must be init to avoid exception
/** @internal */ instanceColor = null; // must be null to avoid exception
/** @internal */ isInstancedMesh = true;
/** @internal */ instanceMatrix = new InstancedBufferAttribute(new Float32Array(0), 16); // overridden in renderer-specific prototype if needed
/** @internal */ instanceColor = null;

// Todo: WebGPU-specific methods
init: () => void;
onBeforeCompile: (shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer) => void;
initPositionsNode: () => void;
initColorsNode: () => void;
initBonesNode: () => void;

/**
* The capacity of the instance buffers.
Expand Down Expand Up @@ -255,8 +263,10 @@ export class InstancedMesh2<
this.availabilityArray = LOD?.availabilityArray ?? new Array(capacity * 2);
this._createEntities = createEntities;

// Only initialize common attributes here.
this.initIndexAttribute();
this.initMatricesTexture();

// Renderer-specific initialization (matricesTexture, colorsTexture, etc.) is done in prototype extension.
}

public override onBeforeShadow(renderer: WebGLRenderer, scene: Scene, camera: Camera, shadowCamera: Camera, geometry: BufferGeometry, depthMaterial: Material, group: any): void {
Expand Down Expand Up @@ -345,21 +355,6 @@ export class InstancedMesh2<
this._geometry.setAttribute('instanceIndex', this.instanceIndex as unknown as BufferAttribute);
}

protected initMatricesTexture(): void {
if (!this._parentLOD) {
this.matricesTexture = new SquareDataTexture(Float32Array, 4, 4, this._capacity);
}
}

protected initColorsTexture(): void {
if (!this._parentLOD) {
this.colorsTexture = new SquareDataTexture(Float32Array, 4, 1, this._capacity);
this.colorsTexture.colorSpace = ColorManagement.workingColorSpace;
this.colorsTexture._data.fill(1);
this.materialsNeedsUpdate();
}
}

protected materialsNeedsUpdate(): void {
if ((this.material as Material).isMaterial) {
(this.material as Material).needsUpdate = true;
Expand Down Expand Up @@ -391,55 +386,12 @@ export class InstancedMesh2<
return `ezInstancedMesh2_${this.id}_${!!this.colorsTexture}_${this._useOpacity}_${!!this.boneTexture}_${!!this.uniformsTexture}_${this._customProgramCacheKeyBase.call(this._currentMaterial)}`;
};

protected _onBeforeCompile = (shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer): void => {
if (this._onBeforeCompileBase) this._onBeforeCompileBase.call(this._currentMaterial, shader, renderer);

shader.instancing = false;

shader.defines ??= {};
shader.defines['USE_INSTANCING_INDIRECT'] = '';

shader.uniforms.matricesTexture = { value: this.matricesTexture };

if (this.uniformsTexture) {
shader.uniforms.uniformsTexture = { value: this.uniformsTexture };
const { vertex, fragment } = this.uniformsTexture.getUniformsGLSL('uniformsTexture', 'instanceIndex', 'uint');
shader.vertexShader = shader.vertexShader.replace('void main() {', vertex);
shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragment);
}

if (this.colorsTexture && shader.fragmentShader.includes('#include <color_pars_fragment>')) {
shader.defines['USE_INSTANCING_COLOR_INDIRECT'] = '';
shader.uniforms.colorsTexture = { value: this.colorsTexture };
shader.vertexShader = shader.vertexShader.replace('<color_vertex>', '<instanced_color_vertex>');

if (shader.vertexColors) {
shader.defines['USE_VERTEX_COLOR'] = '';
}

if (this._useOpacity) {
shader.defines['USE_COLOR_ALPHA'] = '';
} else {
shader.defines['USE_COLOR'] = '';
}
}

if (this.boneTexture) {
shader.defines['USE_SKINNING'] = '';
shader.defines['USE_INSTANCING_SKINNING'] = '';
shader.uniforms.bindMatrix = { value: this.bindMatrix };
shader.uniforms.bindMatrixInverse = { value: this.bindMatrixInverse };
shader.uniforms.bonesPerInstance = { value: this.skeleton.bones.length };
shader.uniforms.boneTexture = { value: this.boneTexture };
}
};

protected patchMaterial(renderer: WebGLRenderer, material: Material): void {
this._currentMaterial = material;
this._customProgramCacheKeyBase = material.customProgramCacheKey; // avoid .bind(material); to prevent memory leak
this._onBeforeCompileBase = material.onBeforeCompile;
material.customProgramCacheKey = this._customProgramCacheKey;
material.onBeforeCompile = this._onBeforeCompile;
// material.onBeforeCompile = this._onBeforeCompile;

const propertiesBase = renderer.properties;

Expand Down Expand Up @@ -677,6 +629,15 @@ export class InstancedMesh2<
this.colorsTexture.enqueueUpdate(id);
}

/**
* Initializes the colors texture for the instances.
* This method should be implemented in the renderer-specific prototype.
* @throws Error if not implemented.
*/
initColorsTexture() {
throw new Error('Method not implemented.');
}

/**
* Gets the color of a specific instance.
* @param id The index of the instance.
Expand Down
90 changes: 90 additions & 0 deletions src/core/InstancedMesh2.webgl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { ColorManagement, WebGLProgramParametersWithUniforms, WebGLRenderer } from 'three';

import { InstancedMesh2 } from '../core/InstancedMesh2.common.js';
import { SquareDataTexture } from './utils/SquareDataTexture.js';


/**
* @internal
* Enhances the InstancedMesh2 prototype with WebGL methods.
*/
export function extendInstancedMesh2PrototypeWebGL(): void {
// WebGL-specific member initialization
InstancedMesh2.prototype.matricesTexture = null;
InstancedMesh2.prototype.colorsTexture = null;
InstancedMesh2.prototype.boneTexture = null;
InstancedMesh2.prototype.uniformsTexture = null;
InstancedMesh2.prototype._renderer = null;
// instanceMatrix is already initialized in .common, but can be overridden if needed

InstancedMesh2.prototype.init = function(): void {
this.initMatricesTexture();
this.initColorsTexture();
// Ensure textures are updated before first render
this.matricesTexture.update(this._renderer);
this.colorsTexture?.update(this._renderer);
};

InstancedMesh2.prototype.initMatricesTexture = function(): void {
if (!this._parentLOD) {
// Only initialize if not already set
if (!this.matricesTexture) {
this.matricesTexture = new SquareDataTexture(Float32Array, 4, 4, this._capacity);
}
}
};

InstancedMesh2.prototype.initColorsTexture = function(): void {
if (!this._parentLOD) {
if (!this.colorsTexture) {
this.colorsTexture = new SquareDataTexture(Float32Array, 4, 1, this._capacity);
this.colorsTexture.colorSpace = ColorManagement.workingColorSpace;
this.colorsTexture._data.fill(1);
this.materialsNeedsUpdate();
}
}
};

InstancedMesh2.prototype.onBeforeCompile = function(shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer): void {
if (this._onBeforeCompileBase) this._onBeforeCompileBase.call(this._currentMaterial, shader, renderer);

shader.instancing = false;

shader.defines ??= {};
shader.defines['USE_INSTANCING_INDIRECT'] = '';

shader.uniforms.matricesTexture = { value: this.matricesTexture };

if (this.uniformsTexture) {
shader.uniforms.uniformsTexture = { value: this.uniformsTexture };
const { vertex, fragment } = this.uniformsTexture.getUniformsGLSL('uniformsTexture', 'instanceIndex', 'uint');
shader.vertexShader = shader.vertexShader.replace('void main() {', vertex);
shader.fragmentShader = shader.fragmentShader.replace('void main() {', fragment);
}

if (this.colorsTexture && shader.fragmentShader.includes('#include <color_pars_fragment>')) {
shader.defines['USE_INSTANCING_COLOR_INDIRECT'] = '';
shader.uniforms.colorsTexture = { value: this.colorsTexture };
shader.vertexShader = shader.vertexShader.replace('<color_vertex>', '<instanced_color_vertex>');

if (shader.vertexColors) {
shader.defines['USE_VERTEX_COLOR'] = '';
}

if (this._useOpacity) {
shader.defines['USE_COLOR_ALPHA'] = '';
} else {
shader.defines['USE_COLOR'] = '';
}
}

if (this.boneTexture) {
shader.defines['USE_SKINNING'] = '';
shader.defines['USE_INSTANCING_SKINNING'] = '';
shader.uniforms.bindMatrix = { value: this.bindMatrix };
shader.uniforms.bindMatrixInverse = { value: this.bindMatrixInverse };
shader.uniforms.bonesPerInstance = { value: this.skeleton.bones.length };
shader.uniforms.boneTexture = { value: this.boneTexture };
}
};
}
95 changes: 95 additions & 0 deletions src/core/InstancedMesh2.webgpu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { ColorManagement } from 'three';
import { MeshBasicNodeMaterial, StorageInstancedBufferAttribute, WebGPURenderer } from 'three/webgpu';

import { getBoneMatrix, getColorTexture, getInstancedMatrix } from '../shaders/tsl/nodes.js';
import { InstancedMesh2, InstancedMesh2Params } from './InstancedMesh2.common.js';
import { uniform } from 'three/tsl';

import { SquareDataTextureGPU } from './utils/SquareDataTexture.js';


/**
* Parameters for configuring an `InstancedMeshGPU` instance.
*/
export interface InstancedMeshGPUParams extends Omit<InstancedMesh2Params, 'renderer'> {
capacity?: number;

createEntities?: boolean;

allowsEuler?: boolean;

renderer?: WebGPURenderer;
}


/**
* @internal
* Enhances the InstancedMesh2 prototype with WebGPU methods.
*/
export function extendInstancedMesh2PrototypeWebGPU(): void {

InstancedMesh2.prototype.type = 'InstancedMeshGPU';

// WebGPU-specific member initialization
InstancedMesh2.prototype.matricesTexture = null; // SquareDataTextureGPU
InstancedMesh2.prototype.colorsTexture = null; // SquareDataTextureGPU
InstancedMesh2.prototype.boneTexture = null; // SquareDataTextureGPU
InstancedMesh2.prototype.uniformsTexture = null; // SquareDataTextureGPU
InstancedMesh2.prototype._renderer = null; // WebGPURenderer | any
InstancedMesh2.prototype.instanceMatrix = new StorageInstancedBufferAttribute(new Float32Array(0), 16);

InstancedMesh2.prototype.init = async function(): Promise<void> {
this._currentMaterial = new MeshBasicNodeMaterial();
this.initMatricesTexture();
this.initColorsTexture();
// Ensure textures are updated before first render
this.matricesTexture.update(this._renderer);
this.colorsTexture?.update(this._renderer);
};

InstancedMesh2.prototype.initPositionsNode = function(): void {
if (!this._parentLOD) {
this.matricesTexture = new SquareDataTextureGPU(Float32Array, 4, 4, this._capacity);
}
// Set the node for instance matrix in the material
if (this._currentMaterial) {
(this._currentMaterial as any).positionNode = getInstancedMatrix(uniform(this.matricesTexture));
}
};

InstancedMesh2.prototype.initColorsNode = function(): void {
if (!this._parentLOD) {
this.colorsTexture = new SquareDataTextureGPU(Float32Array, 4, 1, this._capacity);
this.colorsTexture.colorSpace = ColorManagement.workingColorSpace;
this.colorsTexture._data.fill(1);
this.materialsNeedsUpdate();
}
// Set the node for instance color in the material
if (this._currentMaterial) {
(this._currentMaterial as any).colorNode = getColorTexture(uniform(this.colorsTexture));
}
};

InstancedMesh2.prototype.initBonesNode = function(): void {
if (!this._parentLOD) {
this.boneTexture = new SquareDataTextureGPU(Float32Array, 4, 4, this._capacity);
this.boneTexture.colorSpace = ColorManagement.workingColorSpace;
this.boneTexture._data.fill(1);
this.materialsNeedsUpdate();
}
// Set the node for bone matrix in the material
if (this._currentMaterial) {
(this._currentMaterial as any).boneMatrixNode = getBoneMatrix(uniform(this.boneTexture));
}
};

// Ensure textures are updated before each render
InstancedMesh2.prototype.onBeforeRender = function(renderer, scene, camera, geometry, material, group): void {
this.onBeforeRender(renderer, scene, camera, geometry, material, group);
this.matricesTexture.update(renderer);
this.colorsTexture?.update(renderer);
};

}

const _defaultCapacity = 1000;
Loading