c962d2c6fa
* Color hash from object id attribute * feat(viewer-lib): Implemented screen space outlines based on object id gradients. Still slightly WIP, but the concept seems to be working fine * Added engine model for testing outlines update * feat(viewer-lib): Updates to object-id based gradients - Id gradients now are now binary. If they are greater than 0, they will get updated to 1. This fixes the issue where similar nearby objects that happen to have similar colors would exhibit a fainter gradient - Fixed an issue with DepthNormalIdPass where linear depth was not being used - Cleaned up the edge generator shader * feat(viewer-viewer): Added instancing support for screen space outline from color id gradients - Instances now generate a correct and unique color hash based off a batch index and gl_InstanceID when running WebGL 2.0. For WebGL1.0 it's currently not supported - Removed some unneeded non-null assertion left over from a long time ago - Extended three.js InstancedMesh in order to add a batch index variable to it. Nothing more * feat(viewer-lib): Added support for WebGL1.0 for both instanced and non instanced rendering of the color id gradients - Batcher now takes all the webgl caps instead of just mac vert uniforms and float textures - The depth-normal-id pass shader now works on both WebGL 1.0 and 2.0. - When running WebGL 1.0 we dynamically add a per instance attribute buffer with the object id in order to compute hash. If we're running WebGL 2.0 we do not do that and instead rely on the builtin gl_InstanceID so we conserve bandwidth - Fixed small type issue with the edge generation shader when running WebGL 1.0 * feat(viewer-lib): Updated the edged and shaded view modes to use the depth-normal-id pass * Restored to main version.
437 lines
14 KiB
TypeScript
437 lines
14 KiB
TypeScript
import { Material, Object3D, BufferGeometry, BufferAttribute, Box3 } from 'three'
|
|
import { NodeRenderView } from '../../index.js'
|
|
import {
|
|
AllBatchUpdateRange,
|
|
type Batch,
|
|
type BatchUpdateRange,
|
|
GeometryType,
|
|
NoneBatchUpdateRange
|
|
} from './Batch.js'
|
|
import { type DrawGroup } from './Batch.js'
|
|
import Materials from '../materials/Materials.js'
|
|
import SpeckleStandardColoredMaterial from '../materials/SpeckleStandardColoredMaterial.js'
|
|
import SpecklePointColouredMaterial from '../materials/SpecklePointColouredMaterial.js'
|
|
|
|
export abstract class Primitive<
|
|
TGeometry extends BufferGeometry = BufferGeometry,
|
|
TMaterial extends Material | Material[] = Material | Material[]
|
|
> extends Object3D {
|
|
geometry!: TGeometry
|
|
material!: TMaterial
|
|
visible!: boolean
|
|
}
|
|
|
|
export abstract class PrimitiveBatch implements Batch {
|
|
public id: string
|
|
public subtreeId: string
|
|
public renderViews: NodeRenderView[]
|
|
public batchMaterial: Material
|
|
|
|
protected abstract primitive: Primitive
|
|
protected gradientIndexBuffer: BufferAttribute
|
|
protected needsShuffle: boolean = false
|
|
|
|
abstract get geometryType(): GeometryType
|
|
abstract get bounds(): Box3
|
|
abstract get minDrawCalls(): number
|
|
abstract get triCount(): number
|
|
abstract get pointCount(): number
|
|
abstract get lineCount(): number
|
|
|
|
public get materials(): Material[] {
|
|
return this.primitive.material as Material[]
|
|
}
|
|
|
|
public get groups(): DrawGroup[] {
|
|
/** We always write to geomtry.groups via the set accessor
|
|
* which takes a DrawGroup[], so geometry.groups will always
|
|
* be an array of DrawGroup.
|
|
* Not to mention that **all our draw groupd are DrawGroup because
|
|
* they always have a materialIndex defined** by design and convention!!!
|
|
*/
|
|
return this.primitive.geometry.groups as DrawGroup[]
|
|
}
|
|
|
|
public set groups(value: DrawGroup[]) {
|
|
this.primitive.geometry.groups = value
|
|
}
|
|
|
|
public get renderObject(): Object3D {
|
|
return this.primitive
|
|
}
|
|
|
|
public get drawCalls(): number {
|
|
return this.groups.length
|
|
}
|
|
|
|
public get vertCount(): number {
|
|
return this.primitive.geometry.attributes.position.count
|
|
}
|
|
|
|
public getCount(): number {
|
|
return this.primitive.geometry.index?.count || 0
|
|
}
|
|
|
|
public setBatchMaterial(material: Material): void {
|
|
this.batchMaterial = material
|
|
}
|
|
|
|
public onUpdate() {
|
|
if (this.needsShuffle) {
|
|
this.shuffleDrawGroups()
|
|
this.needsShuffle = false
|
|
}
|
|
}
|
|
|
|
public setVisibleRange(ranges: BatchUpdateRange[]) {
|
|
/** Entire batch needs to NOT be drawn */
|
|
if (ranges.length === 1 && ranges[0] === NoneBatchUpdateRange) {
|
|
this.primitive.geometry.setDrawRange(0, 0)
|
|
/** We unset the 'visible' flag, otherwise three.js will still run pointless buffer binding commands*/
|
|
this.primitive.visible = false
|
|
return
|
|
}
|
|
/** Entire batch needs to BE drawn */
|
|
if (ranges.length === 1 && ranges[0] === AllBatchUpdateRange) {
|
|
this.primitive.geometry.setDrawRange(0, this.getCount())
|
|
this.primitive.visible = true
|
|
return
|
|
}
|
|
|
|
/** Parts of the batch need to be visible. We get the min/max offset and total count */
|
|
let minOffset = Infinity
|
|
let maxOffset = 0
|
|
ranges.forEach((range) => {
|
|
minOffset = Math.min(minOffset, range.offset)
|
|
maxOffset = Math.max(maxOffset, range.offset)
|
|
})
|
|
|
|
const offset = ranges.find((val) => val.offset === maxOffset)
|
|
this.primitive.geometry.setDrawRange(
|
|
minOffset,
|
|
maxOffset - minOffset + (offset ? offset.count : 0)
|
|
)
|
|
this.primitive.visible = true
|
|
}
|
|
|
|
public getVisibleRange(): BatchUpdateRange {
|
|
/** Entire batch is visible */
|
|
if (this.groups.length === 1 && this.primitive.visible) return AllBatchUpdateRange
|
|
/** Entire batch is hidden */
|
|
if (!this.primitive.visible) return NoneBatchUpdateRange
|
|
/** Parts of the batch are visible */
|
|
return {
|
|
offset: this.primitive.geometry.drawRange.start,
|
|
count: this.primitive.geometry.drawRange.count
|
|
}
|
|
}
|
|
|
|
public getOpaque(): BatchUpdateRange {
|
|
/** If there is any transparent or hidden group return the update range up to it's offset */
|
|
const transparentOrHiddenGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return (
|
|
Materials.isTransparent(this.materials[value.materialIndex]) ||
|
|
this.materials[value.materialIndex].visible === false
|
|
)
|
|
})
|
|
|
|
if (transparentOrHiddenGroup) {
|
|
return {
|
|
offset: 0,
|
|
count: transparentOrHiddenGroup.start
|
|
}
|
|
}
|
|
/** Entire batch is opaque */
|
|
return AllBatchUpdateRange
|
|
}
|
|
|
|
public getDepth(): BatchUpdateRange {
|
|
/** If there is any transparent or hidden group return the update range up to it's offset */
|
|
const transparentOrHiddenGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return (
|
|
Materials.isTransparent(this.materials[value.materialIndex]) ||
|
|
this.materials[value.materialIndex].visible === false ||
|
|
this.materials[value.materialIndex].colorWrite === false
|
|
)
|
|
})
|
|
|
|
if (transparentOrHiddenGroup) {
|
|
return {
|
|
offset: 0,
|
|
count: transparentOrHiddenGroup.start
|
|
}
|
|
}
|
|
/** Entire batch is opaque */
|
|
return AllBatchUpdateRange
|
|
}
|
|
|
|
public getTransparent(): BatchUpdateRange {
|
|
/** Look for a transparent group */
|
|
const transparentGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return Materials.isTransparent(this.materials[value.materialIndex])
|
|
})
|
|
/** Look for a hidden group */
|
|
const hiddenGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return this.materials[value.materialIndex].visible === false
|
|
})
|
|
/** If there is a transparent group return it's range */
|
|
if (transparentGroup) {
|
|
return {
|
|
offset: transparentGroup.start,
|
|
count:
|
|
hiddenGroup !== undefined
|
|
? hiddenGroup.start
|
|
: this.getCount() - transparentGroup.start
|
|
}
|
|
}
|
|
/** Entire batch is not transparent */
|
|
return NoneBatchUpdateRange
|
|
}
|
|
|
|
public getStencil(): BatchUpdateRange {
|
|
/** If there is a single group and it's material writes to stencil, return all */
|
|
if (this.groups.length === 1) {
|
|
if (this.materials[0].stencilWrite === true) return AllBatchUpdateRange
|
|
}
|
|
const stencilGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return this.materials[value.materialIndex].stencilWrite === true
|
|
})
|
|
if (stencilGroup) {
|
|
return {
|
|
offset: stencilGroup.start,
|
|
count: stencilGroup.count
|
|
}
|
|
}
|
|
/** No stencil group */
|
|
return NoneBatchUpdateRange
|
|
}
|
|
|
|
public setBatchBuffers(ranges: BatchUpdateRange[]): void {
|
|
let minGradientIndex = Infinity
|
|
let maxGradientIndex = 0
|
|
for (let k = 0; k < ranges.length; k++) {
|
|
const range = ranges[k]
|
|
if (range.materialOptions) {
|
|
if (
|
|
range.materialOptions.rampIndex !== undefined &&
|
|
range.materialOptions.rampWidth !== undefined
|
|
) {
|
|
const start = ranges[k].offset
|
|
const len = ranges[k].offset + ranges[k].count
|
|
/** The ramp indices specify the *begining* of each ramp color. When sampling with Nearest filter (since we don't want filtering)
|
|
* we'll always be sampling right at the edge between texels. Most GPUs will sample consistently, but some won't and we end up with
|
|
* a ton of artifacts. To avoid this, we are shifting the sampling indices so they're right on the center of each texel, so no inconsistent
|
|
* sampling can occur.
|
|
*/
|
|
|
|
const shiftedIndex =
|
|
range.materialOptions.rampIndex + 0.5 / range.materialOptions.rampWidth
|
|
const minMaxIndices = this.updateGradientIndexBufferData(
|
|
start,
|
|
range.count === Infinity
|
|
? this.primitive.geometry.attributes['gradientIndex'].array.length
|
|
: len,
|
|
shiftedIndex
|
|
)
|
|
minGradientIndex = Math.min(minGradientIndex, minMaxIndices.minIndex)
|
|
maxGradientIndex = Math.max(maxGradientIndex, minMaxIndices.maxIndex)
|
|
}
|
|
/** We need to update the texture here, because each batch uses it's own clone for any material we use on it
|
|
* because otherwise three.js won't properly update our custom uniforms
|
|
*/
|
|
if (range.materialOptions.rampTexture !== undefined) {
|
|
if (
|
|
range.material instanceof SpeckleStandardColoredMaterial ||
|
|
range.material instanceof SpecklePointColouredMaterial
|
|
) {
|
|
range.material.setGradientTexture(range.materialOptions.rampTexture)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (minGradientIndex < Infinity && maxGradientIndex > 0)
|
|
this.updateGradientIndexBuffer()
|
|
}
|
|
|
|
protected cleanMaterials() {
|
|
const materialsInUse = [
|
|
...Array.from(
|
|
new Set(
|
|
this.groups.map((value) => {
|
|
if (value.materialIndex === undefined) return undefined
|
|
return this.materials[value.materialIndex]
|
|
})
|
|
)
|
|
)
|
|
]
|
|
let k = 0
|
|
while (this.materials.length > materialsInUse.length) {
|
|
if (!materialsInUse.includes(this.materials[k])) {
|
|
this.materials.splice(k, 1)
|
|
this.groups.forEach((value: DrawGroup) => {
|
|
if (value.materialIndex === undefined) return
|
|
if (value.materialIndex > k) value.materialIndex--
|
|
})
|
|
k = 0
|
|
continue
|
|
}
|
|
k++
|
|
}
|
|
}
|
|
|
|
protected abstract getCurrentIndexBuffer(): BufferAttribute
|
|
protected abstract getNextIndexBuffer(): BufferAttribute
|
|
protected abstract shuffleMaterialOrder(a: DrawGroup, b: DrawGroup): number
|
|
|
|
private shuffleDrawGroups() {
|
|
const groups = this.groups.slice()
|
|
groups.sort(this.shuffleMaterialOrder.bind(this))
|
|
|
|
const materialOrder: Array<number> = []
|
|
groups.reduce((previousValue, currentValue) => {
|
|
if (currentValue.materialIndex !== undefined) {
|
|
if (previousValue.indexOf(currentValue.materialIndex) === -1) {
|
|
previousValue.push(currentValue.materialIndex)
|
|
}
|
|
}
|
|
return previousValue
|
|
}, materialOrder)
|
|
|
|
const grouped = []
|
|
for (let k = 0; k < materialOrder.length; k++) {
|
|
grouped.push(
|
|
groups.filter((val) => {
|
|
return val.materialIndex === materialOrder[k]
|
|
})
|
|
)
|
|
}
|
|
|
|
const sourceIBO: BufferAttribute = this.getCurrentIndexBuffer()
|
|
const targetIBO: BufferAttribute = this.getNextIndexBuffer()
|
|
const sourceIBOData: Uint16Array | Uint32Array = sourceIBO.array as
|
|
| Uint16Array
|
|
| Uint32Array
|
|
const targetIBOData: Uint16Array | Uint32Array = targetIBO.array as
|
|
| Uint16Array
|
|
| Uint32Array
|
|
const newGroups = []
|
|
const scratchRvs = this.renderViews.slice()
|
|
scratchRvs.sort((a, b) => {
|
|
return a.batchStart - b.batchStart
|
|
})
|
|
let targetIBOOffset = 0
|
|
for (let k = 0; k < grouped.length; k++) {
|
|
const materialGroup = grouped[k]
|
|
const materialGroupStart = targetIBOOffset
|
|
let materialGroupCount = 0
|
|
for (let i = 0; i < (materialGroup as []).length; i++) {
|
|
const start = materialGroup[i].start
|
|
const count = materialGroup[i].count
|
|
const subArray = sourceIBOData.subarray(start, start + count)
|
|
targetIBOData.set(subArray, targetIBOOffset)
|
|
let rvTrisCount = 0
|
|
for (let m = 0; m < scratchRvs.length; m++) {
|
|
if (
|
|
scratchRvs[m].batchStart >= start &&
|
|
scratchRvs[m].batchEnd <= start + count
|
|
) {
|
|
scratchRvs[m].setBatchData(
|
|
this.id,
|
|
targetIBOOffset + rvTrisCount,
|
|
scratchRvs[m].batchCount
|
|
)
|
|
rvTrisCount += scratchRvs[m].batchCount
|
|
scratchRvs.splice(m, 1)
|
|
m--
|
|
}
|
|
}
|
|
targetIBOOffset += count
|
|
materialGroupCount += count
|
|
}
|
|
newGroups.push({
|
|
offset: materialGroupStart,
|
|
count: materialGroupCount,
|
|
materialIndex: materialGroup[0].materialIndex
|
|
})
|
|
}
|
|
this.groups = []
|
|
for (let i = 0; i < newGroups.length; i++) {
|
|
this.primitive.geometry.addGroup(
|
|
newGroups[i].offset,
|
|
newGroups[i].count,
|
|
newGroups[i].materialIndex
|
|
)
|
|
}
|
|
|
|
this.primitive.geometry.setIndex(targetIBO)
|
|
/** Catering to typescript
|
|
* The line above literally makes sure the index is set. Absurd
|
|
*/
|
|
if (this.primitive.geometry.index) this.primitive.geometry.index.needsUpdate = true
|
|
|
|
const hiddenGroup = this.groups.find((value) => {
|
|
if (value.materialIndex === undefined) return false
|
|
return this.materials[value.materialIndex].visible === false
|
|
})
|
|
if (hiddenGroup) {
|
|
this.setVisibleRange([
|
|
{
|
|
offset: 0,
|
|
count: hiddenGroup.start
|
|
}
|
|
])
|
|
} else this.setVisibleRange([AllBatchUpdateRange])
|
|
|
|
// console.log('Final -> ', this.id, this.groups.slice())
|
|
}
|
|
|
|
protected abstract updateGradientIndexBufferData(
|
|
start: number,
|
|
end: number,
|
|
value: number
|
|
): { minIndex: number; maxIndex: number }
|
|
|
|
protected updateGradientIndexBuffer(rangeMin?: number, rangeMax?: number): void {
|
|
this.gradientIndexBuffer.updateRange = {
|
|
offset: rangeMin !== undefined ? rangeMin : 0,
|
|
count:
|
|
rangeMin !== undefined && rangeMax !== undefined ? rangeMax - rangeMin + 1 : -1
|
|
}
|
|
this.gradientIndexBuffer.needsUpdate = true
|
|
this.primitive.geometry.attributes['gradientIndex'].needsUpdate = true
|
|
}
|
|
|
|
public abstract setDrawRanges(ranges: BatchUpdateRange[]): void
|
|
|
|
public resetDrawRanges(): void {
|
|
this.primitive.visible = true
|
|
this.primitive.geometry.clearGroups()
|
|
this.primitive.geometry.addGroup(0, this.getCount(), 0)
|
|
this.primitive.geometry.setDrawRange(0, Infinity)
|
|
}
|
|
|
|
public abstract buildBatch(): Promise<void>
|
|
public abstract getRenderView(index: number): NodeRenderView | null
|
|
public abstract getMaterialAtIndex(index: number): Material | null
|
|
public getMaterial(rv: NodeRenderView): Material | null {
|
|
for (let k = 0; k < this.groups.length; k++) {
|
|
const group = this.groups[k]
|
|
if (rv.batchStart >= group.start && rv.batchEnd <= group.start + group.count) {
|
|
return this.materials[group.materialIndex]
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
public purge(): void {
|
|
this.renderViews.length = 0
|
|
this.primitive.geometry.dispose()
|
|
this.batchMaterial.dispose()
|
|
}
|
|
}
|