Skip to content

Commit

Permalink
feat(troika-3d): remove need for manually defining material.instanceU…
Browse files Browse the repository at this point in the history
…niforms

The set of instance uniforms is now tracked automatically when calling
setInstanceUniform().
  • Loading branch information
lojjic committed Feb 28, 2021
1 parent 230a87d commit a234f8c
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 90 deletions.
1 change: 0 additions & 1 deletion packages/troika-3d-text/src/facade/SelectionRangeRect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 0 additions & 7 deletions packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
59 changes: 30 additions & 29 deletions packages/troika-3d/src/facade/instancing/Instanceable3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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 = `
Expand Down
38 changes: 26 additions & 12 deletions packages/troika-3d/src/facade/instancing/InstancingManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 0 additions & 17 deletions packages/troika-examples/bezier-3d/Bezier3DFacade.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion packages/troika-examples/ui/CubeOfCubes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion packages/troika-examples/ui2/ColorCubes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a234f8c

Please sign in to comment.