/** * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose */ import { Renderable } from './renderable' import { WebGLContext } from './webgl/context'; import { RenderableValues, BaseValues } from './renderable/schema'; import { GraphicsRenderObject, createRenderable } from './render-object'; import { Object3D } from './object3d'; import { Sphere3D } from '../mol-math/geometry'; import { Vec3 } from '../mol-math/linear-algebra'; import { BoundaryHelper } from '../mol-math/geometry/boundary-helper'; import { RuntimeContext, Task } from '../mol-task'; import { AsyncQueue } from '../mol-util/async-queue'; const boundaryHelper = new BoundaryHelper(); function calculateBoundingSphere(renderables: Renderable[], boundingSphere: Sphere3D): Sphere3D { boundaryHelper.reset(0.1); for (let i = 0, il = renderables.length; i < il; ++i) { const boundingSphere = renderables[i].values.boundingSphere.ref.value if (!boundingSphere.radius) continue; boundaryHelper.boundaryStep(boundingSphere.center, boundingSphere.radius); } boundaryHelper.finishBoundaryStep(); for (let i = 0, il = renderables.length; i < il; ++i) { const boundingSphere = renderables[i].values.boundingSphere.ref.value if (!boundingSphere.radius) continue; boundaryHelper.extendStep(boundingSphere.center, boundingSphere.radius); } Vec3.copy(boundingSphere.center, boundaryHelper.center); boundingSphere.radius = boundaryHelper.radius; return boundingSphere; } function renderableSort(a: Renderable, b: Renderable) { const drawProgramIdA = a.getProgram('color').id const drawProgramIdB = b.getProgram('color').id const materialIdA = a.materialId const materialIdB = b.materialId if (drawProgramIdA !== drawProgramIdB) { return drawProgramIdA - drawProgramIdB // sort by program id to minimize gl state changes } else if (materialIdA !== materialIdB) { return materialIdA - materialIdB // sort by material id to minimize gl state changes } else { return a.id - b.id; } } interface Scene extends Object3D { readonly count: number readonly renderables: ReadonlyArray> readonly boundingSphere: Sphere3D readonly isCommiting: boolean update: (objects: ArrayLike | undefined, keepBoundingSphere: boolean) => void add: (o: GraphicsRenderObject) => void // Renderable remove: (o: GraphicsRenderObject) => void syncCommit: () => void commit: () => Task has: (o: GraphicsRenderObject) => boolean clear: () => void forEach: (callbackFn: (value: Renderable, key: GraphicsRenderObject) => void) => void } namespace Scene { export function create(ctx: WebGLContext): Scene { const renderableMap = new Map>() const renderables: Renderable[] = [] const boundingSphere = Sphere3D.zero() let boundingSphereDirty = true const object3d = Object3D.create() const add = (o: GraphicsRenderObject) => { if (!renderableMap.has(o)) { const renderable = createRenderable(ctx, o) renderables.push(renderable) renderableMap.set(o, renderable) boundingSphereDirty = true return renderable; } else { console.warn(`RenderObject with id '${o.id}' already present`) return renderableMap.get(o)! } } const remove = (o: GraphicsRenderObject) => { const renderable = renderableMap.get(o) if (renderable) { renderable.dispose() renderables.splice(renderables.indexOf(renderable), 1) renderableMap.delete(o) boundingSphereDirty = true } } const commitQueue = new AsyncQueue(); const toAdd: GraphicsRenderObject[] = [] const toRemove: GraphicsRenderObject[] = [] type CommitParams = { toAdd: GraphicsRenderObject[], toRemove: GraphicsRenderObject[] } const step = 100 const handle = async (ctx: RuntimeContext, arr: GraphicsRenderObject[], fn: (o: GraphicsRenderObject) => void, message: string) => { for (let i = 0, il = arr.length; i < il; i += step) { if (ctx.shouldUpdate) await ctx.update({ message, current: i, max: il }) for (let j = i, jl = Math.min(i + step, il); j < jl; ++j) { fn(arr[j]) } } } const commit = async (ctx: RuntimeContext, p: CommitParams) => { await handle(ctx, p.toRemove, remove, 'Removing GraphicsRenderObjects') await handle(ctx, p.toAdd, add, 'Adding GraphicsRenderObjects') if (ctx.shouldUpdate) await ctx.update({ message: 'Sorting GraphicsRenderObjects' }) renderables.sort(renderableSort) } return { get view () { return object3d.view }, get position () { return object3d.position }, get direction () { return object3d.direction }, get up () { return object3d.up }, get isCommiting () { return commitQueue.length > 0 }, update(objects, keepBoundingSphere) { Object3D.update(object3d) if (objects) { for (let i = 0, il = objects.length; i < il; ++i) { const o = renderableMap.get(objects[i]); if (!o) continue; o.update(); } } else { for (let i = 0, il = renderables.length; i < il; ++i) { renderables[i].update() } } if (!keepBoundingSphere) boundingSphereDirty = true }, add: (o: GraphicsRenderObject) => { toAdd.push(o) }, remove: (o: GraphicsRenderObject) => { toRemove.push(o) }, syncCommit: () => { for (let i = 0, il = toRemove.length; i < il; ++i) remove(toRemove[i]) toRemove.length = 0 for (let i = 0, il = toAdd.length; i < il; ++i) add(toAdd[i]) toAdd.length = 0 renderables.sort(renderableSort) }, commit: () => { const params = { toAdd: [ ...toAdd ], toRemove: [ ...toRemove ] } toAdd.length = 0 toRemove.length = 0 return Task.create('Commiting GraphicsRenderObjects', async ctx => { const removed = await commitQueue.enqueue(params); if (!removed) return; try { await commit(ctx, params); } finally { commitQueue.handled(params); } }, () => { commitQueue.remove(params); }) }, has: (o: GraphicsRenderObject) => { return renderableMap.has(o) }, clear: () => { for (let i = 0, il = renderables.length; i < il; ++i) { renderables[i].dispose() } renderables.length = 0 renderableMap.clear() boundingSphereDirty = true }, forEach: (callbackFn: (value: Renderable, key: GraphicsRenderObject) => void) => { renderableMap.forEach(callbackFn) }, get count() { return renderables.length }, renderables, get boundingSphere() { if (boundingSphereDirty) calculateBoundingSphere(renderables, boundingSphere) boundingSphereDirty = false return boundingSphere } } } } export default Scene