Skip to content

Commit 5fd4d79

Browse files
committedJan 15, 2021
feat: add InstancedUniformsMesh class for setting shader uniforms per instance
1 parent 2ae29fa commit 5fd4d79

File tree

5 files changed

+238
-10
lines changed

5 files changed

+238
-10
lines changed
 

Diff for: ‎packages/troika-three-utils/README.md

+36
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,42 @@ mesh.customDepthMaterial = customMaterial.getDepthMaterial() //for shadows
6262
You can also declare custom `uniforms` and `defines`, inject fragment shader code to modify the output color, etc. See the JSDoc in [DerivedMaterial.js](./src/DerivedMaterial.js) for full details.
6363

6464

65+
### InstancedUniformsMesh
66+
67+
[Source code](./src/InstancedUniformsMesh.js)
68+
69+
This extends Three.js's [`InstancedMesh`](https://threejs.org/docs/#api/en/objects/InstancedMesh) to allow any of its material's shader uniforms to be set individually per instance. It behaves just like `InstancedMesh` but exposes a new `setUniformAt(uniformName, instanceIndex, value)` method.
70+
71+
When you call `setUniformAt`, the geometry and the material shaders will be automatically upgraded behind the scenes to turn that uniform into an instanced buffer attribute, filling in the other indices with the uniform's default value. You can do this for any uniform of type `float`, `vec2`, `vec3`, or `vec4`. It works both for built-in Three.js materials and also for any custom ShaderMaterial.
72+
73+
For example, here is how you could set random `emissive` and `metalness` values for each instance using a `MeshStandardMaterial`:
74+
75+
```js
76+
import { InstancedUniformsMesh } from 'troika-three-utils'
77+
78+
const count = 100
79+
const mesh = new InstancedUniformsMesh(
80+
someGeometry,
81+
new MeshStandardMaterial(),
82+
count
83+
)
84+
const color = new Color()
85+
for (let i = 0; i < count; i++) {
86+
mesh.setUniformAt('metalness', i, Math.random())
87+
mesh.setUniformAt('emissive', i, color.set(Math.random() * 0xffffff))
88+
}
89+
```
90+
91+
The type of the `value` argument should match the type of the uniform defined in the material's shader:
92+
93+
| For uniform type: | Pass a value of this type: |
94+
| ----------------- | ----------------------------------------- |
95+
| float | Number |
96+
| vec2 | `Vector2` or Array w/ length=2 |
97+
| vec3 | `Vector3` or `Color` or Array w/ length=3 |
98+
| vec4 | `Vector4` or Array w/ length=4 |
99+
100+
65101
### BezierMesh
66102

67103
_[Source code with JSDoc](./src/BezierMesh.js)_ | _[Online example](https://troika-examples.netlify.com/#bezier3d)_
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createDerivedMaterial } from './DerivedMaterial.js'
2+
import { getShaderUniformTypes } from './getShaderUniformTypes.js'
3+
import { voidMainRegExp } from './voidMainRegExp.js'
4+
5+
const precededByUniformRE = /\buniform\s+(int|float|vec[234])\s+$/
6+
const attrRefReplacer = (name, index, str) => (precededByUniformRE.test(str.substr(0, index)) ? name : `troika_${name}`)
7+
const varyingRefReplacer = (name, index, str) => (precededByUniformRE.test(str.substr(0, index)) ? name : `troika_vary_${name}`)
8+
9+
export function createInstancedUniformsDerivedMaterial (baseMaterial, uniformNames) {
10+
const derived = createDerivedMaterial(baseMaterial, {
11+
defines: {
12+
TROIKA_INSTANCED_UNIFORMS: uniformNames.sort().join('|')
13+
},
14+
15+
customRewriter ({ vertexShader, fragmentShader }) {
16+
let vertexDeclarations = []
17+
let vertexAssignments = []
18+
let fragmentDeclarations = []
19+
20+
// Find what uniforms are declared in which shader and their types
21+
let vertexUniforms = getShaderUniformTypes(vertexShader)
22+
let fragmentUniforms = getShaderUniformTypes(fragmentShader)
23+
24+
// Add attributes and varyings for, and rewrite references to, instanceUniforms
25+
uniformNames.forEach((name) => {
26+
let vertType = vertexUniforms[name]
27+
let fragType = fragmentUniforms[name]
28+
if (vertType || fragType) {
29+
let finder = new RegExp(`\\b${name}\\b`, 'g')
30+
vertexDeclarations.push(`attribute ${vertType || fragType} troika_attr_${name};`)
31+
if (vertType) {
32+
vertexShader = vertexShader.replace(finder, attrRefReplacer)
33+
}
34+
if (fragType) {
35+
fragmentShader = fragmentShader.replace(finder, varyingRefReplacer)
36+
let varyingDecl = `varying ${fragType} troika_vary_${name};`
37+
vertexDeclarations.push(varyingDecl)
38+
fragmentDeclarations.push(varyingDecl)
39+
vertexAssignments.push(`troika_vary_${name} = troika_attr_${name};`)
40+
}
41+
}
42+
})
43+
44+
// Inject vertex shader declarations and assignments
45+
vertexShader = `${vertexDeclarations.join('\n')}\n${vertexShader.replace(voidMainRegExp, `\n$&\n${vertexAssignments.join('\n')}`)}`
46+
47+
// Inject fragment shader declarations
48+
if (fragmentDeclarations.length) {
49+
fragmentShader = `${fragmentDeclarations.join('\n')}\n${fragmentShader}`
50+
}
51+
52+
return { vertexShader, fragmentShader }
53+
}
54+
})
55+
56+
derived.isInstancedUniformsMaterial = true
57+
return derived
58+
}
+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { InstancedBufferAttribute, InstancedMesh, MeshBasicMaterial } from 'three'
2+
import { getShadersForMaterial } from './getShadersForMaterial.js'
3+
import { createInstancedUniformsDerivedMaterial } from './InstancedUniformsDerivedMaterial.js'
4+
5+
const defaultMaterial = new MeshBasicMaterial()
6+
7+
export class InstancedUniformsMesh extends InstancedMesh {
8+
constructor (geometry, material, count) {
9+
super(geometry, material, count)
10+
this._instancedUniformNames = [] //treated as immutable
11+
}
12+
13+
/*
14+
* Getter/setter for automatically wrapping the user-supplied geometry with one that will
15+
* carry our extra InstancedBufferAttribute(s)
16+
*/
17+
get geometry () {
18+
return this._derivedGeometry
19+
}
20+
21+
set geometry (geometry) {
22+
// Extend the geometry so we can add our instancing attributes but inherit everything else
23+
if (geometry) {
24+
geometry = Object.create(geometry)
25+
geometry.attributes = Object.create(geometry.attributes)
26+
}
27+
this._derivedGeometry = geometry
28+
}
29+
30+
/*
31+
* Getter/setter for automatically wrapping the user-supplied material with our upgrades. We do the
32+
* wrapping lazily on _read_ rather than write to avoid unnecessary wrapping on transient values.
33+
*/
34+
get material () {
35+
let derivedMaterial = this._derivedMaterial
36+
const baseMaterial = this._baseMaterial || this._defaultMaterial || (this._defaultMaterial = defaultMaterial.clone())
37+
const uniformNames = this._instancedUniformNames
38+
if (!derivedMaterial || derivedMaterial.baseMaterial !== baseMaterial || derivedMaterial._instancedUniformNames !== uniformNames) {
39+
derivedMaterial = this._derivedMaterial = createInstancedUniformsDerivedMaterial(baseMaterial, uniformNames)
40+
derivedMaterial._instancedUniformNames = uniformNames
41+
// dispose the derived material when its base material is disposed:
42+
baseMaterial.addEventListener('dispose', function onDispose () {
43+
baseMaterial.removeEventListener('dispose', onDispose)
44+
derivedMaterial.dispose()
45+
})
46+
}
47+
return derivedMaterial
48+
}
49+
50+
set material (baseMaterial) {
51+
if (Array.isArray(baseMaterial)) {
52+
throw new Error('InstancedUniformsMesh does not support multiple materials')
53+
}
54+
// Unwrap already-derived materials
55+
while (baseMaterial && baseMaterial.isInstancedUniformsMaterial) {
56+
baseMaterial = baseMaterial.baseMaterial
57+
}
58+
this._baseMaterial = baseMaterial
59+
}
60+
61+
get customDepthMaterial () {
62+
return this.material.getDepthMaterial()
63+
}
64+
65+
get customDistanceMaterial () {
66+
return this.material.getDistanceMaterial()
67+
}
68+
69+
/**
70+
* Set the value of a shader uniform for a single instance.
71+
* @param {string} name - the name of the shader uniform
72+
* @param {number} index - the index of the instance to set the value for
73+
* @param {number|Vector2|Vector3|Vector4|Color|Array} value - the uniform value for this instance
74+
*/
75+
setUniformAt (name, index, value) {
76+
const attrs = this.geometry.attributes
77+
const attrName = `troika_attr_${name}`
78+
let attr = attrs[attrName]
79+
if (!attr) {
80+
const defaultValue = getDefaultUniformValue(this._baseMaterial, name)
81+
const itemSize = getItemSizeForValue(defaultValue)
82+
attr = attrs[attrName] = new InstancedBufferAttribute(new Float32Array(itemSize * this.count), itemSize)
83+
// Fill with default value:
84+
if (defaultValue !== null) {
85+
for (let i = 0; i < this.count; i++) {
86+
setAttributeValue(attr, i, defaultValue)
87+
}
88+
}
89+
this._instancedUniformNames = [...this._instancedUniformNames, name]
90+
}
91+
setAttributeValue(attr, index, value)
92+
attr.needsUpdate = true
93+
}
94+
}
95+
96+
function setAttributeValue (attr, index, value) {
97+
let size = attr.itemSize
98+
if (size === 1) {
99+
attr.setX(index, value)
100+
} else if (size === 2) {
101+
attr.setXY(index, value.x, value.y)
102+
} else if (size === 3) {
103+
if (value.isColor) {
104+
attr.setXYZ(index, value.r, value.g, value.b)
105+
} else {
106+
attr.setXYZ(index, value.x, value.y, value.z)
107+
}
108+
} else if (size === 4) {
109+
attr.setXYZW(index, value.x, value.y, value.z, value.w)
110+
}
111+
}
112+
113+
function getDefaultUniformValue (material, name) {
114+
// Try uniforms on the material itself, then try the builtin material shaders
115+
let uniforms = material.uniforms
116+
if (uniforms && uniforms[name]) {
117+
return uniforms[name].value
118+
}
119+
uniforms = getShadersForMaterial(material).uniforms
120+
if (uniforms && uniforms[name]) {
121+
return uniforms[name].value
122+
}
123+
return null
124+
}
125+
126+
function getItemSizeForValue (value) {
127+
return value == null ? 0
128+
: typeof value === 'number' ? 1
129+
: value.isVector2 ? 2
130+
: value.isVector3 || value.isColor ? 3
131+
: value.isVector4 ? 4
132+
: Array.isArray(value) ? value.length
133+
: 0
134+
}
135+

Diff for: ‎packages/troika-three-utils/src/getShadersForMaterial.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MATERIAL_TYPES_TO_SHADERS = {
1010
MeshBasicMaterial: 'basic',
1111
MeshLambertMaterial: 'lambert',
1212
MeshPhongMaterial: 'phong',
13-
MeshToonMaterial: 'phong',
13+
MeshToonMaterial: 'toon',
1414
MeshStandardMaterial: 'physical',
1515
MeshPhysicalMaterial: 'physical',
1616
MeshMatcapMaterial: 'matcap',
@@ -31,4 +31,4 @@ const MATERIAL_TYPES_TO_SHADERS = {
3131
export function getShadersForMaterial(material) {
3232
let builtinType = MATERIAL_TYPES_TO_SHADERS[material.type]
3333
return builtinType ? ShaderLib[builtinType] : material //TODO fallback for unknown type?
34-
}
34+
}

Diff for: ‎packages/troika-three-utils/src/index.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
// Troika Three.js Utilities exports
22

3-
4-
export {createDerivedMaterial} from './DerivedMaterial.js'
5-
export {getShadersForMaterial} from './getShadersForMaterial.js'
6-
export {getShaderUniformTypes} from './getShaderUniformTypes.js'
7-
export {expandShaderIncludes} from './expandShaderIncludes.js'
8-
export {ShaderFloatArray} from './ShaderFloatArray.js'
9-
export {voidMainRegExp} from './voidMainRegExp.js'
10-
export {BezierMesh} from './BezierMesh.js'
3+
export { createDerivedMaterial } from './DerivedMaterial.js'
4+
export { getShadersForMaterial } from './getShadersForMaterial.js'
5+
export { getShaderUniformTypes } from './getShaderUniformTypes.js'
6+
export { expandShaderIncludes } from './expandShaderIncludes.js'
7+
export { voidMainRegExp } from './voidMainRegExp.js'
8+
export { InstancedUniformsMesh } from './InstancedUniformsMesh.js'
9+
export { BezierMesh } from './BezierMesh.js'

0 commit comments

Comments
 (0)
Please sign in to comment.