@itwin/core-frontend
Version:
iTwin.js frontend components
271 lines • 13.4 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ParticleCollectionBuilder = void 0;
const core_geometry_1 = require("@itwin/core-geometry");
const core_common_1 = require("@itwin/core-common");
const GraphicBranch_1 = require("./GraphicBranch");
const DisplayParams_1 = require("../common/internal/render/DisplayParams");
const VertexTableBuilder_1 = require("../common/internal/render/VertexTableBuilder");
const IModelApp_1 = require("../IModelApp");
/**
* @public
* @extensions
*/
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 || (exports.ParticleCollectionBuilder = ParticleCollectionBuilder = {}));
class Particle {
centroid;
transparency;
width;
height;
rotationMatrix;
constructor(centroid, width, height, transparency, rotationMatrix) {
this.centroid = core_geometry_1.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 = core_geometry_1.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 ? core_geometry_1.Transform.createTranslationXYZ(params.origin.x, params.origin.y, params.origin.z) : core_geometry_1.Transform.createIdentity();
if ("number" === typeof params.size)
this._size = new core_geometry_1.Vector2d(params.size, params.size);
else
this._size = core_geometry_1.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 = core_geometry_1.Transform.createTranslation(range.center);
const toWorld = toCollection.multiplyTransformTransform(this._localToWorldTransform);
const branch = new GraphicBranch_1.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 core_common_1.FeatureTable(1, this._pickableId) : undefined;
if (featureTable) {
this._localToWorldTransform.multiplyRange(range, range);
featureTable.insert(new core_common_1.Feature(this._pickableId));
graphic = this._viewport.target.renderSystem.createBatch(graphic, core_common_1.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 core_geometry_1.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 core_geometry_1.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 core_geometry_1.Point3d(-halfWidth, -halfHeight, 0), new core_geometry_1.Point3d(halfWidth, -halfHeight, 0),
new core_geometry_1.Point3d(-halfWidth, halfHeight, 0), new core_geometry_1.Point3d(halfWidth, halfHeight, 0),
];
const range = new core_geometry_1.Range3d();
range.low = corners[0];
range.high = corners[3];
const points = new core_common_1.QPoint3dList(core_common_1.QParams3d.fromRange(range));
for (const corner of corners)
points.add(corner);
const colors = new core_common_1.ColorIndex();
colors.initUniform(core_common_1.ColorDef.white.withTransparency(transparency));
const quadArgs = {
points,
vertIndices: [0, 1, 2, 2, 1, 3],
fillFlags: core_common_1.FillFlags.None,
isPlanar: true,
colors,
features: new core_common_1.FeatureIndex(),
textureMapping: {
texture,
uvParams: [new core_geometry_1.Point2d(0, 1), new core_geometry_1.Point2d(1, 1), new core_geometry_1.Point2d(0, 0), new core_geometry_1.Point2d(1, 0)],
},
};
return (0, VertexTableBuilder_1.createMeshParams)(quadArgs, viewport.target.renderSystem.maxTextureSize, "non-indexed" !== IModelApp_1.IModelApp.tileAdmin.edgeOptions.type);
}
function clampTransparency(transparency) {
transparency = Math.min(255, transparency, Math.max(0, transparency));
transparency = Math.floor(transparency);
if (transparency < DisplayParams_1.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