diff --git a/packages/troika-3d-text/src/facade/SelectionRangeRect.js b/packages/troika-3d-text/src/facade/SelectionRangeRect.js index fc46b67a..eace2acb 100644 --- a/packages/troika-3d-text/src/facade/SelectionRangeRect.js +++ b/packages/troika-3d-text/src/facade/SelectionRangeRect.js @@ -33,7 +33,6 @@ if (rad != 0.0) { ` } ) - material.instanceUniforms = ['rect', 'depthAndCurveRadius', 'diffuse'] const meshes = { normal: new Mesh( new BoxBufferGeometry(1, 1, 1).translate(0.5, 0.5, 0.5), diff --git a/packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js b/packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js index 66a77424..615eddbc 100644 --- a/packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js +++ b/packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js @@ -59,13 +59,6 @@ class UIBlockLayer3DFacade extends Instanceable3DFacade { let mesh = instanceMeshesByKey.get(meshKey) if (!mesh) { let derivedMaterial = createUIBlockLayerDerivedMaterial(material) - derivedMaterial.instanceUniforms = [ - 'diffuse', - 'uTroikaBlockSize', - 'uTroikaClipRect', - 'uTroikaCornerRadii', - 'uTroikaBorderWidth' - ] derivedMaterial.polygonOffset = !!this.depthOffset derivedMaterial.polygonOffsetFactor = derivedMaterial.polygonOffsetUnits = this.depthOffset || 0 // dispose the derived material when its base material is disposed: diff --git a/packages/troika-3d/src/facade/instancing/Instanceable3DFacade.js b/packages/troika-3d/src/facade/instancing/Instanceable3DFacade.js index 94f76211..b18259ec 100644 --- a/packages/troika-3d/src/facade/instancing/Instanceable3DFacade.js +++ b/packages/troika-3d/src/facade/instancing/Instanceable3DFacade.js @@ -24,25 +24,13 @@ import Object3DFacade from '../Object3DFacade.js' * uniforms such as colors to be varied per instance. This works with both custom * shader materials as well as the built-in materials. * - * To enable per-instance uniforms: + * To enable per-instance uniforms, use the `setInstanceUniform(name, value)` + * method to set an instance's values for the enabled uniforms: * - * 1) Declare which uniforms should be instanced by setting an `instanceUniforms` - * property on the `instancedThreeObject`'s material to an array of uniform names: + * `this.setInstanceUniform('diffuse', new Color(color))` * - * `baseObj.material.instanceUniforms = ['diffuse', 'emissive']` - * - * This typically only needs to be done once for each material. Note that for built-in - * materials, you must use the uniform names used internally by their shaders and - * not the more user-friendly material properties, for example use `'diffuse'` rather than - * `'color'` for controlling a `MeshBasicMaterial`'s color. - * - * 2) For each Instanceable3DFacade, use the `setInstanceUniform(name, value)` - * method to set that instance's values for the enabled uniforms: - * - * `this.setInstanceUniform('diffuse', new Color(color))` - * - * If an instance does not have a uniform value set this way, it will fall back to using - * the default value in the material's `uniforms` object. + * If an instance does not have a uniform value set this way, it will fall back to using + * the default value in the material's `uniforms` object. * * The uniform types that allow instancing are: `int`, `float`, `vec2`, `vec3`, and `vec4`. * Mapping from application value types such as `Vector2` or `Color` behaves similarly to @@ -72,34 +60,47 @@ class Instanceable3DFacade extends Object3DFacade { } /** + * @property {Object3D} instancedThreeObject * Sets the Mesh instance to use for batching this instance with others that * reference the same Mesh. */ - set instancedThreeObject(obj) { - if (obj !== this._instancedThreeObject) { - this._instancedThreeObject = obj - this.notifyWorld('instanceableChanged') - this._boundsChanged = true - } - } - get instancedThreeObject() { - return this._instancedThreeObject - } /** - * Sets this instance's value for a shader uniform. The uniform must also be declared - * as instanceable by setting `material.instanceUniforms = ['uniformName']`. + * Sets this instance's value for a shader uniform. * @param {String} name * @param {Number|Vector2|Vector3|Vector4|Color} value */ setInstanceUniform(name, value) { let values = this._instanceUniforms || (this._instanceUniforms = Object.create(null)) if (values[name] !== value) { + // If this is a new uniform value, add it to the Set of instance uniform names + const obj = this.instancedThreeObject + if (obj && !(name in values)) { + const names = obj._instanceUniformNames || (obj._instanceUniformNames = new Set()) + names.add(name) + } values[name] = value this.notifyWorld('instanceableUniformChanged', name) } } + afterUpdate() { + const newObj = this.instancedThreeObject + const oldObj = this._instancedObj + if (newObj !== oldObj) { + if (newObj && this._instanceUniforms) { //make sure new object tracks our instance uniforms + const names = newObj._instanceUniformNames || (newObj._instanceUniformNames = new Set()) + for (let name in this._instanceUniforms) { + names.add(name) + } + } + this._instancedObj = newObj + this.notifyWorld('instanceableChanged') + this._boundsChanged = true + } + super.afterUpdate() + } + updateMatrices() { const prevMatrixVersion = this._worldMatrixVersion diff --git a/packages/troika-3d/src/facade/instancing/InstancingDerivedMaterial.js b/packages/troika-3d/src/facade/instancing/InstancingDerivedMaterial.js index ca668aa3..568e89c3 100644 --- a/packages/troika-3d/src/facade/instancing/InstancingDerivedMaterial.js +++ b/packages/troika-3d/src/facade/instancing/InstancingDerivedMaterial.js @@ -64,8 +64,7 @@ const CACHE = new WeakMap() * The result is cached by baseMaterial+instanceUniforms so we always get the same instance * back rather than getting a clone each time and having to re-upgrade every frame. */ -export function getInstancingDerivedMaterial(baseMaterial) { - let {instanceUniforms} = baseMaterial +export function getInstancingDerivedMaterial(baseMaterial, instanceUniforms) { let instanceUniformsKey = instanceUniforms ? instanceUniforms.sort().join('|') : '' let derived = CACHE.get(baseMaterial) if (!derived || derived._instanceUniformsKey !== instanceUniformsKey) { @@ -89,7 +88,7 @@ export function getInstancingDerivedMaterial(baseMaterial) { * attributes for the builtin matrix uniforms as well as any other uniforms that * have been declared as instanceable. */ -export function upgradeShaders(vertexShader, fragmentShader, instanceUniforms=[]) { +export function upgradeShaders(vertexShader, fragmentShader, instanceUniforms) { // See what gets used let usesModelMatrix = modelMatrixRefRE.test(vertexShader) let usesModelViewMatrix = modelViewMatrixRefRE.test(vertexShader) @@ -122,24 +121,26 @@ export function upgradeShaders(vertexShader, fragmentShader, instanceUniforms=[] } // Add attributes and varyings for, and rewrite references to, instanceUniforms - instanceUniforms.forEach(name => { - let vertType = vertexUniforms[name] - let fragType = fragmentUniforms[name] - if (vertType || fragType) { - let finder = new RegExp(`\\b${name}\\b`, 'g') - vertexDeclarations.push(`attribute ${vertType || fragType} troika_${name};`) - if (vertType) { - vertexShader = vertexShader.replace(finder, attrRefReplacer) + if (instanceUniforms) { + instanceUniforms.forEach(name => { + let vertType = vertexUniforms[name] + let fragType = fragmentUniforms[name] + if (vertType || fragType) { + let finder = new RegExp(`\\b${name}\\b`, 'g') + vertexDeclarations.push(`attribute ${vertType || fragType} troika_${name};`) + if (vertType) { + vertexShader = vertexShader.replace(finder, attrRefReplacer) + } + if (fragType) { + fragmentShader = fragmentShader.replace(finder, varyingRefReplacer) + let varyingDecl = `varying ${fragType} troika_vary_${name};` + vertexDeclarations.push(varyingDecl) + fragmentDeclarations.push(varyingDecl) + vertexAssignments.push(`troika_vary_${name} = troika_${name};`) + } } - if (fragType) { - fragmentShader = fragmentShader.replace(finder, varyingRefReplacer) - let varyingDecl = `varying ${fragType} troika_vary_${name};` - vertexDeclarations.push(varyingDecl) - fragmentDeclarations.push(varyingDecl) - vertexAssignments.push(`troika_vary_${name} = troika_${name};`) - } - } - }) + }) + } // Inject vertex shader declarations and assignments vertexShader = ` diff --git a/packages/troika-3d/src/facade/instancing/InstancingManager.js b/packages/troika-3d/src/facade/instancing/InstancingManager.js index 24450b25..20c0ad58 100644 --- a/packages/troika-3d/src/facade/instancing/InstancingManager.js +++ b/packages/troika-3d/src/facade/instancing/InstancingManager.js @@ -83,7 +83,7 @@ class InstancingManager extends Group3DFacade { if (protoObject && instanceObject.visible) { // Find or create the batch object for this facade's instancedThreeObject let batchKey = this._getBatchKey(protoObject) - let instanceUniforms = protoObject.material.instanceUniforms + let instanceUniforms = this._getInstanceUniformNames(protoObject) let batchObjects = batchObjectsByKey[batchKey] || (batchObjectsByKey[batchKey] = []) let batchObject = batchObjects[batchObjects.length - 1] let batchGeometry = batchObject && batchObject.geometry @@ -202,8 +202,9 @@ class InstancingManager extends Group3DFacade { if (!this._needsRebatch) { let protoObject = facade.instancedThreeObject let batchObject = facade._instancingBatchObject - if (protoObject && batchObject && this._getBatchKey(protoObject) === this._getBatchKey(batchObject)) { - let attr = batchObject.geometry._instanceAttrs.uniforms[uniformName] + let attr + if (protoObject && batchObject && this._getBatchKey(protoObject) === this._getBatchKey(batchObject) + && (attr = batchObject.geometry._instanceAttrs.uniforms[uniformName])) { setAttributeValue(attr, facade._instancingBatchAttrOffset, facade._instanceUniforms[uniformName]) attr.version++ //skip setter } else { @@ -218,23 +219,34 @@ class InstancingManager extends Group3DFacade { let cache = this._batchKeysCache || (this._batchKeysCache = Object.create(null)) //cache results for duration of this frame let key = cache && cache[object.id] if (!key) { - let mat = object.material - let uniforms = mat.instanceUniforms - key = `${object.geometry.id}|${mat.id}|${uniforms ? uniforms.sort().join(',') : ''}` + let uniforms = this._getInstanceUniformNames(object) + key = `${object.geometry.id}|${object.material.id}|${uniforms ? uniforms.sort().join(',') : ''}` cache[object.id] = key } return key } - _getInstanceUniformSizes(material) { + _getInstanceUniformNames(object) { + let namesSet = object._instanceUniformNames + if (!namesSet) return null + let cache = this._uniformNamesCache || (this._uniformNamesCache = new Map()) + let namesArray = cache.get(namesSet) + if (!namesArray) { + namesArray = Array.from(namesSet) + cache.set(namesSet, namesArray) + } + return namesArray + } + + _getInstanceUniformSizes(material, uniformNames) { // Cache results per material for duration of this frame let cache = this._uniformSizesCache || (this._uniformSizesCache = new Map()) let result = cache.get(material) if (!result) { result = Object.create(null) - if (material.instanceUniforms) { - material.instanceUniforms.forEach(name => { - let size = getUniformItemSize(material, name) + if (uniformNames) { + uniformNames.forEach(name => { + let size = getUniformItemSize(material, name) if (size > 0) { result[name] = size } else { @@ -255,12 +267,13 @@ class InstancingManager extends Group3DFacade { throw new Error('Instanceable proto object must use a BufferGeometry') } let batchKey = this._getBatchKey(instancedObject) - let uniformSizes = this._getInstanceUniformSizes(material) + let uniformNames = this._getInstanceUniformNames(instancedObject) + let uniformSizes = this._getInstanceUniformSizes(material, uniformNames) let batchGeometry = this._batchGeometryPool.borrow(batchKey, geometry, uniformSizes) setInstanceCount(batchGeometry, 0) // Upgrade the material to one with the shader modifications for instancing - let batchMaterial = getInstancingDerivedMaterial(material) + let batchMaterial = getInstancingDerivedMaterial(material, uniformNames) let depthMaterial, distanceMaterial // Create a new mesh object to hold it all @@ -336,6 +349,7 @@ class InstancingManager extends Group3DFacade { // Clear caches from this render frame this._batchKeysCache = null + this._uniformNamesCache = null this._uniformSizesCache = null // Remove batch objects from scene diff --git a/packages/troika-examples/bezier-3d/Bezier3DFacade.js b/packages/troika-examples/bezier-3d/Bezier3DFacade.js index 3ac20a64..49b8ad3c 100644 --- a/packages/troika-examples/bezier-3d/Bezier3DFacade.js +++ b/packages/troika-examples/bezier-3d/Bezier3DFacade.js @@ -131,26 +131,9 @@ export class Bezier3DInstanceableFacade extends Instanceable3DFacade { mesh = new BezierMesh() mesh.castShadow = true //TODO - figure out how to control shadow casting mesh.material = material - mesh.material.instanceUniforms = this.getInstanceUniforms() instancingMeshes.set(material, mesh) } this.instancedThreeObject = mesh } } - - getInstanceUniforms() { - return [ - 'pointA', - 'controlA', - 'controlB', - 'pointB', - 'radius', - 'dashing', - - // Set up instancing for material color and opacity by default; users can specify additional - // material-specific uniforms by extending and overriding. TODO - find a nicer way - 'diffuse', - 'opacity' - ] - } } diff --git a/packages/troika-examples/instanceable/InstanceableSphere.js b/packages/troika-examples/instanceable/InstanceableSphere.js index df5a7197..e7249a8c 100644 --- a/packages/troika-examples/instanceable/InstanceableSphere.js +++ b/packages/troika-examples/instanceable/InstanceableSphere.js @@ -15,7 +15,6 @@ const geometry = new SphereBufferGeometry(1) // Common shared material, declaring the diffuse color as an instanceable uniform let material = new MeshPhongMaterial() -material.instanceUniforms = ['diffuse'] // Single mesh shared between all instanceables const protoObj = new Mesh(geometry, material) diff --git a/packages/troika-examples/instanceable/InstanceableSphereNoMatrix.js b/packages/troika-examples/instanceable/InstanceableSphereNoMatrix.js index 9836f7c7..45d57fc5 100644 --- a/packages/troika-examples/instanceable/InstanceableSphereNoMatrix.js +++ b/packages/troika-examples/instanceable/InstanceableSphereNoMatrix.js @@ -25,7 +25,6 @@ let customShaderMaterial = createDerivedMaterial(new MeshPhongMaterial(), { vertexDefs: 'uniform float radius;', vertexTransform: 'position *= radius;' }) -customShaderMaterial.instanceUniforms = ['radius', 'diffuse'] // Single mesh shared between all instanceables const protoObj = new Mesh(geometry, customShaderMaterial) diff --git a/packages/troika-examples/ui/CubeOfCubes.js b/packages/troika-examples/ui/CubeOfCubes.js index 5e4c3056..6518b776 100644 --- a/packages/troika-examples/ui/CubeOfCubes.js +++ b/packages/troika-examples/ui/CubeOfCubes.js @@ -5,7 +5,6 @@ import { BoxBufferGeometry, Color, Mesh, MeshStandardMaterial } from 'three' const cubeMaterial = new MeshStandardMaterial({roughness: 0.7, metalness: 0.7}) -cubeMaterial.instanceUniforms = ['diffuse'] const cubeMesh = new Mesh( new BoxBufferGeometry(1, 1, 1), cubeMaterial diff --git a/packages/troika-examples/ui2/ColorCubes.js b/packages/troika-examples/ui2/ColorCubes.js index d82fe2cb..47f44caa 100644 --- a/packages/troika-examples/ui2/ColorCubes.js +++ b/packages/troika-examples/ui2/ColorCubes.js @@ -5,7 +5,6 @@ import { BoxBufferGeometry, Color, Mesh, MeshStandardMaterial } from 'three' const cubeMaterial = new MeshStandardMaterial({roughness: 0.7, metalness: 0.7}) -cubeMaterial.instanceUniforms = ['diffuse'] const cubeMesh = new Mesh( new BoxBufferGeometry(1, 1, 1), cubeMaterial