UNPKG

@itwin/core-frontend

Version:
268 lines • 12.8 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Rendering */ import { Point2d, Point3d, Range3d, Transform, Vector2d } from "@itwin/core-geometry"; import { ColorDef, ColorIndex, Feature, FeatureIndex, FeatureTable, FillFlags, PackedFeatureTable, QParams3d, QPoint3dList, } from "@itwin/core-common"; import { GraphicBranch } from "./GraphicBranch"; import { DisplayParams } from "../common/internal/render/DisplayParams"; import { createMeshParams } from "../common/internal/render/VertexTableBuilder"; import { IModelApp } from "../IModelApp"; /** * @public * @extensions */ export var ParticleCollectionBuilder; (function (ParticleCollectionBuilder) { /** Creates a new ParticleCollectionBuilder. * @throws Error if size is not greater than zero. */ function create(params) { return new Builder(params); } ParticleCollectionBuilder.create = create; })(ParticleCollectionBuilder || (ParticleCollectionBuilder = {})); class Particle { centroid; transparency; width; height; rotationMatrix; constructor(centroid, width, height, transparency, rotationMatrix) { this.centroid = Point3d.fromJSON(centroid); this.transparency = transparency; this.width = width; this.height = height; this.rotationMatrix = rotationMatrix; } } class Builder { _viewport; _isViewCoords; _pickableId; _texture; _size; _transparency; _hasVaryingTransparency = false; _localToWorldTransform; _range = Range3d.createNull(); _particlesOpaque = []; _particlesTranslucent = []; constructor(params) { this._viewport = params.viewport; this._isViewCoords = true === params.isViewCoords; this._pickableId = params.pickableId; this._texture = params.texture; this._transparency = undefined !== params.transparency ? clampTransparency(params.transparency) : 0; this._localToWorldTransform = params.origin ? Transform.createTranslationXYZ(params.origin.x, params.origin.y, params.origin.z) : Transform.createIdentity(); if ("number" === typeof params.size) this._size = new Vector2d(params.size, params.size); else this._size = Vector2d.fromJSON(params.size); if (this._size.x <= 0 || this._size.y <= 0) throw new Error("Particle size must be greater than zero"); } get size() { return this._size; } get transparency() { return this._transparency; } set transparency(transparency) { transparency = clampTransparency(transparency); if (transparency !== this._transparency) { this._transparency = transparency; this._hasVaryingTransparency = this._particlesTranslucent.length > 0; } } addParticle(props) { const size = props.size ?? this._size; let width, height; if ("number" === typeof size) { width = height = size; } else { width = size.x; height = size.y; } if (width <= 0 || height <= 0) throw new Error("A particle must have a size greater than zero"); const transparency = undefined !== props.transparency ? clampTransparency(props.transparency) : this.transparency; if (transparency !== this.transparency && this._particlesTranslucent.length > 0) this._hasVaryingTransparency = true; const particle = new Particle(props, width, height, transparency, props.rotationMatrix); if (transparency > 0) this._particlesTranslucent.push(particle); else this._particlesOpaque.push(particle); this._range.extendPoint(particle.centroid); } finish() { if (0 === this._particlesTranslucent.length + this._particlesOpaque.length) return undefined; // Order-independent transparency doesn't work well with opaque geometry - it will look semi-transparent. // If we have a mix of opaque and transparent particles, put them in separate graphics to be rendered in separate passes. const opaque = this.createGraphic(this._particlesOpaque, 0); const transparent = this.createGraphic(this._particlesTranslucent, this._hasVaryingTransparency ? undefined : this._transparency); // Empty the collection before any return statements. const range = this._range.clone(); this._range.setNull(); this._particlesOpaque.length = 0; this._particlesTranslucent.length = 0; this._hasVaryingTransparency = false; if (!transparent && !opaque) return undefined; // Transform from origin to collection, then to world. const toCollection = Transform.createTranslation(range.center); const toWorld = toCollection.multiplyTransformTransform(this._localToWorldTransform); const branch = new GraphicBranch(true); if (opaque) branch.add(opaque); if (transparent) branch.add(transparent); let graphic = this._viewport.target.renderSystem.createGraphicBranch(branch, toWorld); // If we have a pickable Id, produce a batch. // NB: We pass this._pickableId as the FeatureTable's modelId so that it will be treated like a reality model or a map - // specifically, it can be located and display a tooltip, but can't be selected. const featureTable = this._pickableId ? new FeatureTable(1, this._pickableId) : undefined; if (featureTable) { this._localToWorldTransform.multiplyRange(range, range); featureTable.insert(new Feature(this._pickableId)); graphic = this._viewport.target.renderSystem.createBatch(graphic, PackedFeatureTable.pack(featureTable), range); } return graphic; } createGraphic(particles, uniformTransparency) { const numParticles = particles.length; if (numParticles <= 0) return undefined; // To keep scale values close to 1, compute mean size to use as size of quad. const meanSize = new Vector2d(); let maxSize = 0; for (const particle of particles) { meanSize.x += particle.width; meanSize.y += particle.height; if (particle.width > maxSize) maxSize = particle.width; if (particle.height > maxSize) maxSize = particle.height; } meanSize.x /= numParticles; meanSize.y /= numParticles; // Define InstancedGraphicParams for particles. const rangeCenter = this._range.center; const floatsPerTransform = 12; const transforms = new Float32Array(floatsPerTransform * numParticles); const bytesPerOverride = 8; const symbologyOverrides = undefined === uniformTransparency ? new Uint8Array(bytesPerOverride * numParticles) : undefined; const viewToWorld = this._viewport.view.getRotation().transpose(); let tfIndex = 0; let ovrIndex = 0; for (const particle of particles) { const scaleX = particle.width / meanSize.x; const scaleY = particle.height / meanSize.y; if (this._isViewCoords) { // Particles already face the camera in view coords - just apply the scale. transforms[tfIndex + 0] = scaleX; transforms[tfIndex + 5] = scaleY; transforms[tfIndex + 10] = 1; } else if (undefined !== particle.rotationMatrix) { // Scale rotation matrix relative to size of quad. transforms[tfIndex + 0] = particle.rotationMatrix.coffs[0] * scaleX; transforms[tfIndex + 1] = particle.rotationMatrix.coffs[1] * scaleY; transforms[tfIndex + 2] = particle.rotationMatrix.coffs[2]; transforms[tfIndex + 4] = particle.rotationMatrix.coffs[3] * scaleX; transforms[tfIndex + 5] = particle.rotationMatrix.coffs[4] * scaleY; transforms[tfIndex + 6] = particle.rotationMatrix.coffs[5]; transforms[tfIndex + 8] = particle.rotationMatrix.coffs[6] * scaleX; transforms[tfIndex + 9] = particle.rotationMatrix.coffs[7] * scaleY; transforms[tfIndex + 10] = particle.rotationMatrix.coffs[8]; } else { // Rotate about origin by inverse view matrix so quads always face the camera and scale relative to size of quad. transforms[tfIndex + 0] = viewToWorld.coffs[0] * scaleX; transforms[tfIndex + 1] = viewToWorld.coffs[1] * scaleY; transforms[tfIndex + 2] = viewToWorld.coffs[2]; transforms[tfIndex + 4] = viewToWorld.coffs[3] * scaleX; transforms[tfIndex + 5] = viewToWorld.coffs[4] * scaleY; transforms[tfIndex + 6] = viewToWorld.coffs[5]; transforms[tfIndex + 8] = viewToWorld.coffs[6] * scaleX; transforms[tfIndex + 9] = viewToWorld.coffs[7] * scaleY; transforms[tfIndex + 10] = viewToWorld.coffs[8]; } // Translate relative to center of particles range. transforms[tfIndex + 3] = particle.centroid.x - rangeCenter.x; transforms[tfIndex + 7] = particle.centroid.y - rangeCenter.y; transforms[tfIndex + 11] = particle.centroid.z - rangeCenter.z; tfIndex += floatsPerTransform; if (symbologyOverrides) { // See FeatureOverrides.buildLookupTable() for layout. symbologyOverrides[ovrIndex + 0] = 1 << 2; // OvrFlags.Alpha symbologyOverrides[ovrIndex + 7] = 0xff - particle.transparency; ovrIndex += bytesPerOverride; } } // Produce instanced quads. // Note: We do not need to allocate an array of featureIds. If we have a pickableId, all particles refer to the same Feature, with index 0. // So we leave the vertex attribute disabled causing the shader to receive the default (0, 0, 0) which happens to correspond to our feature index. const quad = createQuad(meanSize, this._texture, uniformTransparency ?? 0x7f, this._viewport); const transformCenter = new Point3d(0, 0, 0); const range = computeRange(this._range, rangeCenter, maxSize); const instances = { count: numParticles, transforms, transformCenter, symbologyOverrides, range }; return this._viewport.target.renderSystem.createMesh(quad, instances); } } function createQuad(size, texture, transparency, viewport) { const halfWidth = size.x / 2; const halfHeight = size.y / 2; const corners = [ new Point3d(-halfWidth, -halfHeight, 0), new Point3d(halfWidth, -halfHeight, 0), new Point3d(-halfWidth, halfHeight, 0), new Point3d(halfWidth, halfHeight, 0), ]; const range = new Range3d(); range.low = corners[0]; range.high = corners[3]; const points = new QPoint3dList(QParams3d.fromRange(range)); for (const corner of corners) points.add(corner); const colors = new ColorIndex(); colors.initUniform(ColorDef.white.withTransparency(transparency)); const quadArgs = { points, vertIndices: [0, 1, 2, 2, 1, 3], fillFlags: FillFlags.None, isPlanar: true, colors, features: new FeatureIndex(), textureMapping: { texture, uvParams: [new Point2d(0, 1), new Point2d(1, 1), new Point2d(0, 0), new Point2d(1, 0)], }, }; return createMeshParams(quadArgs, viewport.target.renderSystem.maxTextureSize, "non-indexed" !== IModelApp.tileAdmin.edgeOptions.type); } function clampTransparency(transparency) { transparency = Math.min(255, transparency, Math.max(0, transparency)); transparency = Math.floor(transparency); if (transparency < DisplayParams.minTransparency) transparency = 0; return transparency; } function computeRange(centroidRange, center, maxSize) { const range2 = centroidRange.clone(); range2.low.subtractInPlace(center); range2.high.subtractInPlace(center); const halfSize = maxSize * 0.5; range2.low.x -= halfSize; range2.low.y -= halfSize; range2.low.z -= halfSize; range2.high.x += halfSize; range2.high.y += halfSize; range2.high.z += halfSize; return range2; } //# sourceMappingURL=ParticleCollectionBuilder.js.map