Skip to content

Commit

Permalink
perf(troika-3d-ui): make bg/border layers instanced, and move clippin…
Browse files Browse the repository at this point in the history
…g to vertex shader
  • Loading branch information
lojjic committed Apr 3, 2020
1 parent c3ff400 commit f7526f4
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 98 deletions.
52 changes: 32 additions & 20 deletions packages/troika-3d-ui/src/facade/UIBlock3DFacade.js
Expand Up @@ -9,6 +9,8 @@ import ScrollbarsFacade from './ScrollbarsFacade.js'

const raycastMesh = new Mesh(new PlaneBufferGeometry(1, 1).translate(0.5, -0.5, 0))
const tempMat4 = new Matrix4()
const tempVec4 = new Vector4(0,0,0,0)
const emptyVec4 = Object.freeze(new Vector4(0,0,0,0))
const tempPlane = new Plane()
const DEFAULT_FONT_SIZE = 16
const DEFAULT_LINE_HEIGHT = 'normal'
Expand Down Expand Up @@ -39,10 +41,12 @@ class UIBlock3DFacade extends Group3DFacade {
this.layers = new Group3DFacade(this)
this.layers.children = [null, null, null]

this._sizeVec2 = new Vector2()
this._clipRectVec4 = new Vector4()
this._borderWidthVec4 = new Vector4()
this._borderRadiiVec4 = new Vector4()
// Shared objects for passing down to layers - treated as immutable
this._sizeVec2 = Object.freeze(new Vector2())
this._clipRectVec4 = emptyVec4
this._borderWidthVec4 = emptyVec4
this._borderRadiiVec4 = emptyVec4

;(this._geomBoundingSphere = new Sphere()).version = 0
this._wasFullyClipped = true
}
Expand All @@ -55,7 +59,6 @@ class UIBlock3DFacade extends Group3DFacade {
*/
updateChildren(children) {
if (!this.isFullyClipped || !this._wasFullyClipped) {
this._wasFullyClipped = this.isFullyClipped
super.updateChildren(children)
}
}
Expand All @@ -81,6 +84,7 @@ class UIBlock3DFacade extends Group3DFacade {
parentFlexNode,
flexNodeDepth,
isFullyClipped,
_wasFullyClipped,
_borderWidthVec4,
_clipRectVec4,
_sizeVec2
Expand All @@ -101,7 +105,7 @@ class UIBlock3DFacade extends Group3DFacade {
this.y = -(offsetTop - (isAbsPos ? 0 : parentFlexNode.scrollTop))
}
if (offsetWidth !== _sizeVec2.x || offsetHeight !== _sizeVec2.y) {
_sizeVec2.set(offsetWidth, offsetHeight)
_sizeVec2 = this._sizeVec2 = Object.freeze(new Vector2(offsetWidth, offsetHeight))

// Update pre-worldmatrix bounding sphere
const sphere = this._geomBoundingSphere
Expand All @@ -111,16 +115,23 @@ class UIBlock3DFacade extends Group3DFacade {
}
}

if (!isFullyClipped) {
if (!isFullyClipped || !_wasFullyClipped) {
// Update shared vector objects for the sublayers
const radii = (hasBg || hasBorder) ? this._normalizeBorderRadius() : null
_borderWidthVec4.fromArray(borderWidth)
_clipRectVec4.set(

tempVec4.fromArray(borderWidth)
if (!tempVec4.equals(_borderWidthVec4)) {
_borderWidthVec4 = this._borderWidthVec4 = Object.freeze(tempVec4.clone())
}
tempVec4.set(
Math.max(this.clipLeft, 0),
Math.max(this.clipTop, 0),
Math.min(this.clipRight, offsetWidth),
Math.min(this.clipBottom, offsetHeight)
)
if (!tempVec4.equals(_clipRectVec4)) {
_clipRectVec4 = this._clipRectVec4 = Object.freeze(tempVec4.clone())
}

// Update rendering layers...
let bgLayer = null
Expand Down Expand Up @@ -155,7 +166,7 @@ class UIBlock3DFacade extends Group3DFacade {
borderLayer.material = borderMaterial
borderLayer.clipRect = _clipRectVec4
borderLayer.depthOffset = -flexNodeDepth - 1
borderLayer.renderOrder = flexNodeDepth + 0.1 //TODO how can we make this play with the rest of the scene?
borderLayer.renderOrder = flexNodeDepth + 1 //TODO how can we make this play with the rest of the scene?
borderLayer.castShadow = this.castShadow
borderLayer.receiveShadow = this.receiveShadow
}
Expand All @@ -169,7 +180,7 @@ class UIBlock3DFacade extends Group3DFacade {
facade: ScrollbarsFacade,
target: this
})
scrollbarsLayer.renderOrder = flexNodeDepth + 0.2 //TODO how can we make this play with the rest of the scene?
scrollbarsLayer.renderOrder = flexNodeDepth + 2 //TODO how can we make this play with the rest of the scene?
}
layers.children[2] = scrollbarsLayer

Expand All @@ -194,7 +205,7 @@ class UIBlock3DFacade extends Group3DFacade {
textChild.color = getInheritable(this, 'color')
textChild.material = this.textMaterial
textChild.depthOffset = -flexNodeDepth - 1
textChild.renderOrder = flexNodeDepth + 0.2
textChild.renderOrder = flexNodeDepth + 1
textChild.castShadow = this.castShadow
textChild.receiveShadow = this.receiveShadow
this._actualChildren = textChild //NOTE: text content will clobber any other defined children
Expand Down Expand Up @@ -231,9 +242,10 @@ class UIBlock3DFacade extends Group3DFacade {
}

super.afterUpdate()
if (!isFullyClipped) {
if (!isFullyClipped || !_wasFullyClipped) {
layers.afterUpdate()
}
this._wasFullyClipped = isFullyClipped
}

describeChildren () {
Expand All @@ -245,11 +257,11 @@ class UIBlock3DFacade extends Group3DFacade {
}

_normalizeBorderRadius() {
const {
let {
borderRadius:input,
offsetWidth=0,
offsetHeight=0,
_borderRadiiVec4:vec4
_borderRadiiVec4:prevVec4
} = this

// Normalize to four corner values
Expand Down Expand Up @@ -295,12 +307,12 @@ class UIBlock3DFacade extends Group3DFacade {
}
}

// Update the Vector4 if anything hanged
if (tl !== vec4.x || tr !== vec4.y || br !== vec4.z || bl !== vec4.w) {
vec4.set(tl, tr, br, bl)
// Update the Vector4 if anything changed
tempVec4.set(tl, tr, br, bl)
if (!tempVec4.equals(prevVec4)) {
prevVec4 = this._borderRadiiVec4 = Object.freeze(tempVec4.clone())
}

return vec4
return prevVec4
}

/**
Expand Down
105 changes: 57 additions & 48 deletions packages/troika-3d-ui/src/facade/UIBlockLayer3DFacade.js
@@ -1,9 +1,11 @@
import { Object3DFacade } from 'troika-3d'
import { Color, Mesh, MeshBasicMaterial, PlaneBufferGeometry, Vector4 } from 'three'
import { Instanceable3DFacade } from 'troika-3d'
import { Color, Mesh, MeshBasicMaterial, PlaneBufferGeometry, Vector2, Vector4 } from 'three'
import { createUIBlockLayerDerivedMaterial } from './UIBlockLayerDerivedMaterial.js'

const geometry = new PlaneBufferGeometry(1, 1).translate(0.5, -0.5, 0)
const defaultBgMaterial = new MeshBasicMaterial({color: 0})
const noclip = Object.freeze(new Vector4())
const defaultMaterial = new MeshBasicMaterial({color: 0})
const emptyVec2 = Object.freeze(new Vector2())
const emptyVec4 = Object.freeze(new Vector4(0,0,0,0))

const shadowMaterialPropDefs = {
// Create and update materials for shadows upon request:
Expand All @@ -19,53 +21,79 @@ const shadowMaterialPropDefs = {
}
}

const instanceMeshesByKey = new Map()

/**
* A single layer in a UI Block's rendering, e.g. background or border. All layers honor
* border radius, which is calculated shader-side for perfectly smooth curves at any scale,
* with antialiasing.
*
* Layer meshes are rendered via GPU instancing when possible -- specifically when they share
* the same Material instance, layering depth, and shadow behavior.
*
* You shouldn't have to use this directly; UIBlock3DFacade will create these as needed
* based on background/border styles.
*/
class UIBlockLayer3DFacade extends Object3DFacade {
class UIBlockLayer3DFacade extends Instanceable3DFacade {
constructor(parent) {
const mesh = new Mesh(geometry, defaultBgMaterial)
mesh.frustumCulled = false //TODO moot if we make this an Instanceable, otherwise need to fix culling by transformed size
Object.defineProperties(mesh, shadowMaterialPropDefs)

super(parent, mesh)
super(parent)

this._colorObj = new Color()

// Properties
// this.size = new Vector2()
// this.borderRadius = new Vector4()
// this.borderWidth = new Vector4()
// this.color = 0
// this.material = null
// this.isBorder = false
this.size = emptyVec2
this.borderRadius = emptyVec4
this.borderWidth = emptyVec4
this.color = 0
this.isBorder = false
this.material = defaultMaterial
}

afterUpdate() {
const {color} = this
let {material, depthOffset, castShadow, receiveShadow, color, renderOrder} = this
if (!material) { material = defaultMaterial }

// Ensure we're using an upgraded material
const layerMaterial = this.threeObject.material = this._getUpgradedMaterial()
// Find or create the instanced mesh
let meshKey = `${material.id}|${renderOrder}|${depthOffset}|${castShadow}|${receiveShadow}`
if (meshKey !== this._prevMeshKey) {
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:
material.addEventListener('dispose', function onDispose() {
material.removeEventListener('dispose', onDispose)
derivedMaterial.dispose()
})

layerMaterial.polygonOffset = !!this.depthOffset
layerMaterial.polygonOffsetFactor = layerMaterial.polygonOffsetUnits = this.depthOffset || 0
mesh = new Mesh(geometry, derivedMaterial)
mesh._instanceKey = meshKey
mesh.castShadow = castShadow
mesh.receiveShadow = receiveShadow
mesh.renderOrder = renderOrder
Object.defineProperties(mesh, shadowMaterialPropDefs)
instanceMeshesByKey.set(meshKey, mesh)
}
this.instancedThreeObject = mesh
this._prevMeshKey = meshKey
}

// Set material uniform values
const uniforms = layerMaterial.uniforms
uniforms.uTroikaBlockSize.value = this.size
uniforms.uTroikaCornerRadii.value = this.borderRadius
uniforms.uTroikaClipRect.value = this.clipRect || noclip
if (this.isBorder) {
uniforms.uTroikaBorderWidth.value = this.borderWidth
}
this.setInstanceUniform('uTroikaBlockSize', this.size)
this.setInstanceUniform('uTroikaCornerRadii', this.borderRadius)
this.setInstanceUniform('uTroikaClipRect', this.clipRect)
this.setInstanceUniform('uTroikaBorderWidth', this.isBorder ? this.borderWidth : emptyVec4)
if (color !== this._lastColor) {
this._colorObj.set(color)
this._lastColor = color
this.setInstanceUniform('diffuse', new Color(color))
}

super.afterUpdate()
Expand All @@ -74,25 +102,6 @@ class UIBlockLayer3DFacade extends Object3DFacade {
getBoundingSphere() {
return null //parent will handle bounding sphere and raycasting
}

_getUpgradedMaterial() {
const baseMaterial = this.material || defaultBgMaterial
let upgradedMaterial = this._upgradedMaterial
if (!upgradedMaterial || upgradedMaterial.baseMaterial !== baseMaterial) {
if (upgradedMaterial) {
upgradedMaterial.dispose()
}
upgradedMaterial = this._upgradedMaterial = createUIBlockLayerDerivedMaterial(baseMaterial, this.isBorder)
upgradedMaterial.color = this._colorObj

// dispose the derived material when its base material is disposed:
baseMaterial.addEventListener('dispose', function onDispose() {
baseMaterial.removeEventListener('dispose', onDispose)
upgradedMaterial.dispose()
})
}
return upgradedMaterial
}
}


Expand Down

0 comments on commit f7526f4

Please sign in to comment.