src/buffer/buffer.js
/**
* @file Buffer
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @private
*/
import {
Color, Vector3, Matrix4,
FrontSide, BackSide, DoubleSide, VertexColors, NoBlending,
BufferGeometry, BufferAttribute,
UniformsUtils, UniformsLib,
Group, LineSegments, Points, Mesh,
ShaderMaterial
} from '../../lib/three.es6.js'
import { Log } from '../globals.js'
import { defaults, getTypedArray, getUintArray } from '../utils.js'
import { getShader } from '../shader/shader-utils.js'
/**
* Buffer parameter object.
* @typedef {Object} BufferParameters - buffer parameters
* @property {Boolean} opaqueBack - render back-side opaque
* @property {Boolean} dullInterior - render back-side with dull lighting
* @property {String} side - which triangle sides to render, "front" front-side,
* "back" back-side, "double" front- and back-side
* @property {Float} opacity - translucency: 1 is fully opaque, 0 is fully transparent
* @property {Boolean} depthWrite - depth write
* @property {Integer} clipNear - position of camera near/front clipping plane
* in percent of scene bounding box
* @property {Boolean} flatShaded - render flat shaded
* @property {Boolean} wireframe - render as wireframe
* @property {Float} roughness - how rough the material is, between 0 and 1
* @property {Float} metalness - how metallic the material is, between 0 and 1
* @property {Color} diffuse - diffuse color for lighting
* @property {Boolean} forceTransparent - force the material to allow transparency
* @property {Matrix4} matrix - additional transformation matrix
* @property {Boolean} disablePicking - disable picking
*/
function getThreeSide (side) {
if (side === 'front') {
return FrontSide
} else if (side === 'back') {
return BackSide
} else if (side === 'double') {
return DoubleSide
} else {
return DoubleSide
}
}
const itemSize = {
'f': 1, 'v2': 2, 'v3': 3, 'c': 3
}
function setObjectMatrix (object, matrix) {
object.matrix.copy(matrix)
object.matrix.decompose(object.position, object.quaternion, object.scale)
object.matrixWorldNeedsUpdate = true
}
/**
* Buffer class. Base class for buffers.
* @interface
*/
class Buffer {
/**
* @param {Object} data - attribute object
* @param {Float32Array} data.position - positions
* @param {Float32Array} data.color - colors
* @param {Float32Array} data.index - triangle indices
* @param {Picker} [data.picking] - picking ids
* @param {BufferParameters} params - parameters object
*/
constructor (data, params) {
const d = data || {}
const p = params || {}
this.opaqueBack = defaults(p.opaqueBack, false)
this.dullInterior = defaults(p.dullInterior, false)
this.side = defaults(p.side, 'double')
this.opacity = defaults(p.opacity, 1.0)
this.depthWrite = defaults(p.depthWrite, true)
this.clipNear = defaults(p.clipNear, 0)
this.clipRadius = defaults(p.clipRadius, 0)
this.clipCenter = defaults(p.clipCenter, new Vector3())
this.flatShaded = defaults(p.flatShaded, false)
this.background = defaults(p.background, false)
this.wireframe = defaults(p.wireframe, false)
this.roughness = defaults(p.roughness, 0.4)
this.metalness = defaults(p.metalness, 0.0)
this.diffuse = defaults(p.diffuse, 0xffffff)
this.forceTransparent = defaults(p.forceTransparent, false)
this.disablePicking = defaults(p.disablePicking, false)
this.geometry = new BufferGeometry()
this.indexVersion = 0
this.wireframeIndexVersion = -1
this.uniforms = UniformsUtils.merge([
UniformsLib.common,
{
fogColor: { value: null },
fogNear: { value: 0.0 },
fogFar: { value: 0.0 },
opacity: { value: this.opacity },
nearClip: { value: 0.0 },
clipRadius: { value: this.clipRadius },
clipCenter: { value: this.clipCenter }
},
{
emissive: { value: new Color(0x000000) },
roughness: { value: this.roughness },
metalness: { value: this.metalness }
},
UniformsLib.ambient,
UniformsLib.lights
])
this.uniforms.diffuse.value.set(this.diffuse)
this.pickingUniforms = {
nearClip: { value: 0.0 },
objectId: { value: 0 },
opacity: { value: this.opacity }
}
this.group = new Group()
this.wireframeGroup = new Group()
this.pickingGroup = new Group()
// requires Group objects to be present
this.matrix = defaults(p.matrix, new Matrix4())
//
const position = d.position || d.position1
this._positionDataSize = position ? position.length / 3 : 0
this.addAttributes({
position: { type: 'v3', value: d.position },
color: { type: 'c', value: d.color },
primitiveId: { type: 'f', value: d.primitiveId }
})
if (d.index) {
this.initIndex(d.index)
}
this.picking = d.picking
this.makeWireframeGeometry()
}
get parameters () {
return {
opaqueBack: { updateShader: true },
dullInterior: { updateShader: true },
side: { updateShader: true, property: true },
opacity: { uniform: true },
depthWrite: { property: true },
clipNear: { updateShader: true, property: true },
clipRadius: { updateShader: true, property: true, uniform: true },
clipCenter: { uniform: true },
flatShaded: { updateShader: true },
background: { updateShader: true },
wireframe: { updateVisibility: true },
roughness: { uniform: true },
metalness: { uniform: true },
diffuse: { uniform: true },
matrix: {}
}
}
set matrix (m) {
this.setMatrix(m)
}
get matrix () {
return this.group.matrix.clone()
}
get transparent () {
return this.opacity < 1 || this.forceTransparent
}
get size () {
return this._positionDataSize
}
get attributeSize () {
return this.size
}
get pickable () {
return !!this.picking && !this.disablePicking
}
get dynamic () { return true }
/**
* @abstract
*/
get vertexShader () {}
/**
* @abstract
*/
get fragmentShader () {}
setMatrix (m) {
setObjectMatrix(this.group, m)
setObjectMatrix(this.wireframeGroup, m)
setObjectMatrix(this.pickingGroup, m)
}
initIndex (index) {
this.geometry.setIndex(
new BufferAttribute(index, 1)
)
this.geometry.getIndex().setDynamic(this.dynamic)
}
makeMaterial () {
const side = getThreeSide(this.side)
const m = new ShaderMaterial({
uniforms: this.uniforms,
vertexShader: '',
fragmentShader: '',
depthTest: true,
transparent: this.transparent,
depthWrite: this.depthWrite,
lights: true,
fog: true,
side: side
})
m.vertexColors = VertexColors
m.extensions.derivatives = this.flatShaded
m.extensions.fragDepth = this.isImpostor
m.clipNear = this.clipNear
const wm = new ShaderMaterial({
uniforms: this.uniforms,
vertexShader: '',
fragmentShader: '',
depthTest: true,
transparent: this.transparent,
depthWrite: this.depthWrite,
lights: false,
fog: true,
side: side
})
wm.vertexColors = VertexColors
wm.clipNear = this.clipNear
const pm = new ShaderMaterial({
uniforms: this.pickingUniforms,
vertexShader: '',
fragmentShader: '',
depthTest: true,
transparent: false,
depthWrite: this.depthWrite,
lights: false,
fog: false,
side: side,
blending: NoBlending
})
pm.vertexColors = VertexColors
pm.extensions.fragDepth = this.isImpostor
pm.clipNear = this.clipNear
this.material = m
this.wireframeMaterial = wm
this.pickingMaterial = pm
// also sets vertexShader/fragmentShader
this.updateShader()
}
makeWireframeGeometry () {
this.makeWireframeIndex()
const geometry = this.geometry
const wireframeIndex = this.wireframeIndex
const wireframeGeometry = new BufferGeometry()
wireframeGeometry.attributes = geometry.attributes
if (wireframeIndex) {
wireframeGeometry.setIndex(
new BufferAttribute(wireframeIndex, 1)
.setDynamic(this.dynamic)
)
wireframeGeometry.setDrawRange(0, this.wireframeIndexCount)
}
this.wireframeGeometry = wireframeGeometry
}
makeWireframeIndex () {
const edges = []
function checkEdge (a, b) {
if (a > b) {
const tmp = a
a = b
b = tmp
}
const list = edges[ a ]
if (list === undefined) {
edges[ a ] = [ b ]
return true
} else if (!list.includes(b)) {
list.push(b)
return true
}
return false
}
const geometry = this.geometry
const index = geometry.index
if (!this.wireframe) {
this.wireframeIndex = new Uint16Array(0)
this.wireframeIndexCount = 0
} else if (index) {
const array = index.array
let n = array.length
if (geometry.drawRange.count !== Infinity) {
n = geometry.drawRange.count
}
let wireframeIndex
if (this.wireframeIndex && this.wireframeIndex.length > n * 2) {
wireframeIndex = this.wireframeIndex
} else {
const count = geometry.attributes.position.count
wireframeIndex = getUintArray(n * 2, count)
}
let j = 0
edges.length = 0
for (let i = 0; i < n; i += 3) {
const a = array[ i + 0 ]
const b = array[ i + 1 ]
const c = array[ i + 2 ]
if (checkEdge(a, b)) {
wireframeIndex[ j + 0 ] = a
wireframeIndex[ j + 1 ] = b
j += 2
}
if (checkEdge(b, c)) {
wireframeIndex[ j + 0 ] = b
wireframeIndex[ j + 1 ] = c
j += 2
}
if (checkEdge(c, a)) {
wireframeIndex[ j + 0 ] = c
wireframeIndex[ j + 1 ] = a
j += 2
}
}
this.wireframeIndex = wireframeIndex
this.wireframeIndexCount = j
this.wireframeIndexVersion = this.indexVersion
} else {
const n = geometry.attributes.position.count
let wireframeIndex
if (this.wireframeIndex && this.wireframeIndex.length > n * 2) {
wireframeIndex = this.wireframeIndex
} else {
wireframeIndex = getUintArray(n * 2, n)
}
for (let i = 0, j = 0; i < n; i += 3) {
wireframeIndex[ j + 0 ] = i
wireframeIndex[ j + 1 ] = i + 1
wireframeIndex[ j + 2 ] = i + 1
wireframeIndex[ j + 3 ] = i + 2
wireframeIndex[ j + 4 ] = i + 2
wireframeIndex[ j + 5 ] = i
j += 6
}
this.wireframeIndex = wireframeIndex
this.wireframeIndexCount = n * 2
this.wireframeIndexVersion = this.indexVersion
}
}
updateWireframeIndex () {
this.wireframeGeometry.setDrawRange(0, Infinity)
if (this.wireframeIndexVersion < this.indexVersion) this.makeWireframeIndex()
if (this.wireframeIndex.length > this.wireframeGeometry.index.array.length) {
this.wireframeGeometry.setIndex(
new BufferAttribute(this.wireframeIndex, 1).setDynamic(this.dynamic)
)
} else {
const index = this.wireframeGeometry.getIndex()
index.set(this.wireframeIndex)
index.needsUpdate = this.wireframeIndexCount > 0
index.updateRange.count = this.wireframeIndexCount
}
this.wireframeGeometry.setDrawRange(0, this.wireframeIndexCount)
}
getRenderOrder () {
let renderOrder = 0
if (this.isText) {
renderOrder = 1
} else if (this.transparent) {
if (this.isSurface) {
renderOrder = 3
} else {
renderOrder = 2
}
}
return renderOrder
}
_getMesh (materialName) {
if (!this.material) this.makeMaterial()
const g = this.geometry
const m = this[ materialName ]
let mesh
if (this.isLine) {
mesh = new LineSegments(g, m)
} else if (this.isPoint) {
mesh = new Points(g, m)
if (this.sortParticles) mesh.sortParticles = true
} else {
mesh = new Mesh(g, m)
}
mesh.frustumCulled = false
mesh.renderOrder = this.getRenderOrder()
return mesh
}
getMesh () {
return this._getMesh('material')
}
getWireframeMesh () {
let mesh
if (!this.material) this.makeMaterial()
if (!this.wireframeGeometry) this.makeWireframeGeometry()
mesh = new LineSegments(
this.wireframeGeometry, this.wireframeMaterial
)
mesh.frustumCulled = false
mesh.renderOrder = this.getRenderOrder()
return mesh
}
getPickingMesh () {
return this._getMesh('pickingMaterial')
}
getShader (name, type) {
return getShader(name, this.getDefines(type))
}
getVertexShader (type) {
return this.getShader(this.vertexShader, type)
}
getFragmentShader (type) {
return this.getShader(this.fragmentShader, type)
}
getDefines (type) {
const defines = {}
if (this.clipNear) {
defines.NEAR_CLIP = 1
}
if (this.clipRadius) {
defines.RADIUS_CLIP = 1
}
if (type === 'picking') {
defines.PICKING = 1
} else {
if (type === 'background' || this.background) {
defines.NOLIGHT = 1
}
if (this.flatShaded) {
defines.FLAT_SHADED = 1
}
if (this.opaqueBack) {
defines.OPAQUE_BACK = 1
}
if (this.dullInterior) {
defines.DULL_INTERIOR = 1
}
}
return defines
}
getParameters () {
const params = {}
for (let name in this.parameters) {
params[ name ] = this[ name ]
}
return params
}
addUniforms (uniforms) {
this.uniforms = UniformsUtils.merge(
[ this.uniforms, uniforms ]
)
this.pickingUniforms = UniformsUtils.merge(
[ this.pickingUniforms, uniforms ]
)
}
addAttributes (attributes) {
for (let name in attributes) {
let buf
const a = attributes[ name ]
const arraySize = this.attributeSize * itemSize[ a.type ]
if (a.value) {
if (arraySize !== a.value.length) {
Log.error('attribute value has wrong length', name)
}
buf = a.value
} else {
buf = getTypedArray('float32', arraySize)
}
this.geometry.addAttribute(
name,
new BufferAttribute(buf, itemSize[ a.type ]).setDynamic(this.dynamic)
)
}
}
updateRenderOrder () {
const renderOrder = this.getRenderOrder()
function setRenderOrder (mesh) {
mesh.renderOrder = renderOrder
}
this.group.children.forEach(setRenderOrder)
if (this.pickingGroup) {
this.pickingGroup.children.forEach(setRenderOrder)
}
}
updateShader () {
const m = this.material
const wm = this.wireframeMaterial
const pm = this.pickingMaterial
m.vertexShader = this.getVertexShader()
m.fragmentShader = this.getFragmentShader()
m.needsUpdate = true
wm.vertexShader = this.getShader('Line.vert')
wm.fragmentShader = this.getShader('Line.frag')
wm.needsUpdate = true
pm.vertexShader = this.getVertexShader('picking')
pm.fragmentShader = this.getFragmentShader('picking')
pm.needsUpdate = true
}
/**
* Set buffer parameters
* @param {BufferParameters} params - buffer parameters object
* @return {undefined}
*/
setParameters (params) {
if (!params) return
const p = params
const tp = this.parameters
const propertyData = {}
const uniformData = {}
let doShaderUpdate = false
let doVisibilityUpdate = false
for (let name in p) {
const value = p[ name ]
if (value === undefined) continue
if (tp[ name ] === undefined) continue
this[ name ] = value
if (tp[ name ].property) {
if (tp[ name ].property !== true) {
propertyData[ tp[ name ].property ] = value
} else {
propertyData[ name ] = value
}
}
if (tp[ name ].uniform) {
if (tp[ name ].uniform !== true) {
uniformData[ tp[ name ].uniform ] = value
} else {
uniformData[ name ] = value
}
}
if (tp[ name ].updateShader) {
doShaderUpdate = true
}
if (tp[ name ].updateVisibility) {
doVisibilityUpdate = true
}
if (this.dynamic && name === 'wireframe' && value === true) {
this.updateWireframeIndex()
}
if (name === 'flatShaded') {
this.material.extensions.derivatives = this.flatShaded
}
if (name === 'forceTransparent') {
propertyData.transparent = this.transparent
}
}
this.setProperties(propertyData)
this.setUniforms(uniformData)
if (doShaderUpdate) this.updateShader()
if (doVisibilityUpdate) this.setVisibility(this.visible)
}
/**
* Sets buffer attributes
* @param {Object} data - An object where the keys are the attribute names
* and the values are the attribute data.
* @example
* var buffer = new Buffer();
* buffer.setAttributes({ attrName: attrData });
*/
setAttributes (data) {
const geometry = this.geometry
const attributes = geometry.attributes
for (let name in data) {
if (name === 'picking') continue
const array = data[ name ]
const length = array.length
if (name === 'index') {
const index = geometry.getIndex()
geometry.setDrawRange(0, Infinity)
if (length > index.array.length) {
geometry.setIndex(
new BufferAttribute(array, 1).setDynamic(this.dynamic)
)
} else {
index.set(array)
index.needsUpdate = length > 0
index.updateRange.count = length
geometry.setDrawRange(0, length)
}
this.indexVersion++
if (this.wireframe) this.updateWireframeIndex()
} else {
const attribute = attributes[ name ]
if (length > attribute.array.length) {
geometry.addAttribute(
name,
new BufferAttribute(array, attribute.itemSize)
.setDynamic(this.dynamic)
)
} else {
attributes[ name ].set(array)
attributes[ name ].needsUpdate = length > 0
attributes[ name ].updateRange.count = length
}
}
}
}
setUniforms (data) {
if (!data) return
const u = this.material.uniforms
const wu = this.wireframeMaterial.uniforms
const pu = this.pickingMaterial.uniforms
for (let name in data) {
if (name === 'opacity') {
this.setProperties({ transparent: this.transparent })
}
if (u[ name ] !== undefined) {
if (u[ name ].value.isVector3) {
u[ name ].value.copy(data[ name ])
} else if (u[ name ].value.set) {
u[ name ].value.set(data[ name ])
} else {
u[ name ].value = data[ name ]
}
}
if (wu[ name ] !== undefined) {
if (wu[ name ].value.isVector3) {
wu[ name ].value.copy(data[ name ])
} else if (wu[ name ].value.set) {
wu[ name ].value.set(data[ name ])
} else {
wu[ name ].value = data[ name ]
}
}
if (pu[ name ] !== undefined) {
if (pu[ name ].value.isVector3) {
pu[ name ].value.copy(data[ name ])
} else if (pu[ name ].value.set) {
pu[ name ].value.set(data[ name ])
} else {
pu[ name ].value = data[ name ]
}
}
}
}
setProperties (data) {
if (!data) return
const m = this.material
const wm = this.wireframeMaterial
const pm = this.pickingMaterial
for (let name in data) {
let value = data[ name ]
if (name === 'transparent') {
this.updateRenderOrder()
} else if (name === 'side') {
value = getThreeSide(value)
}
if (m[ name ] !== undefined) {
m[ name ] = value
}
if (wm[ name ] !== undefined) {
wm[ name ] = value
}
if (pm[ name ] !== undefined) {
pm[ name ] = value
}
}
m.needsUpdate = true
wm.needsUpdate = true
pm.needsUpdate = true
}
/**
* Set buffer visibility
* @param {Boolean} value - visibility value
* @return {undefined}
*/
setVisibility (value) {
this.visible = value
if (this.wireframe) {
this.group.visible = false
this.wireframeGroup.visible = value
if (this.pickable) {
this.pickingGroup.visible = false
}
} else {
this.group.visible = value
this.wireframeGroup.visible = false
if (this.pickable) {
this.pickingGroup.visible = value
}
}
}
/**
* Free buffer resources
* @return {undefined}
*/
dispose () {
if (this.material) this.material.dispose()
if (this.wireframeMaterial) this.wireframeMaterial.dispose()
if (this.pickingMaterial) this.pickingMaterial.dispose()
this.geometry.dispose()
if (this.wireframeGeometry) this.wireframeGeometry.dispose()
}
}
export default Buffer