src/viewer/scene/webgl/OcclusionTester.js
import {math} from '../math/math.js';
import {Program} from "./Program.js";
import {ArrayBuf} from "./ArrayBuf.js";
import {RenderBuffer} from "./RenderBuffer.js";
const TEST_MODE = false;
const MARKER_COLOR = math.vec3([1.0, 0.0, 0.0]);
const POINT_SIZE = 20;
/**
* Manages occlusion testing. Private member of a Renderer.
*/
class OcclusionTester {
constructor(scene) {
this._scene = scene;
this._markers = {}; // ID map of Markers
this._markerList = []; // Ordered array of Markers
this._markerIndices = {}; // ID map of Marker indices in _markerList
this._numMarkers = 0; // Length of _markerList
this._positions = []; // Packed array of World-space marker positions
this._indices = []; // Indices corresponding to array above
this._positionsBuf = null; // Positions VBO to render marker positions
this._indicesBuf = null; // Indices VBO
this._occlusionTestList = []; // List of
this._lenOcclusionTestList = 0;
this._pixels = [];
this._shaderSource = null;
this._program = null;
this._shaderSourceHash = null;
this._shaderSourceDirty = true; // Need to build shader source code ?
this._programDirty = false; // Need to build shader program ?
this._markerListDirty = false; // Need to (re)build _markerList ?
this._positionsDirty = false; // Need to (re)build _positions and _indices ?
this._vbosDirty = false; // Need to rebuild _positionsBuf and _indicesBuf ?
this._occlusionTestListDirty = false; // Need to build _occlusionTestList ?
this._lenPositionsBuf = 0;
scene.camera.on("viewMatrix", () => {
this._occlusionTestListDirty = true;
});
scene.camera.on("projMatrix", () => {
this._occlusionTestListDirty = true;
});
scene.canvas.on("boundary", () => {
this._occlusionTestListDirty = true;
});
}
/**
* Adds a Marker for occlusion testing.
* @param marker
*/
addMarker(marker) {
this._markers[marker.id] = marker;
this._markerListDirty = true;
}
/**
* Notifies OcclusionTester that a Marker has updated its World-space position.
* @param marker
*/
markerWorldPosUpdated(marker) {
if (!this._markers[marker.id]) { // Not added
return;
}
const i = this._markerIndices[marker.id];
this._positions[i * 3 + 0] = marker.worldPos[0];
this._positions[i * 3 + 1] = marker.worldPos[1];
this._positions[i * 3 + 2] = marker.worldPos[2];
this._positionsDirty = true; // TODO: avoid reallocating VBO each time
}
/**
* Removes a Marker from occlusion testing.
* @param marker
*/
removeMarker(marker) {
delete this._markers[marker.id];
this._markerListDirty = true;
}
/**
* Prepares for an occlusion test.
* Binds render buffer.
*/
bindRenderBuf() {
const shaderSourceHash = [this._scene.canvas.canvas.id, this._scene._sectionPlanesState.getHash()].join(";");
if (shaderSourceHash !== this._shaderSourceHash) {
this._shaderSourceHash = shaderSourceHash;
this._shaderSourceDirty = true;
}
if (this._shaderSourceDirty) { // TODO: Set this when hash changes
this._buildShaderSource();
this._shaderSourceDirty = false;
this._programDirty = true;
}
if (this._programDirty) {
this._buildProgram();
this._programDirty = false;
this._occlusionTestListDirty = true;
}
if (this._markerListDirty) {
this._buildMarkerList();
this._markerListDirty = false;
this._positionsDirty = true;
this._occlusionTestListDirty = true;
}
if (this._positionsDirty) { ////////////// TODO: Don't rebuild this when positions change, very wasteful
this._buildPositions();
this._positionsDirty = false;
this._vbosDirty = true;
}
if (this._vbosDirty) {
this._buildVBOs();
this._vbosDirty = false;
}
if (this._occlusionTestListDirty) {
this._buildOcclusionTestList();
}
if (!TEST_MODE) {
this._readPixelBuf = this._readPixelBuf || (this._readPixelBuf = new RenderBuffer(this._scene.canvas.canvas, this._scene.canvas.gl));
this._readPixelBuf.bind();
this._readPixelBuf.clear();
}
}
_buildShaderSource() {
this._shaderSource = {
vertex: this._buildVertexShaderSource(),
fragment: this._buildFragmentShaderSource()
};
}
_buildVertexShaderSource() {
const scene = this._scene;
const clipping = scene._sectionPlanesState.sectionPlanes.length > 0;
const src = [];
src.push("// Mesh occlusion vertex shader");
src.push("attribute vec3 position;");
src.push("uniform mat4 modelMatrix;");
src.push("uniform mat4 viewMatrix;");
src.push("uniform mat4 projMatrix;");
if (clipping) {
src.push("varying vec4 vWorldPosition;");
}
src.push("void main(void) {");
src.push("vec4 worldPosition = vec4(position, 1.0); ");
src.push(" vec4 viewPosition = viewMatrix * worldPosition;");
if (clipping) {
src.push(" vWorldPosition = worldPosition;");
}
src.push(" gl_Position = projMatrix * viewPosition;");
src.push(" gl_PointSize = " + POINT_SIZE + ".0;");
src.push("}");
return src;
}
_buildFragmentShaderSource() {
const scene = this._scene;
const sectionPlanesState = scene._sectionPlanesState;
const clipping = sectionPlanesState.sectionPlanes.length > 0;
const src = [];
src.push("// Mesh occlusion fragment shader");
src.push("precision lowp float;");
if (clipping) {
src.push("varying vec4 vWorldPosition;");
for (var i = 0; i < sectionPlanesState.sectionPlanes.length; i++) {
src.push("uniform bool sectionPlaneActive" + i + ";");
src.push("uniform vec3 sectionPlanePos" + i + ";");
src.push("uniform vec3 sectionPlaneDir" + i + ";");
}
}
src.push("void main(void) {");
if (clipping) {
src.push(" float dist = 0.0;");
for (var i = 0; i < sectionPlanesState.sectionPlanes.length; i++) {
src.push("if (sectionPlaneActive" + i + ") {");
src.push(" dist += clamp(dot(-sectionPlaneDir" + i + ".xyz, vWorldPosition.xyz - sectionPlanePos" + i + ".xyz), 0.0, 1000.0);");
src.push("}");
}
src.push(" if (dist > 0.0) { discard; }");
}
src.push(" gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); ");
src.push("}");
return src;
}
_buildProgram() {
if (this._program) {
this._program.destroy();
}
const scene = this._scene;
const gl = scene.canvas.gl;
const sectionPlanesState = scene._sectionPlanesState;
this._program = new Program(gl, this._shaderSource);
if (this._program.errors) {
this.errors = this._program.errors;
return;
}
const program = this._program;
this._uViewMatrix = program.getLocation("viewMatrix");
this._uProjMatrix = program.getLocation("projMatrix");
this._uSectionPlanes = [];
const sectionPlanes = sectionPlanesState.sectionPlanes;
for (var i = 0, len = sectionPlanes.length; i < len; i++) {
this._uSectionPlanes.push({
active: program.getLocation("sectionPlaneActive" + i),
pos: program.getLocation("sectionPlanePos" + i),
dir: program.getLocation("sectionPlaneDir" + i)
});
}
this._aPosition = program.getAttribute("position");
}
_buildMarkerList() {
this._numMarkers = 0;
for (var id in this._markers) {
if (this._markers.hasOwnProperty(id)) {
this._markerList[this._numMarkers] = this._markers[id];
this._markerIndices[id] = this._numMarkers;
this._numMarkers++;
}
}
this._markerList.length = this._numMarkers;
}
_buildPositions() {
var j = 0;
for (var i = 0; i < this._numMarkers; i++) {
if (this._markerList[i]) {
const marker = this._markerList[i];
const worldPos = marker.worldPos;
this._positions[j++] = worldPos[0];
this._positions[j++] = worldPos[1];
this._positions[j++] = worldPos[2];
this._indices[i] = i;
}
}
this._positions.length = this._numMarkers * 3;
this._indices.length = this._numMarkers;
}
_buildVBOs() {
if (this._positionsBuf) {
if (this._lenPositionsBuf === this._positions.length) { // Just updating buffer elements, don't need to reallocate
this._positionsBuf.setData(this._positions); // Indices don't need updating
return;
}
this._positionsBuf.destroy();
this._positionsBuf = null;
this._indicesBuf.destroy();
this._indicesBuf = null;
}
const gl = this._scene.canvas.gl;
const lenPositions = this._numMarkers * 3;
const lenIndices = this._numMarkers;
this._positionsBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, new Float32Array(this._positions), lenPositions, 3, gl.STATIC_DRAW);
this._indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(this._indices), lenIndices, 1, gl.STATIC_DRAW);
this._lenPositionsBuf = this._positions.length;
}
_buildOcclusionTestList() {
const canvas = this._scene.canvas;
const near = this._scene.camera.perspective.near; // Assume near enough to ortho near
let marker;
let canvasPos;
let viewPos;
let canvasX;
let canvasY;
let lenPixels = 0;
let i;
const boundary = canvas.boundary;
const canvasWidth = boundary[2];
const canvasHeight = boundary[3];
this._lenOcclusionTestList = 0;
for (i = 0; i < this._numMarkers; i++) {
marker = this._markerList[i];
viewPos = marker.viewPos;
if (viewPos[2] > -near) { // Clipped by near plane
marker._setVisible(false);
continue;
}
canvasPos = marker.canvasPos;
canvasX = canvasPos[0];
canvasY = canvasPos[1];
if ((canvasX + 10) < 0 || (canvasY + 10) < 0 || (canvasX - 10) > canvasWidth || (canvasY - 10) > canvasHeight) {
marker._setVisible(false);
continue;
}
if (marker.entity && !marker.entity.visible) {
marker._setVisible(false);
continue;
}
if (marker.occludable) {
this._occlusionTestList[this._lenOcclusionTestList++] = marker;
this._pixels[lenPixels++] = canvasX;
this._pixels[lenPixels++] = canvasY;
continue;
}
marker._setVisible(true);
}
}
/**
* Draws {@link Marker}s to the render buffer.
* @param frameCtx
*/
drawMarkers(frameCtx) {
const scene = this._scene;
const gl = scene.canvas.gl;
const program = this._program;
const sectionPlanesState = scene._sectionPlanesState;
const camera = scene.camera;
const cameraState = camera._state;
program.bind();
if (sectionPlanesState.sectionPlanes.length > 0) {
const sectionPlanes = scene._sectionPlanesState.sectionPlanes;
let sectionPlaneUniforms;
let uSectionPlaneActive;
let sectionPlane;
let uSectionPlanePos;
let uSectionPlaneDir;
for (var i = 0, len = this._uSectionPlanes.length; i < len; i++) {
sectionPlaneUniforms = this._uSectionPlanes[i];
uSectionPlaneActive = sectionPlaneUniforms.active;
sectionPlane = sectionPlanes[i];
if (uSectionPlaneActive) {
gl.uniform1i(uSectionPlaneActive, sectionPlane.active);
}
uSectionPlanePos = sectionPlaneUniforms.pos;
if (uSectionPlanePos) {
gl.uniform3fv(sectionPlaneUniforms.pos, sectionPlane.pos);
}
uSectionPlaneDir = sectionPlaneUniforms.dir;
if (uSectionPlaneDir) {
gl.uniform3fv(sectionPlaneUniforms.dir, sectionPlane.dir);
}
}
}
gl.uniformMatrix4fv(this._uViewMatrix, false, cameraState.matrix);
gl.uniformMatrix4fv(this._uProjMatrix, false, camera._project._state.matrix);
this._aPosition.bindArrayBuffer(this._positionsBuf);
this._indicesBuf.bind();
gl.drawElements(gl.POINTS, this._indicesBuf.numItems, this._indicesBuf.itemType, 0);
}
/**
* Reads render buffer and updates visibility states of {@link Marker}s if they can be found in the buffer.
*/
doOcclusionTest() {
if (!TEST_MODE) {
const markerR = MARKER_COLOR[0] * 255;
const markerG = MARKER_COLOR[1] * 255;
const markerB = MARKER_COLOR[2] * 255;
for (var i = 0; i < this._lenOcclusionTestList; i++) {
const marker = this._occlusionTestList[i];
const j = i * 2;
const k = i * 4;
const color = this._readPixelBuf.read(this._pixels[j], this._pixels[j + 1]);
const visible = (color[0] === markerR) && (color[1] === markerG) && (color[2] === markerB);
marker._setVisible(visible);
}
}
}
/**
* Unbinds render buffer.
*/
unbindRenderBuf() {
if (!TEST_MODE) {
this._readPixelBuf.unbind();
}
}
/**
* Destroys this OcclusionTester.
*/
destroy() {
this._markers = {};
this._markerList.length = 0;
if (this._positionsBuf) {
this._positionsBuf.destroy();
}
if (this._indicesBuf) {
this._indicesBuf.destroy();
}
if (this._program) {
this._program.destroy();
}
}
}
export {OcclusionTester};