Skip to content

Commit

Permalink
feat: Add a set of facades for common primitive meshes, using shared …
Browse files Browse the repository at this point in the history
…geometries and with setters for builtin material properties.
  • Loading branch information
lojjic committed Feb 20, 2020
1 parent 16efb01 commit d4b309b
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 0 deletions.
45 changes: 45 additions & 0 deletions packages/troika-3d/src/facade/primitives/BoxFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { utils } from 'troika-core'
import { BoxBufferGeometry } from 'three'
import { MeshFacade } from './MeshFacade.js'

/**
* Return a singleton instance of a 1x1x1 BoxBufferGeometry
* @type {function(): BoxBufferGeometry}
*/
export const getBoxGeometry = utils.memoize(() => {
return new BoxBufferGeometry(1, 1, 1, 1, 1)
})


/**
* A simple sphere, centered on the origin.
* The `width` property controls x scale, the `height` property controls y scale, and the `depth`
* property controls z scale.
* To control the material, see {@link MeshFacade}.
*/
export class BoxFacade extends MeshFacade {
get geometry() {
return getBoxGeometry()
}

set width(width) {
this.scaleX = width
}
get width() {
return this.scaleX
}

set height(height) {
this.scaleY = height
}
get height() {
return this.scaleY
}

set depth(width) {
this.scaleZ = width
}
get depth() {
return this.scaleZ
}
}
20 changes: 20 additions & 0 deletions packages/troika-3d/src/facade/primitives/CubeFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MeshFacade } from './MeshFacade.js'
import { getBoxGeometry } from './BoxFacade.js'

/**
* A simple cube, centered on the origin.
* The `size` property sets the uniform edge length. For non-uniform boxes, use {@link BoxFacade.js#Box}
* To control the material, see {@link MeshFacade}.
*/
export class CubeFacade extends MeshFacade {
get geometry() {
return getBoxGeometry()
}

set size(size) {
this.scale = size
}
get size() {
return this.scale
}
}
127 changes: 127 additions & 0 deletions packages/troika-3d/src/facade/primitives/MeshFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Object3DFacade from '../Object3DFacade.js'
import {
BufferGeometry,
Mesh,
MeshBasicMaterial, MeshDepthMaterial, MeshDistanceMaterial,
MeshLambertMaterial, MeshMatcapMaterial, MeshNormalMaterial,
MeshPhongMaterial, MeshPhysicalMaterial,
MeshStandardMaterial, MeshToonMaterial
} from 'three'

const dummyGeometry = new BufferGeometry()
const dummyMaterial = new MeshBasicMaterial()

export const MESH_MATERIALS = {
'basic': MeshBasicMaterial,
'depth': MeshDepthMaterial,
'distance': MeshDistanceMaterial,
'lambert': MeshLambertMaterial,
'matcap': MeshMatcapMaterial,
'normal': MeshNormalMaterial,
'phong': MeshPhongMaterial,
'physical': MeshPhysicalMaterial,
'standard': MeshStandardMaterial,
'toon': MeshToonMaterial,
}



/**
* A facade for rendering a Mesh. The following properties are supported:
*
* @member {Geometry|BufferGeometry} geometry - The geometry instance to be used for this
* mesh. It's recommended to use a shared geometry instance between meshes when possible.
* @member {string|class|Material} material - The type of the material to be used for this mesh. Can either
* be a reference to a Material class, a Material instance, or one of the strings in the `MESH_MATERIALS`
* enum. Defaults to 'standard'.
* @member {boolean} autoDisposeGeometry - Whether the geometry should be automatically disposed when this
* mesh is removed from the scene. Defaults to `false`. You can set it to `true` as a memory optimization
* if the geometry is not expected to return to the scene later, but this is not generally needed.
* @member {boolean} autoDisposeMaterial - Whether the material's shader program should be automatically disposed
* when this mesh is removed from the scene. Defaults to `false`. You can set it to `true` as a memory
* optimization if the material uses a custom shader that is not expected to be used again, but this is
* not generally needed.
*
* Also, for convenience, properties of the material can be set via `material.*` shortcut properties. For example,
* passing `{"material.transparent": true, "material.opacity": 0.5}` will set the material to half-opaque
* transparency. Colors will call `set` on the Color object for that material property.
*/
export class MeshFacade extends Object3DFacade {
constructor (parent) {
super(parent, new Mesh(dummyGeometry, dummyMaterial))
this.material = 'standard'
this.autoDisposeGeometry = false
this.autoDisposeMaterial = false
this._dirtyMtlProps = null
}

afterUpdate() {
let {geometry, material, threeObject} = this

threeObject.geometry = geometry || dummyGeometry

// Resolve `material` prop to a Material instance
if (material !== this._lastMtl) {
this._lastMtl = material
if (typeof material === 'string') {
material = new (MESH_MATERIALS[material] || MeshStandardMaterial)()
}
else if (material && material.isMaterial) {
// material = material
}
else if (typeof material === 'function') {
material = new material()
}
else {
material = new MeshStandardMaterial()
}
threeObject.material = material
}

// If any of the material setters were called, sync the dirty values to the material
const dirties = this._dirtyMtlProps
if (dirties) {
threeObject.material.setValues(dirties)
this._dirtyMtlProps = null
}

super.afterUpdate()
}

destructor () {
if (this.autoDisposeGeometry) {
this.threeObject.geometry.dispose()
}
if (this.autoDisposeMaterial) {
this.threeObject.material.dispose()
}
super.destructor()
}
}

// For all of the known mesh materials, add `material.*` setters for all of their
// supported properties. The setters will update a "dirty" object which will then be
// applied to the material during afterUpdate; this lets us only deal with the specific
// material props that have been set rather than having to iterate over all props.
const ignoreMaterialProps = {type:1, id:1, uuid:1, version:1}
Object.keys(MESH_MATERIALS).forEach(key => {
let material = new MESH_MATERIALS[key]()
for (let mtlProp in material) {
if (material.hasOwnProperty(mtlProp) && !ignoreMaterialProps.hasOwnProperty(mtlProp)) {
Object.defineProperty(MeshFacade.prototype, `material.${mtlProp}`, {
enumerable: true,
configurable: true,
get() {
const dirties = this._dirtyMtlProps
return (dirties && mtlProp in dirties) ? dirties[mtlProp] : this.threeObject.material[mtlProp]
},
set(value) {
const dirties = this._dirtyMtlProps || (this._dirtyMtlProps = Object.create(null))
dirties[mtlProp] = value
}
})
}
}
})


32 changes: 32 additions & 0 deletions packages/troika-3d/src/facade/primitives/PlaneFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { utils } from 'troika-core'
import { PlaneBufferGeometry } from 'three'
import { MeshFacade } from './MeshFacade.js'

const getGeometry = utils.memoize(() => {
return new PlaneBufferGeometry(1, 1, 1, 1).rotateX(Math.PI / 2)
})

/**
* A simple rectangular plane that lays along the x-z plane, facing up, centered on the origin.
* The `width` property controls x scale and the `depth` property controls z scale.
* To control the material, see {@link MeshFacade}.
*/
export class PlaneFacade extends MeshFacade {
get geometry() {
return getGeometry()
}

set width(width) {
this.scaleX = width
}
get width() {
return this.scaleX
}

set depth(width) {
this.scaleZ = width
}
get depth() {
return this.scaleZ
}
}
43 changes: 43 additions & 0 deletions packages/troika-3d/src/facade/primitives/SphereFacade.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { utils } from 'troika-core'
import { SphereBufferGeometry } from 'three'
import { MeshFacade } from './MeshFacade.js'

function createGeometry(widthSegments, heightSegments) {
return new SphereBufferGeometry(1, widthSegments, heightSegments)
}

const geometries = Object.create(null, {
low: {
get: utils.memoize(() => createGeometry(16, 12))
},
medium: {
get: utils.memoize(() => createGeometry(32, 24))
},
high: {
get: utils.memoize(() => createGeometry(64, 48))
}
})

export function getSphereGeometry(detail) {
return geometries[detail] || geometries.medium
}

/**
* A simple sphere, centered on the origin.
* The `radius` property is an alias to the uniform `scale`. Set `scaleX/Y/Z` individually if
* you need non-uniform scaling.
* The `detail` property allows selecting a LOD; its values can be 'low', 'medium', or 'high'.
* To control the material, see {@link MeshFacade}.
*/
export class SphereFacade extends MeshFacade {
get geometry() {
return getSphereGeometry(this.detail)
}

set radius(r) {
this.scale = r
}
get radius() {
return this.scale
}
}
7 changes: 7 additions & 0 deletions packages/troika-3d/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ export {makeWorldTextureProvider} from './facade/WorldTextureProvider.js'
export {default as InstancingManager} from './facade/instancing/InstancingManager.js'
export {default as Instanceable3DFacade} from './facade/instancing/Instanceable3DFacade.js'

// Primitives
export {BoxFacade} from './facade/primitives/BoxFacade.js'
export {CubeFacade} from './facade/primitives/CubeFacade.js'
export {MeshFacade} from './facade/primitives/MeshFacade.js'
export {PlaneFacade} from './facade/primitives/PlaneFacade.js'
export {SphereFacade} from './facade/primitives/SphereFacade.js'

// React entry point
export {default as Canvas3D} from './react/Canvas3D.js'

Expand Down

0 comments on commit d4b309b

Please sign in to comment.