quadTree/Node.tstypescript
import { Extent } from "../Extent";
import { binaryInsert, getMatrixSubArray32, getMatrixSubArray64, getMatrixSubArrayBoundsExt } from "../utils/shared";
import { getMedian } from "../terrain/terrainUtils";
import { LonLat } from "../LonLat";
import { MAX, MIN } from "../math";
import { Segment } from "../segment/Segment";
import { QuadTreeStrategy } from "./QuadTreeStrategy";
import { Vec2 } from "../math/Vec2";
import { Vec3 } from "../math/Vec3";
import {
E,
MAX_RENDERED_NODES,
N,
NE,
NEIGHBOUR,
NOPSORD,
NOTRENDERING,
NW,
OPPART,
OPSIDE,
PARTOFFSET,
RENDERING,
ROOTPART,
S,
SE,
SIDEFLIP,
SW,
W,
WALKTHROUGH
} from "./quadTree";
import { PlanetCamera } from "../camera/PlanetCamera";
import type { Camera } from "../camera/Camera";
let _tempHigh = new Vec3(),
_tempLow = new Vec3();
const _vertOrder: Vec2[] = [new Vec2(0, 0), new Vec2(1, 0), new Vec2(0, 1), new Vec2(1, 1)];
const _neGridSize = Math.sqrt(_vertOrder.length) - 1;
type BoundsType = {
xmin: number;
ymin: number;
zmin: number;
xmax: number;
ymax: number;
zmax: number;
// Squared geocentric distance range of the same vertices, see getMatrixSubArrayBoundsExt.
r2min: number;
r2max: number;
};
let BOUNDS: BoundsType = {
xmin: 0.0,
ymin: 0.0,
zmin: 0.0,
xmax: 0.0,
ymax: 0.0,
zmax: 0.0,
r2min: 0.0,
r2max: 0.0
};
// Squared vertex radii of the cut segment, their median is the LOD radius
let _r2 = new Float64Array(_vertOrder.length);
function getR2Buffer(count: number): Float64Array {
if (_r2.length < count) {
_r2 = new Float64Array(count);
}
return _r2;
}
// Node parts on the way up from the node, see Node._linkSideNeighbors
const _pathId: number[] = [];
let __staticCounter = 0;
/**
* Quad tree planet segment node.
* @constructor
* @param {Segment} segmentPrototype - Planet segment node constructor.
* @param {QuadTreeStrategy} quadTreeStrategy - Quad tree strategy handler.
* @param {number} partId - NorthEast, SouthWest etc.
* @param {Node} parent - Parent of this node.
* @param {number} id - Tree node identifier (id * 4 + 1);
* @param {number} tileZoom - Deep index of the quad tree.
* @param {Extent} extent - Segment extent.
*/
class Node {
public __id: number;
public SegmentPrototype: typeof Segment;
public quadTreeStrategy: QuadTreeStrategy;
public parentNode: Node | null;
public partId: number;
public nodeId: number;
public state: number | null;
public prevState: number | null;
public appliedTerrainNodeId: number;
public sideSizeLog2: [number, number, number, number];
public ready: boolean;
public neighbors: [Node[], Node[], Node[], Node[]];
public equalizedSideWithNodeId: number[];
public nodes: [Node, Node, Node, Node] | [];
public segment: Segment;
public _cameraInside: boolean;
public inFrustum: number;
public _fadingNodes: Node[];
/**
* View camera collection the node was last gone through by, see QuadTreeStrategy._viewWalkFrame.
*/
public _viewWalkFrame: number;
/**
* Neighbor search the node was last taken into as an opaque node, see QuadTreeStrategy._neighborFrame.
*/
public _neighborFrame: number;
/**
* Root nodes behind the N, E, S and W sides of a root node, null on a side with no tree behind it.
*/
public rootNeighbors: [Node | null, Node | null, Node | null, Node | null] | null;
constructor(
SegmentPrototype: typeof Segment,
quadTreeStrategy: QuadTreeStrategy,
partId: number,
parent: Node | null,
tileZoom: number,
extent: Extent
) {
quadTreeStrategy.planet._createdNodesCount++;
this.__id = __staticCounter++;
this.SegmentPrototype = SegmentPrototype;
this.quadTreeStrategy = quadTreeStrategy;
this.parentNode = parent;
this.partId = partId;
this.nodeId = partId + (parent ? parent.nodeId * 4 + 1 : 0);
this.state = null;
this.prevState = null;
this.appliedTerrainNodeId = -1;
this.sideSizeLog2 = [0, 0, 0, 0];
this.ready = false;
this.neighbors = [[], [], [], []];
this.equalizedSideWithNodeId = [this.nodeId, this.nodeId, this.nodeId, this.nodeId];
this.nodes = [];
this.segment = new SegmentPrototype(this, quadTreeStrategy, tileZoom, extent);
this._cameraInside = false;
this.inFrustum = 0;
this._fadingNodes = [];
this._viewWalkFrame = -1;
this._neighborFrame = -1;
this.rootNeighbors = null;
this.createBounds();
}
public createChildNodes() {
this.ready = true;
const qts = this.quadTreeStrategy;
const ps = this.segment;
const ext = ps._extent;
const z = ps.tileZoom + 1;
const size_x = ext.getWidth() * 0.5;
const size_y = ext.getHeight() * 0.5;
const ne = ext.northEast;
const sw = ext.southWest;
const c = new LonLat(sw.lon + size_x, sw.lat + size_y);
const nd = this.nodes;
nd[NW] = new Node(
this.SegmentPrototype,
qts,
NW,
this,
z,
new Extent(new LonLat(sw.lon, sw.lat + size_y), new LonLat(sw.lon + size_x, ne.lat))
);
nd[NE] = new Node(this.SegmentPrototype, qts, NE, this, z, new Extent(c, new LonLat(ne.lon, ne.lat)));
nd[SW] = new Node(this.SegmentPrototype, qts, SW, this, z, new Extent(new LonLat(sw.lon, sw.lat), c));
nd[SE] = new Node(
this.SegmentPrototype,
qts,
SE,
this,
z,
new Extent(new LonLat(sw.lon + size_x, sw.lat), new LonLat(ne.lon, sw.lat + size_y))
);
}
public createBounds() {
let seg = this.segment;
seg._setExtentLonLat();
if (seg.tileZoom === 0) {
seg.setBoundingSphere(0.0, 0.0, 0.0, new Vec3(0.0, 0.0, seg.planet.ellipsoid.equatorialSize));
} else if (seg.tileZoom < seg.planet.terrain!.minZoom) {
seg.createBoundsByExtent();
} else {
seg.createBoundsByParent();
}
let x = seg.bsphere.center.x,
y = seg.bsphere.center.y,
z = seg.bsphere.center.z;
let length = 1.0 / Math.sqrt(x * x + y * y + z * z);
seg.centerNormal.x = x * length;
seg.centerNormal.y = y * length;
seg.centerNormal.z = z * length;
// Initial relative center is the same as bounding sphere center
seg._relativeCenter.set(x, y, z);
seg._createPlainBounds();
}
public getState(): number | null {
if (this.state === -1) {
return this.state;
}
let pn = this.parentNode;
while (pn) {
if (pn.state !== WALKTHROUGH) {
return NOTRENDERING;
}
pn = pn.parentNode;
}
return this.state;
}
/**
* Returns the same deep existent neighbour node.
* @public
* @param {number} side - Neighbour side index e.g. og.quadTree.N, og.quadTree.W etc.
* @returns {Node} -
*/
public getEqualNeighbor(side: number): Node | undefined {
let pn: Node = this;
let part = NEIGHBOUR[side][pn.partId];
if (part !== -1) {
// (!) it means that we would never ask to get head node neighbors
return pn.parentNode!.nodes[part];
} else {
let pathId = [];
while (pn.parentNode) {
pathId.push(pn.partId);
part = NEIGHBOUR[side][pn.partId];
pn = pn.parentNode;
if (part !== -1) {
let i = pathId.length;
side = OPSIDE[side];
while (pn && i--) {
part = OPPART[side][pathId[i]];
pn = pn.nodes[part];
}
return pn;
}
}
}
}
public traverseNodes(
cam: PlanetCamera,
maxZoom?: number | null,
terrainReadySegment?: Segment | null,
stopLoading?: boolean,
zoomPassNode?: Node
) {
if (!this.ready) {
this.createChildNodes();
}
let n = this.nodes;
n[0]!.renderTree(cam, maxZoom, terrainReadySegment, stopLoading, zoomPassNode);
n[1]!.renderTree(cam, maxZoom, terrainReadySegment, stopLoading, zoomPassNode);
n[2]!.renderTree(cam, maxZoom, terrainReadySegment, stopLoading, zoomPassNode);
n[3]!.renderTree(cam, maxZoom, terrainReadySegment, stopLoading, zoomPassNode);
}
public renderTree(
cam: PlanetCamera,
maxZoom?: number | null,
terrainReadySegment?: Segment | null,
stopLoading?: boolean,
zoomPassNode?: Node
) {
if (this.quadTreeStrategy._renderedNodes.length >= MAX_RENDERED_NODES) {
return;
}
if (!maxZoom || (zoomPassNode && this.segment.tileZoom > zoomPassNode.segment.tileZoom)) {
this.prevState = this.state;
}
this.state = WALKTHROUGH;
this.clearNeighbors();
let seg = this.segment;
this._cameraInside = false;
// Search a node which the camera is flying over.
if (!this.parentNode || this.parentNode._cameraInside) {
if (seg._extent.isInside(seg.getInsideLonLat(cam))) {
cam._insideSegment = seg;
this._cameraInside = true;
}
}
this.inFrustum = 0;
let frustums = cam.frustums,
numFrustums = frustums.length;
if (seg.tileZoom < 6) {
for (let i = 0; i < numFrustums; i++) {
if (frustums[i].containsSphere(seg.bsphere)) {
this.inFrustum |= 1 << i;
}
}
} else {
for (let i = 0; i < numFrustums; i++) {
if (seg.terrainReady) {
if (frustums[i].containsBox(seg.bbox)) {
this.inFrustum |= 1 << i;
}
} else {
if (frustums[i].containsSphere(seg.bsphere)) {
this.inFrustum |= 1 << i;
}
}
}
}
if (this.inFrustum || this._cameraInside || seg.tileZoom < 3) {
let h = Math.abs(cam._lonLat.height);
let horizonDist = this.quadTreeStrategy._horizonDist2;
let altVis =
seg.tileZoom < 2 ||
seg.tileZoom > 19 ||
/* Could be replaced with camera frustum always looking down check,
and not to go through nodes from the opposite of the globe*/
(seg.tileZoom < 6 && !seg.terrainReady);
//
// It brakes depth camera
//
// if (cam.isOrthographic) {
// let f = cam.getForward();
// altVis =
// altVis ||
// f.dot(seg._sw.getNormal()) < -0 ||
// f.dot(seg._nw.getNormal()) < -0 ||
// f.dot(seg._ne.getNormal()) < -0 ||
// f.dot(seg._se.getNormal()) < -0;
// } else {
altVis =
altVis ||
cam.eye.distance2(seg._sw) < horizonDist ||
cam.eye.distance2(seg._nw) < horizonDist ||
cam.eye.distance2(seg._ne) < horizonDist ||
cam.eye.distance2(seg._se) < horizonDist;
//}
if ((this.inFrustum && (altVis || h > 10000.0)) || this._cameraInside) {
this.quadTreeStrategy.collectVisibleNode(this);
}
if (seg.tileZoom < 2) {
this.traverseNodes(cam, maxZoom, terrainReadySegment, stopLoading, zoomPassNode);
} else if (
seg.terrainReady &&
((!maxZoom && cam.projectedSize(seg.lodCenter, seg._plainRadius) < this.quadTreeStrategy.lodSize) ||
(maxZoom && (seg.tileZoom === maxZoom || !altVis)))
) {
if (altVis) {
seg.passReady = true;
this.renderNode(this.inFrustum, !this.inFrustum, terrainReadySegment, stopLoading);
} else {
this.state = NOTRENDERING;
}
} else if (
seg.terrainReady &&
seg.checkZoom() &&
(!maxZoom || cam.projectedSize(seg.lodCenter, seg._plainRadius) > this.quadTreeStrategy._maxLodSize)
) {
if (seg.tileZoom < this.quadTreeStrategy.maxZoomLimit) {
this.traverseNodes(cam, maxZoom, seg, stopLoading, zoomPassNode);
} else {
this.renderNode(this.inFrustum, !this.inFrustum, terrainReadySegment, stopLoading);
}
} else if (altVis) {
seg.passReady = maxZoom ? seg.terrainReady : false;
this.renderNode(this.inFrustum, !this.inFrustum, terrainReadySegment, stopLoading);
} else {
this.state = NOTRENDERING;
}
} else {
this.state = NOTRENDERING;
}
}
/**
* Bit mask of the camera frustums the node is in, the same test renderTree makes.
* @public
* @param {Camera} cam - Camera.
* @returns {number} -
*/
public getFrustumMask(cam: Camera): number {
const seg = this.segment,
frustums = cam.frustums,
useBox = seg.tileZoom >= 6 && seg.terrainReady;
let mask = 0;
for (let i = 0, len = frustums.length; i < len; i++) {
if (useBox ? frustums[i].containsBox(seg.bbox) : frustums[i].containsSphere(seg.bsphere)) {
mask |= 1 << i;
}
}
return mask;
}
/**
* Tree traversal of a view (depth) camera, after the main camera one. While mainState is true the node state
* was set by the main traversal in this frame: it stops at the main rendered nodes (the strategy takes them)
* and goes down where the main camera went down. Elsewhere the nodes are chosen by the main camera LOD and the
* node state is not read anymore. It never changes the node state.
* @public
*/
public renderViewCameraTree(
slot: number,
cam: Camera,
frustumMask: number,
mainCamera: PlanetCamera,
horizonDist2: number,
mainState: boolean,
terrainReadySegment: Segment | null
) {
const qts = this.quadTreeStrategy;
const openFrustums = qts._getViewCameraOpenFrustums(slot);
if (!openFrustums) {
return;
}
const inFrustum = this.getFrustumMask(cam) & frustumMask & openFrustums;
if (!inFrustum) {
return;
}
const seg = this.segment;
let goDown: boolean;
if (mainState && this.state === RENDERING) {
return;
} else if (mainState && this.state === WALKTHROUGH) {
goDown = true;
} else {
mainState = false;
const altVis =
seg.tileZoom < 2 ||
seg.tileZoom > 19 ||
(seg.tileZoom < 6 && !seg.terrainReady) ||
cam.eye.distance2(seg._sw) < horizonDist2 ||
cam.eye.distance2(seg._nw) < horizonDist2 ||
cam.eye.distance2(seg._ne) < horizonDist2 ||
cam.eye.distance2(seg._se) < horizonDist2;
if (!altVis) {
return;
}
goDown =
seg.tileZoom < 2 ||
(seg.terrainReady &&
seg.tileZoom < qts.maxZoomLimit &&
mainCamera.projectedSize(seg.lodCenter, seg._plainRadius) >= qts.lodSize &&
seg.checkZoom());
}
if (goDown) {
this._viewWalkFrame = qts._viewWalkFrame;
if (!this.ready) {
this.createChildNodes();
}
const n = this.nodes,
trs = seg.terrainReady ? seg : terrainReadySegment;
for (let i = 0; i < n.length; i++) {
n[i].renderViewCameraTree(slot, cam, frustumMask, mainCamera, horizonDist2, mainState, trs);
}
} else {
// Terrain loading part of renderNode. Forced, because the terrain loader drops the requests of the nodes
// the main traversal has marked NOTRENDERING, and that is where the view camera own nodes are.
if (!seg.terrainReady) {
qts._viewCamerasTerrainCompleted = false;
if (!seg.initialized) {
seg.initialize();
}
this.whileTerrainLoading(terrainReadySegment);
if (!seg.plainProcessing) {
seg.createPlainSegmentAsync();
}
if (seg.plainReady) {
seg.loadTerrain(true);
}
}
qts._addViewCameraNode(slot, this, inFrustum);
}
}
public renderNode(
inFrustum: number,
onlyTerrain?: boolean,
terrainReadySegment?: Segment | null,
stopLoading?: boolean
) {
let seg = this.segment;
// Create and load terrain data
if (!seg.terrainReady) {
if (!seg.initialized) {
seg.initialize();
}
this.whileTerrainLoading(terrainReadySegment);
if (!seg.plainProcessing) {
seg.createPlainSegmentAsync();
}
if (seg.plainReady && !stopLoading) {
seg.loadTerrain();
}
}
// Create normal map texture
if (!seg.normalMapReady) {
this.whileNormalMapCreating();
}
if (onlyTerrain) {
this.state = -1;
return;
}
// Calculate minimal and maximal zoom index on the screen
if (!this._cameraInside && seg.tileZoom > this.quadTreeStrategy.maxCurrZoom) {
this.quadTreeStrategy.maxCurrZoom = seg.tileZoom;
}
if (seg.tileZoom < this.quadTreeStrategy.minCurrZoom) {
this.quadTreeStrategy.minCurrZoom = seg.tileZoom;
}
seg._addViewExtent();
// Finally this node proceeds to rendering.
this.addToRender(inFrustum);
}
public childrenPrevStateEquals(state: number): boolean {
let n = this.nodes;
return (
n.length === 4 &&
n[0].prevState === state &&
n[1].prevState === state &&
n[2].prevState === state &&
n[3].prevState === state
);
}
/**
* Returns true if the node is rendered, or fades in or out after a level of detail change.
* @public
* @returns {boolean} -
*/
public isRenderedOrFading(): boolean {
return this.getState() === RENDERING || this.isInFadingTransition();
}
/**
* Returns true if the node is drawn this frame as a fading node of a level of detail change:
* a parent fading out after zooming in, or a child fading out after zooming out.
* Unlike isFading(), it also covers the fading children, whose state is NOTRENDERING.
* @public
* @returns {boolean} -
*/
public isInFadingTransition(): boolean {
return this.quadTreeStrategy._fadingNodes.has(this.__id);
}
public isFading(): boolean {
let n = this.nodes;
return (
this.state === WALKTHROUGH &&
this.segment._transitionOpacity > 0.0 &&
n.length === 4 &&
n[0].state === RENDERING &&
n[1].state === RENDERING &&
n[2].state === RENDERING &&
n[3].state === RENDERING
);
}
public _collectFadingNodes() {
if (this.segment.tileZoom < 3) {
this.segment._transitionOpacity = 1.0;
return;
}
// Light up the node
if (this.prevState !== RENDERING) {
// means that the node is lighting up
this.segment._transitionOpacity = 0.0;
// store fading nodes, could be a parent or children nodes
this._fadingNodes = [];
let timestamp = window.performance.now();
this.segment._transitionTimestamp = timestamp;
if (this.parentNode) {
// Parent was visible the last frame, make the parent fading
if (this.parentNode.prevState === RENDERING) {
let pn: Node | null = this.parentNode.parentNode;
while (pn) {
if (pn.isFading()) {
for (let i = 0; i < pn.nodes.length; i++) {
pn.nodes[i].segment._transitionOpacity = 1.0;
pn.nodes[i]._fadingNodes = [];
}
pn.segment._transitionOpacity = 0.0;
break;
}
pn = pn.parentNode;
}
// not sure that it's necessary here
this.parentNode.whileTerrainLoading();
this._fadingNodes.push(this.parentNode);
this.parentNode.segment._transitionOpacity = 2.0;
this.parentNode.segment._transitionTimestamp = timestamp;
} else {
// Check if the children were visible last frame, and make them fading
if (this.segment.childrenInitialized() && this.childrenPrevStateEquals(RENDERING)) {
for (let i = 0; i < this.nodes.length; i++) {
let ni = this.nodes[i];
// not sure that it's necessary here
ni.whileTerrainLoading();
this._fadingNodes.push(ni);
ni.segment._transitionOpacity = 2.0;
ni.segment._transitionTimestamp = timestamp;
ni.prevState = ni.state;
ni.state = NOTRENDERING;
}
}
}
}
}
}
public clearNeighbors() {
//this.sideSizeLog2[0] = this.sideSizeLog2[1] = this.sideSizeLog2[2] = this.sideSizeLog2[3] = Math.log2(this.segment.gridSize);
if (this.neighbors) {
// @ts-ignore
this.neighbors[0] = this.neighbors[1] = this.neighbors[2] = this.neighbors[3] = null;
this.neighbors[0] = [];
this.neighbors[1] = [];
this.neighbors[2] = [];
this.neighbors[3] = [];
}
}
public _refreshTransitionOpacity() {
if (this._fadingNodes.length === 0) {
this.segment._transitionOpacity = 1.0;
} else {
if (this._fadingNodes.length === 4 && !this.childrenPrevStateEquals(RENDERING)) {
this.segment._transitionOpacity = 1.0;
this._fadingNodes = [];
} else {
// Looks like a bug fix for suddenly empty spaces
for (let i = 0; i < this._fadingNodes.length; i++) {
if (
this.segment._transitionOpacity < 1.0 &&
this._fadingNodes[i].segment._transitionOpacity === 0
) {
this._fadingNodes[i].segment._transitionOpacity = 0;
this.segment._transitionOpacity = 1.0;
}
}
this.segment.increaseTransitionOpacity();
}
}
}
/**
* Picking up current node to render processing.
* @public
*/
public addToRender(inFrustum: number) {
this.state = RENDERING;
let seg = this.segment,
qts = this.quadTreeStrategy;
if (seg.maxTerrainRadius > 0) {
if (seg.minTerrainRadius < qts._minTerrainRadiusAcc) {
qts._minTerrainRadiusAcc = seg.minTerrainRadius;
}
if (seg.maxTerrainRadius > qts._maxTerrainRadiusAcc) {
qts._maxTerrainRadiusAcc = seg.maxTerrainRadius;
}
}
let nodes = this.quadTreeStrategy._renderedNodes;
//@ts-ignore
if (!this.quadTreeStrategy._transitionOpacityEnabled) {
nodes.push(this);
} else {
//@todo: check if it's possible to get rid of the sorting when using breadth traverse tree
binaryInsert(nodes, this, (a: Node, b: Node) => {
return a.segment.tileZoom - b.segment.tileZoom;
});
}
if (!this.segment.terrainReady) {
this.quadTreeStrategy._renderCompleted = false;
this.quadTreeStrategy._terrainCompleted = false;
}
let k = 0,
rf = this.quadTreeStrategy._renderedNodesInFrustum;
while (inFrustum) {
if (inFrustum & 1) {
rf[k].push(this);
}
k++;
inFrustum >>= 1;
}
}
public applyNeighbor(node: Node, side: number) {
const opcs = this.segment.opside[side];
if (this.neighbors[side].length === 0 || node.neighbors[opcs].length === 0) {
const ap = this.segment;
const bp = node.segment;
const ld = ap.gridSize / (bp.gridSize * Math.pow(2, bp.tileZoom - ap.tileZoom));
let cs_size = ap.gridSize,
opcs_size = bp.gridSize;
if (ld > 1) {
cs_size = Math.ceil(ap.gridSize / ld);
opcs_size = bp.gridSize;
} else if (ld < 1) {
cs_size = ap.gridSize;
opcs_size = Math.ceil(bp.gridSize * ld);
}
this.sideSizeLog2[side] = Math.log2(cs_size);
node.sideSizeLog2[opcs] = Math.log2(opcs_size);
}
//@todo: fix dupe neighbors
this.neighbors[side].push(node);
node.neighbors[opcs].push(this);
}
/**
* Links the node with its neighbors of the same or lower zoom among the nodes marked with the frame. The deeper
* ones find this node themselves, as both sides of a pair are set by applyNeighbor.
* @public
* @param {number} frame - Neighbor search counter of the strategy.
*/
public getRenderedNodesNeighbors(frame: number) {
for (let side = N; side <= W; side++) {
this._linkSideNeighbors(side, frame);
}
}
/**
* Climbs to the node that holds both this node and the tile next to it, or to the neighbor tree when the side
* lies on the edge of the tree, and goes down to the tile by the mirrored path.
*/
protected _linkSideNeighbors(side: number, frame: number) {
let pn: Node = this,
part = -1,
depth = 0;
while (pn.parentNode && part === -1) {
_pathId[depth++] = pn.partId;
part = NEIGHBOUR[side][pn.partId];
pn = pn.parentNode;
}
let mirror = OPPART[OPSIDE[side]];
if (part === -1) {
const rootNeighbor = pn.rootNeighbors && pn.rootNeighbors[side];
if (!rootNeighbor) {
return;
}
pn = rootNeighbor;
mirror = ROOTPART[side][this.segment.opside[side]];
}
const zoom = this.segment.tileZoom;
// Fading can leave a parent and its child opaque in one frame, so the walk doesn't stop at the first one
for (let i = depth - 1; i >= 0 && pn.nodes.length; i--) {
pn = pn.nodes[mirror[_pathId[i]]];
if (pn._neighborFrame === frame && (pn.segment.tileZoom < zoom || pn.__id < this.__id)) {
this.applyNeighbor(pn, side);
}
}
}
public whileNormalMapCreating() {
const seg = this.segment;
if (!seg.terrainIsLoading && seg.terrainExists && !seg._inTheQueue) {
seg.planet._normalMapCreator.queue(seg);
}
let pn: Node = this;
while (pn.parentNode && !pn.segment.normalMapReady) {
pn = pn.parentNode;
const ps = pn.segment;
if (!ps.normalMapReady && !ps.terrainIsLoading && ps.terrainExists && !ps._inTheQueue) {
ps.planet._normalMapCreator.queue(ps);
}
}
const dZ2 = 2 << (seg.tileZoom - pn.segment.tileZoom - 1);
seg._normalMapSource = pn.segment;
seg.normalMapTexture = pn.segment.normalMapTexture;
seg.normalMapTextureBias[0] = seg.tileX - pn.segment.tileX * dZ2;
seg.normalMapTextureBias[1] = seg.tileY - pn.segment.tileY * dZ2;
seg.normalMapTextureBias[2] = 1.0 / dZ2;
}
public whileTerrainLoading(terrainReadySegment?: Segment | null) {
const seg = this.segment;
let pn: Node = this;
if (terrainReadySegment && terrainReadySegment.terrainReady) {
pn = terrainReadySegment.node;
} else {
while (pn.parentNode && !pn.segment.terrainReady) {
pn = pn.parentNode;
}
}
if (!pn.segment.terrainReady || this.appliedTerrainNodeId === pn.nodeId) {
return;
}
let dZ2 = 2 << (seg.tileZoom - pn.segment.tileZoom - 1), // 2 * Math.pow(2, dZ-1)
offsetX = seg.tileX - pn.segment.tileX * dZ2,
offsetY = seg.tileY - pn.segment.tileY * dZ2;
const pseg = pn.segment;
let renderVertices: Float64Array, renderVerticesHigh: Float32Array, renderVerticesLow: Float32Array;
this.appliedTerrainNodeId = pn.nodeId;
this.equalizedSideWithNodeId[N] =
this.equalizedSideWithNodeId[E] =
this.equalizedSideWithNodeId[S] =
this.equalizedSideWithNodeId[W] =
this.appliedTerrainNodeId;
let gridSize = pn.segment.gridSize / dZ2,
gridSizeExt = pn.segment.fileGridSize / dZ2;
BOUNDS.xmin = BOUNDS.ymin = BOUNDS.zmin = MAX;
BOUNDS.xmax = BOUNDS.ymax = BOUNDS.zmax = MIN;
BOUNDS.r2min = Infinity;
BOUNDS.r2max = -Infinity;
if (gridSize >= 1) {
seg.gridSize = gridSize;
let len = (gridSize + 1) * (gridSize + 1) * 3;
renderVertices = new Float64Array(len);
renderVerticesHigh = new Float32Array(len);
renderVerticesLow = new Float32Array(len);
getMatrixSubArrayBoundsExt(
pseg.terrainVertices!,
pseg.gridSize,
gridSize * offsetY,
gridSize * offsetX,
gridSize,
pseg._relativeCenter,
seg._relativeCenter,
renderVertices,
renderVerticesHigh,
renderVerticesLow,
BOUNDS,
getR2Buffer(len / 3)
);
} else if (gridSizeExt >= 1 && pn.segment.terrainExists) {
seg.gridSize = gridSizeExt;
let len = (gridSizeExt + 1) * (gridSizeExt + 1) * 3;
renderVertices = new Float64Array(len);
renderVerticesHigh = new Float32Array(len);
renderVerticesLow = new Float32Array(len);
getMatrixSubArrayBoundsExt(
pseg.normalMapVertices!,
pn.segment.fileGridSize,
gridSizeExt * offsetY,
gridSizeExt * offsetX,
gridSizeExt,
pseg._relativeCenter,
seg._relativeCenter,
renderVertices,
renderVerticesHigh,
renderVerticesLow,
BOUNDS,
getR2Buffer(len / 3)
);
} else {
seg.gridSize = _neGridSize;
let i0 = Math.floor(gridSize * offsetY),
j0 = Math.floor(gridSize * offsetX);
let bigOne;
if (pseg.gridSize === 1) {
bigOne = pseg.terrainVertices!;
} else {
bigOne = getMatrixSubArray64(pseg.terrainVertices!, pseg.gridSize, i0, j0, 1);
}
let insideSize = 1.0 / gridSize;
let t_i0 = offsetY - insideSize * i0,
t_j0 = offsetX - insideSize * j0;
let v_lt = new Vec3(bigOne[0], bigOne[1], bigOne[2]),
v_rb = new Vec3(bigOne[9], bigOne[10], bigOne[11]);
let vn = new Vec3(bigOne[3] - bigOne[0], bigOne[4] - bigOne[1], bigOne[5] - bigOne[2]),
vw = new Vec3(bigOne[6] - bigOne[0], bigOne[7] - bigOne[1], bigOne[8] - bigOne[2]),
ve = new Vec3(bigOne[3] - bigOne[9], bigOne[4] - bigOne[10], bigOne[5] - bigOne[11]),
vs = new Vec3(bigOne[6] - bigOne[9], bigOne[7] - bigOne[10], bigOne[8] - bigOne[11]);
let coords = new Vec3();
renderVertices = new Float64Array(3 * _vertOrder.length);
renderVerticesHigh = new Float32Array(3 * _vertOrder.length);
renderVerticesLow = new Float32Array(3 * _vertOrder.length);
for (let i = 0; i < _vertOrder.length; i++) {
let vi_y = _vertOrder[i].y + t_i0,
vi_x = _vertOrder[i].x + t_j0;
let vi_x_is = vi_x * gridSize,
vi_y_is = vi_y * gridSize;
if (vi_y + vi_x < insideSize) {
coords = vn.scaleTo(vi_x_is).addA(vw.scaleTo(vi_y_is)).addA(v_lt);
} else {
coords = vs
.scaleTo(1 - vi_x_is)
.addA(ve.scaleTo(1 - vi_y_is))
.addA(v_rb);
}
coords.addA(pseg._relativeCenter);
let dstCoords = coords.sub(seg._relativeCenter);
Vec3.doubleToTwoFloats(dstCoords, _tempHigh, _tempLow);
let i3 = i * 3;
renderVertices[i3] = dstCoords.x;
renderVertices[i3 + 1] = dstCoords.y;
renderVertices[i3 + 2] = dstCoords.z;
renderVerticesHigh[i3] = _tempHigh.x;
renderVerticesHigh[i3 + 1] = _tempHigh.y;
renderVerticesHigh[i3 + 2] = _tempHigh.z;
renderVerticesLow[i3] = _tempLow.x;
renderVerticesLow[i3 + 1] = _tempLow.y;
renderVerticesLow[i3 + 2] = _tempLow.z;
if (coords.x < BOUNDS.xmin) BOUNDS.xmin = coords.x;
if (coords.x > BOUNDS.xmax) BOUNDS.xmax = coords.x;
if (coords.y < BOUNDS.ymin) BOUNDS.ymin = coords.y;
if (coords.y > BOUNDS.ymax) BOUNDS.ymax = coords.y;
if (coords.z < BOUNDS.zmin) BOUNDS.zmin = coords.z;
if (coords.z > BOUNDS.zmax) BOUNDS.zmax = coords.z;
let r2 = coords.x * coords.x + coords.y * coords.y + coords.z * coords.z;
if (r2 < BOUNDS.r2min) BOUNDS.r2min = r2;
if (r2 > BOUNDS.r2max) BOUNDS.r2max = r2;
_r2[i] = r2;
}
}
seg.readyToEngage = true;
seg.terrainVertices = renderVertices;
seg.terrainVerticesHigh = renderVerticesHigh;
seg.terrainVerticesLow = renderVerticesLow;
seg.renderVertices = renderVertices;
seg.renderVerticesHigh = renderVerticesHigh;
seg.renderVerticesLow = renderVerticesLow;
seg.setTerrainBoundingVolume([BOUNDS.xmin, BOUNDS.ymin, BOUNDS.zmin, BOUNDS.xmax, BOUNDS.ymax, BOUNDS.zmax]);
seg.setTerrainRadiusRange(
BOUNDS.r2max >= BOUNDS.r2min ? [Math.sqrt(BOUNDS.r2min), Math.sqrt(BOUNDS.r2max)] : null
);
seg.setLodRadius(Math.sqrt(getMedian(_r2, renderVertices.length / 3)));
if (seg.tileZoom > seg.planet.terrain!.maxZoom) {
if (pn.segment.tileZoom >= seg.planet.terrain!.maxZoom) {
seg.terrainReady = true;
seg.terrainIsLoading = false;
seg.terrainVertices = renderVertices;
seg.terrainVerticesHigh = renderVerticesHigh;
seg.terrainVerticesLow = renderVerticesLow;
seg.passReady = true;
this.appliedTerrainNodeId = this.nodeId;
this.equalizedSideWithNodeId[N] =
this.equalizedSideWithNodeId[E] =
this.equalizedSideWithNodeId[S] =
this.equalizedSideWithNodeId[W] =
this.appliedTerrainNodeId;
if (pn.segment.terrainExists) {
seg.normalMapVertices = renderVertices;
seg.fileGridSize = Math.sqrt(renderVertices.length / 3) - 1;
let fgs = Math.sqrt(pseg.normalMapNormals!.length / 3) - 1,
fgsZ = fgs / dZ2;
if (fgs > 1) {
seg.normalMapNormals = getMatrixSubArray32(
pseg.normalMapNormals!,
fgs,
fgsZ * offsetY,
fgsZ * offsetX,
fgsZ
);
} else {
// TODO: interpolation
seg.normalMapNormals = pseg.normalMapNormals;
}
}
}
}
}
public destroy() {
this.prevState = this.state = NOTRENDERING;
this.segment.destroySegment();
let n = this.neighbors;
for (let i = 0, len = n.length; i < len; i++) {
let ni = n[i];
if (ni) {
for (let j = 0; j < ni.length; j++) {
let nij = ni[j];
if (nij && nij.neighbors) {
nij.clearNeighbors();
}
}
}
}
// @ts-ignore
this.neighbors = null;
// @ts-ignore
this.parentNode = null;
// @ts-ignore
this.sideSizeLog2 = null;
// @ts-ignore
this.segment = null;
}
/**
* Destroys the branches the main camera does not render.
* @public
* @param {boolean} [keepViewCameraBranches=false] - Keeps the branches the last view camera collection has
* gone through, they lead to the nodes the depth cameras render.
*/
public clearTree(keepViewCameraBranches: boolean = false) {
const state = this.getState();
const viewBranch = keepViewCameraBranches && this._viewWalkFrame === this.quadTreeStrategy._viewWalkFrame;
if (!viewBranch && (state === NOTRENDERING || state === RENDERING)) {
this.destroyBranches();
} else {
for (let i = 0; i < this.nodes.length; i++) {
this.nodes[i] && this.nodes[i].clearTree(keepViewCameraBranches);
}
}
}
public clearBranches() {
for (let i = 0; i < this.nodes.length; i++) {
this.nodes[i]!.clearBranches();
this.nodes[i]!.segment.deleteMaterials();
}
}
public destroyBranches() {
if (this.ready) {
let nodesToRemove: Node[] = [],
i;
for (i = 0; i < this.nodes.length; i++) {
nodesToRemove[i] = this.nodes[i]!;
}
this.ready = false;
this.nodes = [];
for (i = 0; i < nodesToRemove.length; i++) {
nodesToRemove[i].destroyBranches();
nodesToRemove[i].destroy();
//@ts-ignore
nodesToRemove[i] = null;
}
nodesToRemove.length = 0;
// @ts-ignore
nodesToRemove = null;
}
}
public traverseTree(callback: Function) {
callback(this);
if (this.ready) {
for (let i = 0; i < this.nodes.length; i++) {
this.nodes[i]!.traverseTree(callback);
}
}
}
/**
* Returns the neighbor behind the side that has a corner in the same point as the corner of this node.
* @public
* @param {number} side - The side that the corner belongs to.
* @param {number} corner - Corner index, NW, NE, SW or SE.
* @returns {Node | null | undefined} - Neighbor, null when there is none, undefined when the point lies inside
* the side of a neighbor of a lower zoom.
*/
public getCornerNeighbor(side: number, corner: number): Node | null | undefined {
const list = this.neighbors[side],
seg = this.segment,
far = NOPSORD[side][corner],
// The tile index along the side: x along the N and S sides, y along the E and W ones
pos = (side & 1 ? seg.tileY : seg.tileX) + far,
opcs = seg.opside[side],
flip = SIDEFLIP[side][opcs];
for (let i = 0; i < list.length; i++) {
const n = list[i],
ns = n.segment,
nIndex = opcs & 1 ? ns.tileY : ns.tileX,
nPos = (flip ? ns.powTileZoom - 1 - nIndex : nIndex) + far,
dz = ns.tileZoom - seg.tileZoom;
if (dz >= 0) {
if (nPos === pos * (1 << dz)) {
return n;
}
} else {
return pos === nPos * (1 << -dz) ? n : undefined;
}
}
return null;
}
/**
* Returns the corner of the neighbors behind the side that lies in the same point as the corner of this node.
* @public
* @param {number} side - The side that the corner belongs to.
* @param {number} corner - Corner index, NW, NE, SW or SE.
* @returns {number} -
*/
public getNeighborCorner(side: number, corner: number): number {
return ROOTPART[side][this.segment.opside[side]][corner];
}
public getOffsetOppositeNeighbourSide(neighbourNode: Node, side: number): number {
let pNode: Node = this,
neighbourZoom = neighbourNode.segment.tileZoom,
offset = 0;
while (pNode.segment.tileZoom > neighbourZoom) {
offset += PARTOFFSET[pNode.partId][side] / (1 << (pNode.segment.tileZoom - neighbourZoom));
pNode = pNode.parentNode!;
}
return offset;
}
}
export { Node };