@itwin/core-frontend
Version:
iTwin.js frontend components
200 lines • 12.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 { Frustum, FrustumPlanes, Npc, RenderMode } from "@itwin/core-common";
import { ClipUtilities, ConvexClipPlaneSet, Map4d, Matrix3d, Matrix4d, Plane3dByOriginAndUnitNormal, Point3d, Range1d, Range2d, Range3d, Ray3d, Transform, } from "@itwin/core-geometry";
import { ApproximateTerrainHeights } from "../../../ApproximateTerrainHeights";
import { RenderState } from "./RenderState";
const scratchRange = Range3d.createNull();
const scratchMap4d = Map4d.createIdentity();
const scratchMatrix4d = Matrix4d.createIdentity();
export class PlanarTextureProjection {
static _postProjectionMatrixNpc = Matrix4d.createRowValues(/* Row 1 */ 0, 1, 0, 0, /* Row 2 */ 0, 0, 1, 0, /* Row 3 */ 1, 0, 0, 0, /* Row 4 */ 0, 0, 0, 1);
static isTileRangeInBounds(tileRange, drapeRange) {
// return false if tile is outside of drapeRange, ignoring height (x) for this
if (tileRange.low.y > drapeRange.high.y || tileRange.high.y < drapeRange.low.y)
return false;
if (tileRange.low.z > drapeRange.high.z || tileRange.high.z < drapeRange.low.z)
return false;
return true;
}
static computePlanarTextureProjection(texturePlane, sceneContext, target, drapeRefs, viewState, textureWidth, textureHeight, maskRange, _heightRange) {
const textureZ = texturePlane.getNormalRef();
const viewingSpace = sceneContext.viewingSpace;
const viewX = viewingSpace.rotation.rowX();
const viewZ = viewingSpace.rotation.rowZ();
const minCrossMagnitude = 1.0E-4;
if (viewZ === undefined)
return {}; // View without depth?....
let textureX = viewZ.crossProduct(textureZ);
let textureY;
if (textureX.magnitude() < minCrossMagnitude) {
textureY = viewX.crossProduct(textureZ);
textureX = textureY.crossProduct(textureZ).normalize();
}
else {
textureX.normalizeInPlace();
textureY = textureZ.crossProduct(textureX).normalize();
}
const frustumX = textureZ, frustumY = textureX, frustumZ = textureY;
const textureMatrix = Matrix3d.createRows(frustumX, frustumY, frustumZ);
const textureTransform = Transform.createRefs(Point3d.createZero(), textureMatrix);
const viewFrustum = viewingSpace.getFrustum().transformBy(textureTransform);
const viewPlanes = FrustumPlanes.fromFrustum(viewFrustum);
const viewClipPlanes = ConvexClipPlaneSet.createPlanes(viewPlanes.planes);
const contentUnBoundedRange = Range1d.createNull();
// calculate drapeRange from drapeRefs (mask references or drape reference).
const drapeRange = Range3d.createNull();
for (const drapeRef of drapeRefs) {
const drapeTree = drapeRef.treeOwner.tileTree;
if (!drapeTree)
return {};
if (drapeTree.isContentUnbounded) {
let heightRange = viewingSpace.getTerrainHeightRange();
if (!heightRange)
heightRange = ApproximateTerrainHeights.instance.globalHeightRange;
contentUnBoundedRange.low = Math.min(contentUnBoundedRange.low, heightRange.low);
contentUnBoundedRange.high = Math.max(contentUnBoundedRange.high, heightRange.high);
}
else if (maskRange.isNull) {
const r = Range3d.createNull();
drapeRef.unionFitRange(r);
const contentRange = textureTransform.multiplyRange(r);
if (!contentRange.isNull)
drapeRange.extendRange(contentRange);
}
else {
const contentRange = textureTransform.multiplyRange(maskRange);
drapeRange.extendRange(contentRange);
}
}
// get range of only the tiles to be masked or draped onto.
let textureRange = Range3d.createNull();
const tileToTexture = textureTransform.multiplyTransformTransform(target.location);
for (const tile of target.tiles) {
tileToTexture.multiplyRange(tile.range, scratchRange);
// Skip tile if it is outside of drapeRange because we don't want the extra heights from distant tiles included.
if (drapeRange.isNull || PlanarTextureProjection.isTileRangeInBounds(scratchRange, drapeRange))
textureRange.extendRange(scratchRange);
}
if (textureRange.isNull)
return {};
textureRange = ClipUtilities.rangeOfClipperIntersectionWithRange(viewClipPlanes, textureRange);
if (!contentUnBoundedRange.isNull) {
// Union of height
textureRange.low.x = Math.min(textureRange.low.x, contentUnBoundedRange.low);
textureRange.high.x = Math.max(textureRange.high.x, contentUnBoundedRange.high);
}
if (!drapeRange.isNull) {
// Union of height
textureRange.low.x = Math.min(textureRange.low.x, drapeRange.low.x);
textureRange.high.x = Math.max(textureRange.high.x, drapeRange.high.x);
// Intersection of texture extents.
textureRange.low.y = Math.max(textureRange.low.y, drapeRange.low.y);
textureRange.high.y = Math.min(textureRange.high.y, drapeRange.high.y);
textureRange.low.z = Math.max(textureRange.low.z, drapeRange.low.z);
textureRange.high.z = Math.min(textureRange.high.z, drapeRange.high.z);
}
const epsilon = .01;
textureRange.low.x -= epsilon;
textureRange.high.x += epsilon;
const textureFrustum = Frustum.fromRange(textureRange);
let debugFrustum;
if (true) // debugFrustum as textureRange.
debugFrustum = textureFrustum.clone();
else // debugFrustum as drapeRange.
debugFrustum = Frustum.fromRange(drapeRange);
textureTransform.multiplyInversePoint3dArray(debugFrustum.points, debugFrustum.points);
const viewZVecZ = viewState.getRotation().rowZ().z;
// This code attempts to use a projection frustum that aligns to the camera frustum in order to get higher mask resolution closer to the eye.
// Limit its use to views that have an eyepoint above the bottom of the frustum and are looking down at a view angle > 5 degrees, otherwise it causes issues.
// viewZVecZ is negative when looking up, positive when looking down.
if (viewState.isCameraOn && viewState.getEyePoint().z > textureRange.low.x && viewZVecZ > 0.09) {
// NB moved the eyePlane from the center to the bottom of the textureRange to solve problems when the eye was below the eyePlane.
const eyePlane = Plane3dByOriginAndUnitNormal.create(Point3d.createScale(textureZ, textureRange.low.x), textureZ); // at bottom of range - parallel to texture.
const projectionRay = Ray3d.create(viewState.getEyePoint(), viewZ.crossProduct(textureX).normalize());
let projectionDistance = projectionRay.intersectionWithPlane(eyePlane);
const minNearToFarRatio = .01; // Smaller value allows texture projection to conform tightly to view frustum.
if (undefined !== projectionDistance) {
projectionDistance = Math.max(.1, projectionDistance);
const eyePoint = textureTransform.multiplyPoint3d(projectionRay.fractionToPoint(projectionDistance));
let near = eyePoint.z - textureRange.high.z;
let far = eyePoint.z - textureRange.low.z;
if (near / far < minNearToFarRatio) {
// If the near-far ratio is less than minimum move the camera back.
far = (textureRange.high.z - textureRange.low.z) / (1.0 - minNearToFarRatio);
near = far * minNearToFarRatio;
eyePoint.z = near + textureRange.high.z;
}
const farRange = Range2d.createNull();
const nearRange = Range2d.createNull();
// Create a frustum that includes the entire view frustum and all Z values.
nearRange.low.x = textureRange.low.x;
nearRange.high.x = textureRange.high.x;
farRange.low.x = eyePoint.x + far / near * (textureRange.low.x - eyePoint.x);
farRange.high.x = eyePoint.x + far / near * (textureRange.high.x - eyePoint.x);
ClipUtilities.announceLoopsOfConvexClipPlaneSetIntersectRange(viewClipPlanes, textureRange, (points) => {
points.getPoint3dArray().forEach((rangePoint) => {
const farScale = far / (eyePoint.z - rangePoint.z);
const nearScale = near / (eyePoint.z - rangePoint.z);
const nearY = eyePoint.y + nearScale * (rangePoint.y - eyePoint.y);
const farY = eyePoint.y + farScale * (rangePoint.y - eyePoint.y);
nearRange.low.y = Math.min(nearRange.low.y, nearY);
nearRange.high.y = Math.max(nearRange.high.y, nearY);
farRange.low.y = Math.min(farRange.low.y, farY);
farRange.high.y = Math.max(farRange.high.y, farY);
});
});
// Set NPC from results.
textureFrustum.points[Npc._000].set(farRange.low.x, farRange.low.y, eyePoint.z - far);
textureFrustum.points[Npc._100].set(farRange.high.x, farRange.low.y, eyePoint.z - far);
textureFrustum.points[Npc._010].set(farRange.low.x, farRange.high.y, eyePoint.z - far);
textureFrustum.points[Npc._110].set(farRange.high.x, farRange.high.y, eyePoint.z - far);
textureFrustum.points[Npc._001].set(nearRange.low.x, nearRange.low.y, eyePoint.z - near);
textureFrustum.points[Npc._101].set(nearRange.high.x, nearRange.low.y, eyePoint.z - near);
textureFrustum.points[Npc._011].set(nearRange.low.x, nearRange.high.y, eyePoint.z - near);
textureFrustum.points[Npc._111].set(nearRange.high.x, nearRange.high.y, eyePoint.z - near);
}
}
textureMatrix.transposeInPlace();
textureMatrix.multiplyVectorArrayInPlace(textureFrustum.points);
const frustumMap = textureFrustum.toMap4d();
if (undefined === frustumMap) {
return {};
}
const worldToNpc = PlanarTextureProjection._postProjectionMatrixNpc.multiplyMatrixMatrix(frustumMap.transform0);
const npcToView = Map4d.createBoxMap(Point3d.create(0, 0, 0), Point3d.create(1, 1, 1), Point3d.create(0, 0, 0), Point3d.create(textureWidth, textureHeight, 1), scratchMap4d);
const npcToWorld = worldToNpc.createInverse(scratchMatrix4d);
if (undefined === npcToWorld) {
return {};
}
const worldToNpcMap = Map4d.createRefs(worldToNpc, npcToWorld);
const worldToViewMap = npcToView.multiplyMapMap(worldToNpcMap);
return { textureFrustum, worldToViewMap, projectionMatrix: worldToNpc, debugFrustum };
}
static getTextureDrawingParams(target) {
const state = new RenderState();
state.flags.depthMask = false;
state.flags.blend = false;
state.flags.depthTest = false;
const viewFlags = target.currentViewFlags.copy({
renderMode: RenderMode.SmoothShade,
wiremesh: false,
transparency: false,
textures: false,
lighting: false,
shadows: false,
monochrome: false,
materials: false,
ambientOcclusion: false,
visibleEdges: false,
hiddenEdges: false,
});
return { state, viewFlags };
}
}
//# sourceMappingURL=PlanarTextureProjection.js.map