src/viewer/scene/webgl/Renderer.js
import {FrameContext} from './FrameContext.js';
import {math} from '../math/math.js';
import {stats} from '../stats.js';
import {WEBGL_INFO} from '../webglInfo.js';
import {Map} from "../utils/Map.js";
import {PickResult} from "./PickResult.js";
import {OcclusionTester} from "./occlusion/OcclusionTester.js";
import {createRTCViewMat} from "../math/rtcCoords.js";
import {RenderBuffer} from "./RenderBuffer.js";
import {getExtension} from "./getExtension.js";
import {createProgramVariablesState} from "./WebGLRenderer.js";
import {ArrayBuf} from "./ArrayBuf.js";
const OCCLUSION_TEST_MODE = false;
const vec3_0 = math.vec3([0,0,0]);
const iota = (n) => { const ret = [ ]; for (let i = 0; i < n; ++i) ret.push(i); return ret; };
const tempPlanes = iota(6).map(() => math.vec4());
const tempVec4 = math.vec4();
const bitShiftScreenZ = math.vec4([1.0 / (256.0 * 256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0 * 256.0), 1.0 / (256.0 * 256.0), 1.0 / 256.0]);
const pixelToInt = pix => pix[0] + (pix[1] << 8) + (pix[2] << 16) + (pix[3] << 24);
const toWorldNormal = (n) => math.normalizeVec3(math.divVec3Scalar(n, math.MAX_INT, math.vec3()));
const toWorldPos = (p, origin, scale) => math.vec3([ p[0] * scale[0] + origin[0],
p[1] * scale[1] + origin[1],
p[2] * scale[2] + origin[2] ]);
const makeFrustumAABBIntersectionTest = function(camera) {
const m = math.mat4();
math.mulMat4(camera.projMatrix, camera.viewMatrix, m);
for (let i = 0; i < 3; ++i) {
tempPlanes[i * 2 + 0][0] = m[ 3] + m[i];
tempPlanes[i * 2 + 0][1] = m[ 7] + m[i + 4];
tempPlanes[i * 2 + 0][2] = m[11] + m[i + 8];
tempPlanes[i * 2 + 0][3] = m[15] + m[i + 12];
tempPlanes[i * 2 + 1][0] = m[ 3] - m[i];
tempPlanes[i * 2 + 1][1] = m[ 7] - m[i + 4];
tempPlanes[i * 2 + 1][2] = m[11] - m[i + 8];
tempPlanes[i * 2 + 1][3] = m[15] - m[i + 12];
}
// Normalize each plane
tempPlanes.forEach(p => math.divVec4Scalar(p, math.lenVec3(p), p));
return aabb => tempPlanes.every(p => {
// Compute the positive vertex (farthest in direction of normal)
tempVec4[0] = aabb[(p[0] >= 0) ? 3 : 0];
tempVec4[1] = aabb[(p[1] >= 0) ? 4 : 1];
tempVec4[2] = aabb[(p[2] >= 0) ? 5 : 2];
tempVec4[3] = 1;
return math.dotVec4(p, tempVec4) >= 0;
});
};
const makeRayAABBIntersectionTest = (O, D) => aabb => {
let tmin = -Infinity;
let tmax = Infinity;
for (let i = 0; i < 3; i++) {
const origin = O[i];
const direction = D[i];
const minBound = aabb[i];
const maxBound = aabb[i + 3];
if (Math.abs(direction) < 1e-8) {
// Ray is parallel to plane
if ((origin < minBound) || (origin > maxBound)) {
return false; // No intersection
}
} else {
const t1 = (minBound - origin) / direction;
const t2 = (maxBound - origin) / direction;
tmin = Math.max(tmin, Math.min(t1, t2)); // near
tmax = Math.min(tmax, Math.max(t1, t2)); // far
if (tmin > tmax) {
return false; // No intersection
}
}
}
return tmax >= 0; // Check if intersection is in front of ray
};
/**
* @private
*/
const Renderer = function (scene, options) {
options = options || {};
const frameCtx = new FrameContext();
const canvas = scene.canvas.canvas;
/**
* @type {WebGL2RenderingContext}
*/
const gl = scene.canvas.gl;
const canvasTransparent = (!!options.transparent);
const alphaDepthMask = options.alphaDepthMask;
const pickIDs = new Map({});
let drawableTypeInfo = {};
let drawables = {};
let postSortDrawableList = [];
let postCullDrawableList = [];
let uiDrawableList = [];
let drawableListDirty = true;
let stateSortDirty = true;
let imageDirty = true;
let shadowsDirty = true;
let transparentEnabled = true;
let edgesEnabled = true;
let saoEnabled = true;
let pbrEnabled = true;
let colorTextureEnabled = true;
const renderBufferManager = (function() {
const renderBuffersBasic = {};
const renderBuffersScaled = {};
return {
getRenderBuffer: (id, colorFormats, hasDepthTexture) => {
const renderBuffers = (scene.canvas.resolutionScale === 1.0) ? renderBuffersBasic : renderBuffersScaled;
if (! renderBuffers[id]) {
renderBuffers[id] = new RenderBuffer(gl, colorFormats, hasDepthTexture);
}
return renderBuffers[id];
},
destroy: () => {
Object.values(renderBuffersBasic ).forEach(buf => buf.destroy());
Object.values(renderBuffersScaled).forEach(buf => buf.destroy());
}
};
})();
const SAOProgram = (gl, name, programVariablesState, createOutColorDefinition) => {
const programVariables = programVariablesState.programVariables;
const uViewportInv = programVariables.createUniform("vec2", "uViewportInv");
const uCameraNear = programVariables.createUniform("float", "uCameraNear");
const uCameraFar = programVariables.createUniform("float", "uCameraFar");
const uDepthTexture = programVariables.createUniform("sampler2D", "uDepthTexture");
const uv = programVariables.createAttribute("vec2", "uv");
const vUV = programVariables.createVarying("vec2", "vUV", () => uv);
const outColor = programVariables.createOutput("vec4", "outColor");
const getOutColor = programVariables.createFragmentDefinition(
"getOutColor",
(name, src) => {
const getDepth = "getDepth";
src.push(`
float ${getDepth}(const in vec2 uv) {
return texture(${uDepthTexture}, uv).r;
}
`);
src.push(createOutColorDefinition(name, vUV, uViewportInv, uCameraNear, uCameraFar, getDepth));
});
const [program, errors] = programVariablesState.buildProgram(
gl,
name,
{
clipPos: `vec4(2.0 * ${uv} - 1.0, 0.0, 1.0)`,
appendFragmentOutputs: (src) => src.push(`${outColor} = ${getOutColor}();`)
});
if (errors) {
console.error(errors.join("\n"));
throw errors;
} else {
const uvs = new Float32Array([1,1, 0,1, 0,0, 1,0]);
const uvBuf = new ArrayBuf(gl, gl.ARRAY_BUFFER, uvs, uvs.length, 2, gl.STATIC_DRAW);
// Mitigation: if Uint8Array is used, the geometry is corrupted on OSX when using Chrome with data-textures
const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
const indicesBuf = new ArrayBuf(gl, gl.ELEMENT_ARRAY_BUFFER, indices, indices.length, 1, gl.STATIC_DRAW);
return {
destroy: program.destroy,
bind: (viewportSize, project, depthTexture) => {
program.bind();
uViewportInv.setInputValue([1 / viewportSize[0], 1 / viewportSize[1]]);
uCameraNear.setInputValue(project.near);
uCameraFar.setInputValue(project.far);
uDepthTexture.setInputValue(depthTexture);
uv.setInputValue(uvBuf);
},
draw: () => {
indicesBuf.bind();
gl.drawElements(gl.TRIANGLES, indicesBuf.numItems, indicesBuf.itemType, 0);
}
};
}
};
// SAO implementation inspired from previous SAO work in THREE.js by ludobaka / ludobaka.github.io and bhouston
const saoOcclusionRenderer = (function() {
let currentRenrerer = null;
let curNumSamples = null;
return {
destroy: () => currentRenrerer && currentRenrerer.destroy(),
render: (viewportSize, project, sao, depthTexture) => {
const numSamples = Math.floor(sao.numSamples);
if (curNumSamples !== numSamples) {
currentRenrerer && currentRenrerer.destroy();
const programVariablesState = createProgramVariablesState();
const programVariables = programVariablesState.programVariables;
const uProjectMatrix = programVariables.createUniform("mat4", "uProjectMatrix");
const uInvProjMatrix = programVariables.createUniform("mat4", "uInvProjMatrix");
const uPerspective = programVariables.createUniform("bool", "uPerspective");
const uScale = programVariables.createUniform("float", "uScale");
const uIntensity = programVariables.createUniform("float", "uIntensity");
const uBias = programVariables.createUniform("float", "uBias");
const uKernelRadius = programVariables.createUniform("float", "uKernelRadius");
const uMinResolution = programVariables.createUniform("float", "uMinResolution");
const uRandomSeed = programVariables.createUniform("float", "uRandomSeed");
const program = SAOProgram(
gl,
"SAOOcclusionRenderer",
programVariablesState,
(name, vUV, uViewportInv, uCameraNear, uCameraFar, getDepth) => {
return `
#define EPSILON 1e-6
#define PI 3.14159265359
#define PI2 6.28318530718
#define NUM_SAMPLES ${numSamples}
#define NUM_RINGS 4
const vec3 packFactors = vec3(256. * 256. * 256., 256. * 256., 256.);
vec4 packFloatToRGBA(const in float v) {
vec4 r = vec4(fract(v * packFactors), v);
r.yzw -= r.xyz / 256.;
return r * 256. / 255.;
}
highp float rand(const in vec2 uv) {
const highp float a = 12.9898, b = 78.233, c = 43758.5453;
return fract(sin(mod(dot(uv, vec2(a, b)), PI)) * c);
}
vec3 getViewPos(const in vec2 screenPos, const in float depth) {
float near = ${uCameraNear};
float far = ${uCameraFar};
float viewZ = (${uPerspective}
? ((near * far) / ((far - near) * depth - far))
: (depth * (near - far) - near));
float clipW = ${uProjectMatrix}[2][3] * viewZ + ${uProjectMatrix}[3][3];
return (${uInvProjMatrix} * (clipW * vec4((vec3(screenPos, depth) - 0.5) * 2.0, 1.0))).xyz;
}
vec4 ${name}() {
float centerDepth = ${getDepth}(${vUV});
if (centerDepth >= (1.0 - EPSILON)) {
discard;
}
vec3 centerViewPosition = getViewPos(${vUV}, centerDepth);
float scaleDividedByCameraFar = ${uScale} / ${uCameraFar};
float minResolutionMultipliedByCameraFar = ${uMinResolution} * ${uCameraFar};
vec3 centerViewNormal = normalize(cross(dFdx(centerViewPosition), dFdy(centerViewPosition)));
vec2 radiusStep = ${uKernelRadius} * ${uViewportInv} / float(NUM_SAMPLES);
vec2 radius = radiusStep;
const float angleStep = PI2 * float(NUM_RINGS) / float(NUM_SAMPLES);
float angle = PI2 * rand(${vUV} + ${uRandomSeed});
float occlusionSum = 0.0;
float weightSum = 0.0;
for (int i = 0; i < NUM_SAMPLES; i++) {
vec2 sampleUv = ${vUV} + vec2(cos(angle), sin(angle)) * radius;
radius += radiusStep;
angle += angleStep;
float sampleDepth = ${getDepth}(sampleUv);
if (sampleDepth >= (1.0 - EPSILON)) {
continue;
}
vec3 sampleViewPosition = getViewPos(sampleUv, sampleDepth);
vec3 viewDelta = sampleViewPosition - centerViewPosition;
float scaledScreenDistance = scaleDividedByCameraFar * length(viewDelta);
occlusionSum += max(0.0, (dot(centerViewNormal, viewDelta) - minResolutionMultipliedByCameraFar) / scaledScreenDistance - ${uBias}) / (1.0 + scaledScreenDistance * scaledScreenDistance );
weightSum += 1.0;
}
return packFloatToRGBA(1.0 - occlusionSum * ${uIntensity} / weightSum);
}`;
});
currentRenrerer = {
destroy: program.destroy,
render: (viewportSize, project, sao, depthTexture) => {
program.bind(viewportSize, project, depthTexture);
uProjectMatrix.setInputValue(project.matrix);
uInvProjMatrix.setInputValue(project.inverseMatrix);
uPerspective.setInputValue(project.type === "Perspective");
uScale.setInputValue(sao.scale * project.far / 5);
uIntensity.setInputValue(sao.intensity);
uBias.setInputValue(sao.bias);
uKernelRadius.setInputValue(sao.kernelRadius);
uMinResolution.setInputValue(sao.minResolution);
uRandomSeed.setInputValue(Math.random());
program.draw();
}
};
curNumSamples = numSamples;
}
currentRenrerer.render(viewportSize, project, sao, depthTexture);
}
};
})();
const saoDepthLimitedBlurRenderer = (function() {
const blurStdDev = 4;
const blurDepthCutoff = 0.01;
const KERNEL_RADIUS = 16;
const createSampleOffsets = (uvIncrement) => {
const offsets = [];
for (let i = 0; i <= KERNEL_RADIUS + 1; i++) {
offsets.push(uvIncrement[0] * i);
offsets.push(uvIncrement[1] * i);
}
return new Float32Array(offsets);
};
const sampleOffsetsVer = createSampleOffsets([0, 1]);
const sampleOffsetsHor = createSampleOffsets([1, 0]);
const gaussian = (i, stdDev) => Math.exp(-(i * i) / (2.0 * (stdDev * stdDev))) / (Math.sqrt(2.0 * Math.PI) * stdDev);
const sampleWeights = new Float32Array(iota(KERNEL_RADIUS + 1).map(i => gaussian(i, blurStdDev))); // TODO: Optimize
const programVariablesState = createProgramVariablesState();
const programVariables = programVariablesState.programVariables;
const uDepthCutoff = programVariables.createUniform("float", "uDepthCutoff");
const uSampleOffsets = programVariables.createUniformArray("vec2", "uSampleOffsets", KERNEL_RADIUS + 1);
const uSampleWeights = programVariables.createUniformArray("float", "uSampleWeights", KERNEL_RADIUS + 1);
const uOcclusionTex = programVariables.createUniform("sampler2D", "uOcclusionTex");
const program = SAOProgram(
gl,
"SAODepthLimitedBlurRenderer",
programVariablesState,
(name, vUV, uViewportInv, uCameraNear, uCameraFar, getDepth) => {
return `
#define EPSILON 1e-6
const vec3 packFactors = vec3(256. * 256. * 256., 256. * 256., 256.);
vec4 packFloatToRGBA(const in float v) {
vec4 r = vec4(fract(v * packFactors), v);
r.yzw -= r.xyz / 256.;
return r * 256. / 255.;
}
float getOcclusion(const in vec2 uv) {
vec4 v = texture(${uOcclusionTex}, uv);
return dot(floor(v * 255.0 + 0.5) / 255.0, 255. / 256. / vec4(packFactors, 1.)); // unpackRGBAToFloat
}
float getViewZ(const in float depth) {
return (${uCameraNear} * ${uCameraFar}) / ((${uCameraFar} - ${uCameraNear}) * depth - ${uCameraFar});
}
vec4 ${name}() {
float centerDepth = ${getDepth}(${vUV});
if (centerDepth >= (1.0 - EPSILON)) {
discard;
}
float centerViewZ = getViewZ(centerDepth);
bool rBreak = false;
bool lBreak = false;
float weightSum = ${uSampleWeights}[0];
float occlusionSum = getOcclusion(${vUV}) * weightSum;
for (int i = 1; i <= ${KERNEL_RADIUS}; i++) {
float sampleWeight = ${uSampleWeights}[i];
vec2 sampleUVOffset = ${uSampleOffsets}[i] * ${uViewportInv};
if (! rBreak) {
vec2 rSampleUV = ${vUV} + sampleUVOffset;
if (abs(centerViewZ - getViewZ(${getDepth}(rSampleUV))) > ${uDepthCutoff}) {
rBreak = true;
} else {
occlusionSum += getOcclusion(rSampleUV) * sampleWeight;
weightSum += sampleWeight;
}
}
if (! lBreak) {
vec2 lSampleUV = ${vUV} - sampleUVOffset;
if (abs(centerViewZ - getViewZ(${getDepth}(lSampleUV))) > ${uDepthCutoff}) {
lBreak = true;
} else {
occlusionSum += getOcclusion(lSampleUV) * sampleWeight;
weightSum += sampleWeight;
}
}
}
return packFloatToRGBA(occlusionSum / weightSum);
}`;
});
return {
destroy: program.destroy,
render: (viewportSize, project, direction, depthTexture, occlusionTexture) => {
program.bind(viewportSize, project, depthTexture);
uDepthCutoff.setInputValue(blurDepthCutoff);
uSampleOffsets.setInputValue((direction === 0) ? sampleOffsetsHor : sampleOffsetsVer);
uSampleWeights.setInputValue(sampleWeights);
uOcclusionTex.setInputValue(occlusionTexture);
program.draw();
}
};
})();
const getSceneCameraViewParams = (function() {
let params = null; // scene.camera not defined yet
return function() {
if (! params) {
const camera = scene.camera;
params = {
get eye() { return camera.eye; },
get far() { return camera.project.far; },
get projMatrix() { return camera.projMatrix; },
get viewMatrix() { return camera.viewMatrix; },
get viewNormalMatrix() { return camera.viewNormalMatrix; }
};
}
return params;
};
})();
const getNearPlaneHeight = (camera, drawingBufferHeight) => ((camera.projection === "ortho")
? 1.0
: (drawingBufferHeight / (2 * Math.tan(0.5 * camera.perspective.fov * Math.PI / 180.0))));
this.scene = scene;
this._occlusionTester = null; // Lazy-created in #addMarker()
this.capabilities = {
astcSupported: !!getExtension(gl, 'WEBGL_compressed_texture_astc'),
etc1Supported: true, // WebGL2
etc2Supported: !!getExtension(gl, 'WEBGL_compressed_texture_etc'),
dxtSupported: !!getExtension(gl, 'WEBGL_compressed_texture_s3tc'),
bptcSupported: !!getExtension(gl, 'EXT_texture_compression_bptc'),
pvrtcSupported: !!(getExtension(gl, 'WEBGL_compressed_texture_pvrtc') || getExtension(gl, 'WEBKIT_WEBGL_compressed_texture_pvrtc'))
};
this.setTransparentEnabled = function (enabled) {
transparentEnabled = enabled;
imageDirty = true;
};
this.setEdgesEnabled = function (enabled) {
edgesEnabled = enabled;
imageDirty = true;
};
this.setSAOEnabled = function (enabled) {
saoEnabled = enabled;
imageDirty = true;
};
this.setPBREnabled = function (enabled) {
pbrEnabled = enabled;
imageDirty = true;
};
this.setColorTextureEnabled = function (enabled) {
colorTextureEnabled = enabled;
imageDirty = true;
};
this.needStateSort = function () {
stateSortDirty = true;
};
this.shadowsDirty = function () {
shadowsDirty = true;
};
this.imageDirty = function () {
imageDirty = true;
};
/**
* Inserts a drawable into this renderer.
* @private
*/
this.addDrawable = function (id, drawable) {
const type = drawable.type;
if (!type) {
console.error("Renderer#addDrawable() : drawable with ID " + id + " has no 'type' - ignoring");
return;
}
let drawableInfo = drawableTypeInfo[type];
if (!drawableInfo) {
drawableInfo = {
type: drawable.type,
count: 0,
isStateSortable: drawable.isStateSortable,
stateSortCompare: drawable.stateSortCompare,
drawableMap: {},
drawableListPreCull: [],
drawableList: []
};
drawableTypeInfo[type] = drawableInfo;
}
drawableInfo.count++;
drawableInfo.drawableMap[id] = drawable;
drawables[id] = drawable;
drawableListDirty = true;
};
/**
* Removes a drawable from this renderer.
* @private
*/
this.removeDrawable = function (id) {
const drawable = drawables[id];
if (!drawable) {
console.error("Renderer#removeDrawable() : drawable not found with ID " + id + " - ignoring");
return;
}
const type = drawable.type;
const drawableInfo = drawableTypeInfo[type];
if (--drawableInfo.count <= 0) {
delete drawableTypeInfo[type];
} else {
delete drawableInfo.drawableMap[id];
}
delete drawables[id];
drawableListDirty = true;
};
/**
* Gets a unique pick ID for the given Pickable. A Pickable can be a {@link Mesh} or a {@link PerformanceMesh}.
* @returns {Number} New pick ID.
*/
this.getPickID = function (entity) {
return pickIDs.addItem(entity);
};
/**
* Released a pick ID for reuse.
* @param {Number} pickID Pick ID to release.
*/
this.putPickID = function (pickID) {
pickIDs.removeItem(pickID);
};
/**
* Clears the canvas.
* @private
*/
this.clear = function (params) {
params = params || {};
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
if (canvasTransparent) {
gl.clearColor(1, 1, 1, 1);
} else {
const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? this.lights.getAmbientColorAndIntensity() : scene.canvas.backgroundColor;
gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0);
}
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
};
/**
* Returns true if the next call to render() will draw something
* @returns {Boolean}
*/
this.needsRender = function () {
const needsRender = (imageDirty || drawableListDirty || stateSortDirty);
return needsRender;
}
/**
* Renders inserted drawables.
* @private
*/
this.render = function(pass, clear) {
updateDrawlist();
if (imageDirty) {
draw(pass, clear);
stats.frame.frameCount++;
imageDirty = false;
}
};
function updateDrawlist() { // Prepares state-sorted array of drawables from maps of inserted drawables
if (drawableListDirty) {
Object.values(drawableTypeInfo).forEach(drawableInfo => {
const drawableListPreCull = drawableInfo.drawableListPreCull;
let lenDrawableList = 0;
Object.values(drawableInfo.drawableMap).forEach(drawable => { drawableListPreCull[lenDrawableList++] = drawable; });
drawableListPreCull.length = lenDrawableList;
});
drawableListDirty = false;
stateSortDirty = true;
}
if (stateSortDirty) {
let lenDrawableList = 0;
Object.values(drawableTypeInfo).forEach(drawableInfo => {
drawableInfo.drawableListPreCull.forEach(drawable => { postSortDrawableList[lenDrawableList++] = drawable; });
});
postSortDrawableList.length = lenDrawableList;
postSortDrawableList.sort((a, b) => a.renderOrder - b.renderOrder);
stateSortDirty = false;
imageDirty = true;
}
if (imageDirty) { // Image is usually dirty because the camera moved
let lenDrawableList = 0;
let lenUiList = 0;
postSortDrawableList.forEach(drawable => {
drawable.rebuildRenderFlags();
if (!drawable.renderFlags.culled) {
if (drawable.isUI) {
uiDrawableList[lenUiList++] = drawable;
} else {
postCullDrawableList[lenDrawableList++] = drawable;
}
}
});
postCullDrawableList.length = lenDrawableList;
uiDrawableList.length = lenUiList;
}
}
function draw(pass, clear) {
const sao = scene.sao;
const occlusionTexture = saoEnabled && sao.possible && (sao.numSamples >= 1) && drawSAOBuffers(pass);
scene._lightsState.lights.forEach(light => {
if (light.castsShadow) {
const shadowRenderBuf = light.getShadowRenderBuf();
shadowRenderBuf.bind();
frameCtx.reset();
frameCtx.backfaces = true;
frameCtx.frontface = true;
frameCtx.viewParams.viewMatrix = light.getShadowViewMatrix();
frameCtx.viewParams.projMatrix = light.getShadowProjMatrix();
frameCtx.nearPlaneHeight = getNearPlaneHeight(scene.camera, gl.drawingBufferHeight);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
Object.values(drawableTypeInfo).forEach(
drawableInfo => {
drawableInfo.drawableList.forEach(
drawable => {
if ((drawable.visible !== false) && drawable.castsShadow && drawable.drawShadow) {
if (drawable.renderFlags.colorOpaque) { // Transparent objects don't cast shadows (yet)
drawable.drawShadow(frameCtx);
}
}
});
});
shadowRenderBuf.unbind();
}
});
// const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174
// for (let ii = 0; ii < numVertexAttribs; ii++) {
// gl.disableVertexAttribArray(ii);
// }
//
shadowsDirty = false;
drawColor(pass, clear, occlusionTexture);
}
function drawSAOBuffers(pass) {
const sao = scene.sao;
const size = [gl.drawingBufferWidth, gl.drawingBufferHeight];
// Render depth buffer
const saoDepthRenderBuffer = renderBufferManager.getRenderBuffer("saoDepth", [], true);
saoDepthRenderBuffer.setSize(size);
saoDepthRenderBuffer.bind();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
frameCtx.reset();
frameCtx.pass = pass;
frameCtx.viewParams = getSceneCameraViewParams();
frameCtx.nearPlaneHeight = getNearPlaneHeight(scene.camera, gl.drawingBufferHeight);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.DEPTH_TEST);
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);
gl.depthMask(true);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
postCullDrawableList.forEach(drawable => {
if (!drawable.culled && drawable.visible && drawable.drawDepth && drawable.saoEnabled && drawable.renderFlags.colorOpaque) {
drawable.drawDepth(frameCtx);
}
});
saoDepthRenderBuffer.unbind();
const depthTexture = saoDepthRenderBuffer.depthTexture;
// Render occlusion buffer
const occlusionRenderBuffer1 = renderBufferManager.getRenderBuffer("saoOcclusion");
occlusionRenderBuffer1.setSize(size);
occlusionRenderBuffer1.bind();
gl.viewport(0, 0, size[0], size[1]);
gl.clearColor(0, 0, 0, 1);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
gl.frontFace(gl.CCW);
gl.clear(gl.COLOR_BUFFER_BIT);
saoOcclusionRenderer.render(size, scene.camera.project, sao, depthTexture);
occlusionRenderBuffer1.unbind();
if (sao.blur) {
const occlusionRenderBuffer2 = renderBufferManager.getRenderBuffer("saoOcclusion2");
occlusionRenderBuffer2.setSize(size);
const blurSAO = (src, dst, direction) => {
dst.bind();
gl.viewport(0, 0, size[0], size[1]);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.BLEND);
gl.frontFace(gl.CCW);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const project = scene.camera.project;
saoDepthLimitedBlurRenderer.render(size, project, direction, depthTexture, src.colorTextures[0]);
dst.unbind();
};
blurSAO(occlusionRenderBuffer1, occlusionRenderBuffer2, 0); // horizontally
blurSAO(occlusionRenderBuffer2, occlusionRenderBuffer1, 1); // vertically
}
return occlusionRenderBuffer1.colorTextures[0];
}
function drawColor(pass, clear, occlusionTexture) {
const normalDrawSAOBin = [];
const normalEdgesOpaqueBin = [];
const normalFillTransparentBin = [];
const normalEdgesTransparentBin = [];
const xrayedFillOpaqueBin = [];
const xrayEdgesOpaqueBin = [];
const xrayedFillTransparentBin = [];
const xrayEdgesTransparentBin = [];
const highlightedFillOpaqueBin = [];
const highlightedEdgesOpaqueBin = [];
const highlightedFillTransparentBin = [];
const highlightedEdgesTransparentBin = [];
const selectedFillOpaqueBin = [];
const selectedEdgesOpaqueBin = [];
const selectedFillTransparentBin = [];
const selectedEdgesTransparentBin = [];
const ambientColorAndIntensity = scene._lightsState.getAmbientColorAndIntensity();
frameCtx.reset();
frameCtx.pass = pass;
frameCtx.withSAO = false;
frameCtx.pbrEnabled = pbrEnabled && !!scene.pbrEnabled;
frameCtx.colorTextureEnabled = colorTextureEnabled && !!scene.colorTextureEnabled;
frameCtx.viewParams = getSceneCameraViewParams();
frameCtx.nearPlaneHeight = getNearPlaneHeight(scene.camera, gl.drawingBufferHeight);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
if (canvasTransparent) {
gl.clearColor(0, 0, 0, 0);
} else {
const backgroundColor = scene.canvas.backgroundColorFromAmbientLight ? ambientColorAndIntensity : scene.canvas.backgroundColor;
gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0);
}
gl.enable(gl.DEPTH_TEST);
gl.frontFace(gl.CCW);
gl.enable(gl.CULL_FACE);
gl.depthMask(true);
gl.lineWidth(1);
frameCtx.lineWidth = 1;
const sao = scene.sao;
frameCtx.saoParams = [gl.drawingBufferWidth, gl.drawingBufferHeight, scene.sao.blendCutoff, scene.sao.blendFactor];
frameCtx.occlusionTexture = occlusionTexture;
let i;
let len;
let drawable;
const startTime = Date.now();
if (clear) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
frameCtx.testAABB = makeFrustumAABBIntersectionTest(scene.camera);
const renderDrawables = function(drawables) {
let normalDrawSAOBinLen = 0;
let normalEdgesOpaqueBinLen = 0;
let normalFillTransparentBinLen = 0;
let normalEdgesTransparentBinLen = 0;
let xrayedFillOpaqueBinLen = 0;
let xrayEdgesOpaqueBinLen = 0;
let xrayedFillTransparentBinLen = 0;
let xrayEdgesTransparentBinLen = 0;
let highlightedFillOpaqueBinLen = 0;
let highlightedEdgesOpaqueBinLen = 0;
let highlightedFillTransparentBinLen = 0;
let highlightedEdgesTransparentBinLen = 0;
let selectedFillOpaqueBinLen = 0;
let selectedEdgesOpaqueBinLen = 0;
let selectedFillTransparentBinLen = 0;
let selectedEdgesTransparentBinLen = 0;
//------------------------------------------------------------------------------------------------------
// Render normal opaque solids, defer others to bins to render after
//------------------------------------------------------------------------------------------------------
for (let i = 0, len = drawables.length; i < len; i++) {
drawable = drawables[i];
if (drawable.culled === true || drawable.visible === false) {
continue;
}
const renderFlags = drawable.renderFlags;
if (renderFlags.colorOpaque) {
if (drawable.saoEnabled && occlusionTexture) {
normalDrawSAOBin[normalDrawSAOBinLen++] = drawable;
} else {
drawable.drawColorOpaque(frameCtx);
}
}
if (transparentEnabled) {
if (renderFlags.colorTransparent) {
normalFillTransparentBin[normalFillTransparentBinLen++] = drawable;
}
}
if (renderFlags.xrayedSilhouetteTransparent) {
xrayedFillTransparentBin[xrayedFillTransparentBinLen++] = drawable;
}
if (renderFlags.xrayedSilhouetteOpaque) {
xrayedFillOpaqueBin[xrayedFillOpaqueBinLen++] = drawable;
}
if (renderFlags.highlightedSilhouetteTransparent) {
highlightedFillTransparentBin[highlightedFillTransparentBinLen++] = drawable;
}
if (renderFlags.highlightedSilhouetteOpaque) {
highlightedFillOpaqueBin[highlightedFillOpaqueBinLen++] = drawable;
}
if (renderFlags.selectedSilhouetteTransparent) {
selectedFillTransparentBin[selectedFillTransparentBinLen++] = drawable;
}
if (renderFlags.selectedSilhouetteOpaque) {
selectedFillOpaqueBin[selectedFillOpaqueBinLen++] = drawable;
}
if (drawable.edges && edgesEnabled) {
if (renderFlags.edgesOpaque) {
normalEdgesOpaqueBin[normalEdgesOpaqueBinLen++] = drawable;
}
if (renderFlags.edgesTransparent) {
normalEdgesTransparentBin[normalEdgesTransparentBinLen++] = drawable;
}
if (renderFlags.selectedEdgesTransparent) {
selectedEdgesTransparentBin[selectedEdgesTransparentBinLen++] = drawable;
}
if (renderFlags.selectedEdgesOpaque) {
selectedEdgesOpaqueBin[selectedEdgesOpaqueBinLen++] = drawable;
}
if (renderFlags.xrayedEdgesTransparent) {
xrayEdgesTransparentBin[xrayEdgesTransparentBinLen++] = drawable;
}
if (renderFlags.xrayedEdgesOpaque) {
xrayEdgesOpaqueBin[xrayEdgesOpaqueBinLen++] = drawable;
}
if (renderFlags.highlightedEdgesTransparent) {
highlightedEdgesTransparentBin[highlightedEdgesTransparentBinLen++] = drawable;
}
if (renderFlags.highlightedEdgesOpaque) {
highlightedEdgesOpaqueBin[highlightedEdgesOpaqueBinLen++] = drawable;
}
}
}
//------------------------------------------------------------------------------------------------------
// Render deferred bins
//------------------------------------------------------------------------------------------------------
// Opaque color with SAO
if (normalDrawSAOBinLen > 0) {
frameCtx.withSAO = true;
for (i = 0; i < normalDrawSAOBinLen; i++) {
normalDrawSAOBin[i].drawColorOpaque(frameCtx);
}
}
// Opaque edges
if (normalEdgesOpaqueBinLen > 0) {
for (i = 0; i < normalEdgesOpaqueBinLen; i++) {
normalEdgesOpaqueBin[i].drawEdgesColorOpaque(frameCtx);
}
}
// Opaque X-ray fill
if (xrayedFillOpaqueBinLen > 0) {
for (i = 0; i < xrayedFillOpaqueBinLen; i++) {
xrayedFillOpaqueBin[i].drawSilhouetteXRayed(frameCtx);
}
}
// Opaque X-ray edges
if (xrayEdgesOpaqueBinLen > 0) {
for (i = 0; i < xrayEdgesOpaqueBinLen; i++) {
xrayEdgesOpaqueBin[i].drawEdgesXRayed(frameCtx);
}
}
// Transparent
if (xrayedFillTransparentBinLen > 0 || xrayEdgesTransparentBinLen > 0 || normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) {
gl.enable(gl.CULL_FACE);
gl.enable(gl.BLEND);
if (canvasTransparent) {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
} else {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
frameCtx.backfaces = false;
if (!alphaDepthMask) {
gl.depthMask(false);
}
// Transparent color edges
if (normalFillTransparentBinLen > 0 || normalEdgesTransparentBinLen > 0) {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
if (normalEdgesTransparentBinLen > 0) {
for (i = 0; i < normalEdgesTransparentBinLen; i++) {
drawable = normalEdgesTransparentBin[i];
drawable.drawEdgesColorTransparent(frameCtx);
}
}
// Transparent color fill
if (normalFillTransparentBinLen > 0) {
const eye = frameCtx.viewParams.eye;
normalFillTransparentBin.length = normalFillTransparentBinLen; // normalFillTransparentBin reused by renderDrawables calls, so needs to be truncated if necessary
const byDist = normalFillTransparentBin.map(d => ({ drawable: d, distSq: math.distVec3(d.origin || vec3_0, eye) }));
byDist.sort((a, b) => b.distSq - a.distSq);
for (i = 0; i < normalFillTransparentBinLen; i++) {
byDist[i].drawable.drawColorTransparent(frameCtx);
}
}
// Transparent X-ray edges
if (xrayEdgesTransparentBinLen > 0) {
for (i = 0; i < xrayEdgesTransparentBinLen; i++) {
xrayEdgesTransparentBin[i].drawEdgesXRayed(frameCtx);
}
}
// Transparent X-ray fill
if (xrayedFillTransparentBinLen > 0) {
for (i = 0; i < xrayedFillTransparentBinLen; i++) {
xrayedFillTransparentBin[i].drawSilhouetteXRayed(frameCtx);
}
}
gl.disable(gl.BLEND);
if (!alphaDepthMask) {
gl.depthMask(true);
}
}
// Opaque highlight
if (highlightedFillOpaqueBinLen > 0 || highlightedEdgesOpaqueBinLen > 0) {
frameCtx.lastProgramId = null;
if (scene.highlightMaterial.glowThrough) {
gl.clear(gl.DEPTH_BUFFER_BIT);
}
// Opaque highlighted edges
if (highlightedEdgesOpaqueBinLen > 0) {
for (i = 0; i < highlightedEdgesOpaqueBinLen; i++) {
highlightedEdgesOpaqueBin[i].drawEdgesHighlighted(frameCtx);
}
}
// Opaque highlighted fill
if (highlightedFillOpaqueBinLen > 0) {
for (i = 0; i < highlightedFillOpaqueBinLen; i++) {
highlightedFillOpaqueBin[i].drawSilhouetteHighlighted(frameCtx);
}
}
}
// Highlighted transparent
if (highlightedFillTransparentBinLen > 0 || highlightedEdgesTransparentBinLen > 0 || highlightedFillOpaqueBinLen > 0) {
frameCtx.lastProgramId = null;
if (scene.selectedMaterial.glowThrough) {
gl.clear(gl.DEPTH_BUFFER_BIT);
}
gl.enable(gl.BLEND);
if (canvasTransparent) {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
} else {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
gl.enable(gl.CULL_FACE);
// Highlighted transparent edges
if (highlightedEdgesTransparentBinLen > 0) {
for (i = 0; i < highlightedEdgesTransparentBinLen; i++) {
highlightedEdgesTransparentBin[i].drawEdgesHighlighted(frameCtx);
}
}
// Highlighted transparent fill
if (highlightedFillTransparentBinLen > 0) {
for (i = 0; i < highlightedFillTransparentBinLen; i++) {
highlightedFillTransparentBin[i].drawSilhouetteHighlighted(frameCtx);
}
}
gl.disable(gl.BLEND);
}
// Selected opaque
if (selectedFillOpaqueBinLen > 0 || selectedEdgesOpaqueBinLen > 0) {
frameCtx.lastProgramId = null;
if (scene.selectedMaterial.glowThrough) {
gl.clear(gl.DEPTH_BUFFER_BIT);
}
// Selected opaque fill
if (selectedEdgesOpaqueBinLen > 0) {
for (i = 0; i < selectedEdgesOpaqueBinLen; i++) {
selectedEdgesOpaqueBin[i].drawEdgesSelected(frameCtx);
}
}
// Selected opaque edges
if (selectedFillOpaqueBinLen > 0) {
for (i = 0; i < selectedFillOpaqueBinLen; i++) {
selectedFillOpaqueBin[i].drawSilhouetteSelected(frameCtx);
}
}
}
// Selected transparent
if (selectedFillTransparentBinLen > 0 || selectedEdgesTransparentBinLen > 0) {
frameCtx.lastProgramId = null;
if (scene.selectedMaterial.glowThrough) {
gl.clear(gl.DEPTH_BUFFER_BIT);
}
gl.enable(gl.CULL_FACE);
gl.enable(gl.BLEND);
if (canvasTransparent) {
gl.blendEquation(gl.FUNC_ADD);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
} else {
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
}
// Selected transparent edges
if (selectedEdgesTransparentBinLen > 0) {
for (i = 0; i < selectedEdgesTransparentBinLen; i++) {
selectedEdgesTransparentBin[i].drawEdgesSelected(frameCtx);
}
}
// Selected transparent fill
if (selectedFillTransparentBinLen > 0) {
for (i = 0; i < selectedFillTransparentBinLen; i++) {
selectedFillTransparentBin[i].drawSilhouetteSelected(frameCtx);
}
}
gl.disable(gl.BLEND);
}
};
renderDrawables(postCullDrawableList);
if (uiDrawableList.length > 0) {
gl.clear(gl.DEPTH_BUFFER_BIT);
renderDrawables(uiDrawableList);
}
frameCtx.testAABB = null;
const endTime = Date.now();
const frameStats = stats.frame;
frameStats.renderTime = (endTime - startTime) / 1000.0;
frameStats.drawElements = frameCtx.drawElements;
frameStats.drawArrays = frameCtx.drawArrays;
frameStats.useProgram = frameCtx.useProgram;
frameStats.bindTexture = frameCtx.bindTexture;
frameStats.bindArray = frameCtx.bindArray;
const numTextureUnits = WEBGL_INFO.MAX_TEXTURE_IMAGE_UNITS;
for (let ii = 0; ii < numTextureUnits; ii++) {
gl.activeTexture(gl.TEXTURE0 + ii);
}
gl.bindTexture(gl.TEXTURE_CUBE_MAP, null);
gl.bindTexture(gl.TEXTURE_2D, null);
const numVertexAttribs = WEBGL_INFO.MAX_VERTEX_ATTRIBS; // Fixes https://github.com/xeokit/xeokit-sdk/issues/174
for (let ii = 0; ii < numVertexAttribs; ii++) {
gl.disableVertexAttribArray(ii);
}
}
const resetPickFrameCtx = (canvasPos, clipTransformDiv, camera, eye, projMatrix, viewMatrix, frameCtx) => {
frameCtx.reset();
frameCtx.backfaces = true;
frameCtx.frontface = true; // "ccw"
frameCtx.viewParams.eye = eye;
frameCtx.viewParams.projMatrix = projMatrix;
frameCtx.viewParams.viewMatrix = viewMatrix;
frameCtx.nearPlaneHeight = getNearPlaneHeight(camera, gl.drawingBufferHeight);
const resolutionScale = scene.canvas.resolutionScale;
frameCtx.pickClipPos = [
canvasPos ? ( 2 * canvasPos[0] * resolutionScale / gl.drawingBufferWidth - 1) : 0,
canvasPos ? (1 - 2 * canvasPos[1] * resolutionScale / gl.drawingBufferHeight) : 0
];
frameCtx.pickClipPosInv = [
gl.drawingBufferWidth / clipTransformDiv,
gl.drawingBufferHeight / clipTransformDiv
];
};
/**
* Picks an Entity.
* @private
*/
this.pick = (function () {
const tempVec3a = math.vec3();
const tempVec3b = math.vec3();
const tempVec4a = math.vec4();
const tempVec4b = math.vec4();
const tempVec4c = math.vec4();
const tempVec4d = math.vec4();
const tempVec4e = math.vec4();
const tempMat4a = math.mat4();
const tempMat4b = math.mat4();
const tempMat4c = math.mat4();
const tempMat4d = math.mat4();
const upVec = math.vec3([0, 1, 0]);
const _pickResult = new PickResult();
const nearAndFar = math.vec2();
const canvasPos = math.vec3();
const worldRayOrigin = math.vec3();
const worldRayDir = math.vec3();
const worldSurfacePos = math.vec3();
const worldSurfaceNormal = math.vec3();
const pickBuffer = new RenderBuffer(gl);
pickBuffer.setSize([1, 1]);
const pickNormalBuffer = new RenderBuffer(gl, [gl.RGBA32I]);
pickNormalBuffer.setSize([3, 3]);
return function (params, pickResult = _pickResult) {
pickResult.reset();
updateDrawlist();
let pickViewMatrix = null;
let pickProjMatrix = null;
let projection = null;
const camera = scene.camera;
pickResult.pickSurface = params.pickSurface;
if (params.canvasPos) {
canvasPos[0] = params.canvasPos[0];
canvasPos[1] = params.canvasPos[1];
pickViewMatrix = camera.viewMatrix;
pickProjMatrix = camera.projMatrix;
projection = camera.projection;
nearAndFar[0] = camera.project.near;
nearAndFar[1] = camera.project.far;
pickResult.canvasPos = params.canvasPos;
math.canvasPosToWorldRay(canvas, pickViewMatrix, pickProjMatrix, projection, canvasPos, tempVec3a, tempVec3b);
frameCtx.testAABB = makeRayAABBIntersectionTest(tempVec3a, tempVec3b);
} else {
// Picking with arbitrary World-space ray
// Align camera along ray and fire ray through center of canvas
canvasPos[0] = canvas.clientWidth * 0.5;
canvasPos[1] = canvas.clientHeight * 0.5;
if (params.matrix) {
pickViewMatrix = params.matrix;
pickProjMatrix = camera.projMatrix;
projection = camera.projection;
nearAndFar[0] = camera.project.near;
nearAndFar[1] = camera.project.far;
math.canvasPosToWorldRay(canvas, pickViewMatrix, pickProjMatrix, projection, canvasPos, tempVec3a, tempVec3b);
frameCtx.testAABB = makeRayAABBIntersectionTest(tempVec3a, tempVec3b);
} else {
worldRayOrigin.set(params.origin || [0, 0, 0]);
worldRayDir.set(params.direction || [0, 0, 1]);
const look = math.addVec3(worldRayOrigin, worldRayDir, tempVec3a);
const up = tempVec3b;
if (Math.abs(math.dotVec3(worldRayDir, upVec)) > (1 - 1e-6)) { // worldRayDir aligned with Y axis
up[0] = 0;
up[1] = 0;
up[2] = Math.sign(worldRayDir[1]);
} else {
math.cross3Vec3(worldRayDir, upVec, up);
math.cross3Vec3(up, worldRayDir, up);
math.normalizeVec3(up, up);
}
pickViewMatrix = math.lookAtMat4v(worldRayOrigin, look, up, tempMat4b);
// pickProjMatrix = camera.projMatrix;
pickProjMatrix = camera.ortho.matrix;
projection = "ortho";
nearAndFar[0] = camera.ortho.near;
nearAndFar[1] = camera.ortho.far;
pickResult.origin = worldRayOrigin;
pickResult.direction = worldRayDir;
frameCtx.testAABB = makeRayAABBIntersectionTest(pickResult.origin, pickResult.direction);
}
}
pickBuffer.bind();
const resetFrameCtx = (clipTransformDiv) => resetPickFrameCtx(canvasPos, clipTransformDiv, camera, pickResult.origin || camera.eye, pickProjMatrix || camera.projMatrix, pickViewMatrix || camera.viewMatrix, frameCtx);
// gpuPickPickable
resetFrameCtx(1);
frameCtx.pickInvisible = !!params.pickInvisible;
gl.viewport(0, 0, 1, 1);
gl.depthMask(true);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
const includeEntityIds = params.includeEntityIds;
const excludeEntityIds = params.excludeEntityIds;
const renderDrawables = function(drawables) {
drawables.forEach(drawable => {
if (!drawable.culled && drawable.visible && drawable.pickable && drawable.drawPickMesh // TODO: push this logic into drawable
&& ((! includeEntityIds) || includeEntityIds[drawable.id])
&& ((! excludeEntityIds) || (! excludeEntityIds[drawable.id]))) {
drawable.drawPickMesh(frameCtx);
}
});
};
renderDrawables(postCullDrawableList);
if (uiDrawableList.length > 0) {
gl.clear(gl.DEPTH_BUFFER_BIT);
renderDrawables(uiDrawableList);
}
frameCtx.testAABB = null;
const pickID = pixelToInt(pickBuffer.read(0, 0));
const pickable = (pickID >= 0) && pickIDs.items[pickID];
if (!pickable) {
pickBuffer.unbind();
return null;
}
const pickedEntity = (pickable.delegatePickedEntity) ? pickable.delegatePickedEntity() : pickable;
if (!pickedEntity) {
pickBuffer.unbind();
return null;
}
if (params.pickSurface) {
// GPU-based ray-picking
if (pickable.canPickTriangle && pickable.canPickTriangle()) {
if (pickable.drawPickTriangles) {
resetFrameCtx(1);
// frameCtx.pickInvisible = !!params.pickInvisible;
gl.viewport(0, 0, 1, 1);
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
pickable.drawPickTriangles(frameCtx);
pickResult.primIndex = 3 * pixelToInt(pickBuffer.read(0, 0)); // Convert from triangle number to first vertex in indices
}
pickable.pickTriangleSurface(pickViewMatrix, pickProjMatrix, projection, pickResult);
pickResult.pickSurfacePrecision = false;
} else {
if (pickable.canPickWorldPos && pickable.canPickWorldPos()) {
// pickWorldPos
resetFrameCtx(1);
frameCtx.viewParams.near = nearAndFar[0];
frameCtx.viewParams.far = nearAndFar[1];
gl.viewport(0, 0, 1, 1);
gl.clearColor(0, 0, 0, 0);
gl.depthMask(true);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
pickable.drawPickDepths(frameCtx); // Draw color-encoded fragment screen-space depths
const screenZ = math.dotVec4(pickBuffer.read(0, 0), bitShiftScreenZ);
// Calculate clip space coordinates, which will be in range of x=[-1..1] and y=[-1..1], with y=(+1) at top
const x = (canvasPos[0] - canvas.clientWidth / 2) / (canvas.clientWidth / 2);
const y = -(canvasPos[1] - canvas.clientHeight / 2) / (canvas.clientHeight / 2);
const origin = pickable.origin;
const pvMatInverse = math.inverseMat4(math.mulMat4(pickProjMatrix, (origin ? createRTCViewMat(pickViewMatrix, origin, tempMat4a) : pickViewMatrix), tempMat4c), tempMat4d);
const toWorld = (z, dst) => {
dst[0] = x;
dst[1] = y;
dst[2] = z;
dst[3] = 1;
math.transformVec4(pvMatInverse, dst, dst);
return math.mulVec4Scalar(dst, 1 / dst[3]);
};
const world1 = toWorld(-1, tempVec4a);
const world2 = toWorld( 1, tempVec4b);
const dir = math.subVec3(world2, world1, tempVec4c);
const worldPos = math.addVec3(world1, math.mulVec4Scalar(dir, screenZ, tempVec4d), tempVec4e);
if (origin) {
math.addVec3(worldPos, origin);
}
pickResult.worldPos = worldPos;
if (params.pickSurfaceNormal !== false) {
// gpuPickWorldNormal
resetFrameCtx(3);
pickNormalBuffer.bind();
gl.viewport(0, 0, pickNormalBuffer.size[0], pickNormalBuffer.size[1]);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0]));
pickable.drawPickNormals(frameCtx); // Draw color-encoded fragment World-space normals
const pix = pickNormalBuffer.read(1, 1, gl.RGBA_INTEGER, gl.INT, Int32Array, 4);
pickNormalBuffer.unbind();
pickResult.worldNormal = toWorldNormal(pix);
}
pickResult.pickSurfacePrecision = false;
}
}
}
pickBuffer.unbind();
pickResult.entity = pickedEntity;
return pickResult;
};
})();
/**
* @param {[number, number]} canvasPos
* @param {number} [snapRadiusInPixels=30]
* @param {boolean} [snapToVertex=true]
* @param {boolean} [snapToEdge=true]
* @param pickResult
* @returns {PickResult}
*/
this.snapPick = (function () {
const _pickResult = new PickResult();
const getVertexPickBuffer = (function() {
const cache = { };
return (snapRadiusInPixels) => {
if (! (snapRadiusInPixels in cache)) {
const buf = new RenderBuffer(gl, [gl.RGBA32I, gl.RGBA32I, gl.RGBA8UI], true);
buf.setSize([2 * snapRadiusInPixels + 1, 2 * snapRadiusInPixels + 1]);
cache[snapRadiusInPixels] = buf;
}
return cache[snapRadiusInPixels];
};
})();
return function (params, pickResult = _pickResult) {
const {canvasPos, origin, direction, snapRadius, snapToVertex, snapToEdge} = params;
if (!snapToVertex && !snapToEdge) {
return this.pick({canvasPos, pickSurface: true});
}
const camera = scene.camera;
const snapRadiusInPixels = snapRadius || 30;
const viewMatrix = (canvasPos
? camera.viewMatrix
: math.lookAtMat4v(
origin,
math.addVec3(origin, direction, math.vec3()),
math.vec3([0, 1, 0]),
math.mat4()));
resetPickFrameCtx(canvasPos, 2 * snapRadiusInPixels, camera, camera.eye, camera.projMatrix, viewMatrix, frameCtx);
// Bind and clear the snap render target
const vertexPickBuffer = getVertexPickBuffer(snapRadiusInPixels);
vertexPickBuffer.bind();
gl.viewport(0, 0, vertexPickBuffer.size[0], vertexPickBuffer.size[1]);
gl.enable(gl.DEPTH_TEST);
gl.frontFace(gl.CCW);
gl.disable(gl.CULL_FACE);
gl.depthMask(true);
gl.disable(gl.BLEND);
gl.depthFunc(gl.LEQUAL);
gl.clear(gl.DEPTH_BUFFER_BIT);
gl.clearBufferiv(gl.COLOR, 0, new Int32Array([0, 0, 0, 0]));
gl.clearBufferiv(gl.COLOR, 1, new Int32Array([0, 0, 0, 0]));
gl.clearBufferuiv(gl.COLOR, 2, new Uint32Array([0, 0, 0, 0]));
// a) init z-buffer
gl.drawBuffers([gl.COLOR_ATTACHMENT0, gl.COLOR_ATTACHMENT1, gl.COLOR_ATTACHMENT2]);
frameCtx.snapPickLayerParams = [];
frameCtx.snapPickLayerParams.push(null); // This recreates previous situation, which relied on snapPickLayerNumber
postCullDrawableList.forEach(drawable => {
if (!drawable.culled && drawable.visible && drawable.pickable && drawable.drawSnapInit) {
drawable.drawSnapInit(frameCtx);
}
});
const layerParamsSurface = frameCtx.snapPickLayerParams;
// b) snap-pick
frameCtx.snapPickLayerParams = [];
gl.depthMask(false);
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
const drawSnap = (snapMode) => {
frameCtx.snapMode = snapMode;
frameCtx.snapPickLayerParams.push(null); // This recreates previous situation, which relied on snapPickLayerNumber
postCullDrawableList.forEach(drawable => {
if (!drawable.culled && drawable.visible && drawable.pickable && drawable.drawSnap) {
drawable.drawSnap(frameCtx);
}
});
};
if (snapToEdge) {
drawSnap("edge");
}
if (snapToVertex) {
drawSnap("vertex");
}
gl.depthMask(true);
const layerParamsSnap = frameCtx.snapPickLayerParams;
// Read and decode the snapped coordinates
const readSnapPickBuffer = (source, arrayType, glType) => {
const w = vertexPickBuffer.buffer.width;
const h = vertexPickBuffer.buffer.height;
const pix = new arrayType(4 * w * h);
gl.readBuffer(source);
gl.readPixels(0, 0, w, h, gl.RGBA_INTEGER, glType, pix, 0);
return pix;
};
const snapPickResultArray = readSnapPickBuffer(gl.COLOR_ATTACHMENT0, Int32Array, gl.INT);
const snapPickNormalResultArray = readSnapPickBuffer(gl.COLOR_ATTACHMENT1, Int32Array, gl.INT);
const snapPickIdResultArray = readSnapPickBuffer(gl.COLOR_ATTACHMENT2, Uint32Array, gl.UNSIGNED_INT);
vertexPickBuffer.unbind();
// result 1) regular hi-precision world position
let worldPos = null;
let worldNormal = null;
let pickable = null;
const middleX = snapRadiusInPixels;
const middleY = snapRadiusInPixels;
const middleIndex = (middleX * 4) + (middleY * vertexPickBuffer.size[0] * 4);
const pickResultMiddleXY = snapPickResultArray.slice(middleIndex, middleIndex + 4);
const pickNormalResultMiddleXY = snapPickNormalResultArray.slice(middleIndex, middleIndex + 4);
const pickPickableResultMiddleXY = snapPickIdResultArray.slice(middleIndex, middleIndex + 4);
if (pickResultMiddleXY[3] !== 0) {
const pickedLayerParmasSurface = layerParamsSurface[Math.abs(pickResultMiddleXY[3]) % layerParamsSurface.length];
worldPos = toWorldPos(pickResultMiddleXY, pickedLayerParmasSurface.origin, pickedLayerParmasSurface.coordinateScale);
worldNormal = toWorldNormal(pickNormalResultMiddleXY);
pickable = pickIDs.items[pixelToInt(pickPickableResultMiddleXY)];
}
// result 2) hi-precision snapped (to vertex/edge) world position
const snapPickResult = [ ];
for (let i = 0; i < snapPickResultArray.length; i += 4) {
const layerNumber = snapPickResultArray[i + 3];
if (layerNumber > 0) {
const pixelNumber = Math.floor(i / 4);
const w = vertexPickBuffer.size[0];
const x = pixelNumber % w - Math.floor(w / 2);
const y = Math.floor(pixelNumber / w) - Math.floor(w / 2);
snapPickResult.push({
dist: math.lenVec2([ x, y ]),
isVertex: (snapToVertex && snapToEdge) ? (layerNumber > layerParamsSnap.length / 2) : snapToVertex,
result: snapPickResultArray.subarray(i, i+4),
normal: snapPickNormalResultArray.subarray(i, i+4),
id: snapPickIdResultArray.subarray(i, i+4)
});
}
}
const getPickedEntity = pickable => (pickable && pickable.delegatePickedEntity) ? pickable.delegatePickedEntity() : pickable;
if (snapPickResult.length > 0) {
// closest vertex snap first, then closest edge snap
const res = snapPickResult.reduce((a,b) => ((((a.isVertex-b.isVertex) || (b.dist-a.dist)) > 0) ? a : b));
const snapPick = res.result;
const pickedLayerParmas = layerParamsSnap[snapPick[3]];
const snappedWorldPos = toWorldPos(snapPick, pickedLayerParmas.origin, pickedLayerParmas.coordinateScale);
const snappedCanvasPos = camera.projectWorldPos(snappedWorldPos);
pickResult.reset();
pickResult.snappedToEdge = !res.isVertex;
pickResult.snappedToVertex = res.isVertex;
pickResult.worldPos = snappedWorldPos;
pickResult.worldNormal = toWorldNormal(res.normal);
pickResult.entity = getPickedEntity(pickIDs.items[pixelToInt(res.id)]);
pickResult.canvasPos = canvasPos || (worldPos && camera.projectWorldPos(worldPos)) || snappedCanvasPos;
pickResult.snappedCanvasPos = snappedCanvasPos;
return pickResult;
} else if (worldPos) {
pickResult.reset();
pickResult.snappedToEdge = false;
pickResult.snappedToVertex = false;
pickResult.worldPos = worldPos;
pickResult.worldNormal = worldNormal;
pickResult.entity = getPickedEntity(pickable);
pickResult.canvasPos = canvasPos || camera.projectWorldPos(worldPos);
pickResult.snappedCanvasPos = canvasPos;
return pickResult;
} else {
return null;
}
};
})();
/**
* Adds a {@link Marker} for occlusion testing.
* @param marker
*/
this.addMarker = function (marker) {
this._occlusionTester = this._occlusionTester || new OcclusionTester(scene);
this._occlusionTester.addMarker(marker);
scene.occlusionTestCountdown = 0;
};
/**
* Notifies that a {@link Marker#worldPos} has updated.
* @param marker
*/
this.markerWorldPosUpdated = function (marker) {
this._occlusionTester.markerWorldPosUpdated(marker);
};
/**
* Removes a {@link Marker} from occlusion testing.
* @param marker
*/
this.removeMarker = function (marker) {
this._occlusionTester.removeMarker(marker);
};
/**
* Performs an occlusion test for all added {@link Marker}s, updating
* their {@link Marker#visible} properties accordingly.
*/
this.doOcclusionTest = function () {
if (this._occlusionTester && this._occlusionTester.needOcclusionTest) {
updateDrawlist();
const readPixelBuf = (! OCCLUSION_TEST_MODE) && renderBufferManager.getRenderBuffer("occlusionReadPix");
if (readPixelBuf) {
readPixelBuf.setSize([gl.drawingBufferWidth, gl.drawingBufferHeight]);
readPixelBuf.bind();
}
frameCtx.reset();
frameCtx.backfaces = true;
frameCtx.frontface = true; // "ccw"
frameCtx.viewParams = getSceneCameraViewParams();
frameCtx.nearPlaneHeight = getNearPlaneHeight(scene.camera, gl.drawingBufferHeight);
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
postCullDrawableList.forEach(drawable => {
if (!drawable.culled && drawable.visible && drawable.pickable && drawable.drawOcclusion) { // TODO: Option to exclude transparent?
drawable.drawOcclusion(frameCtx);
}
});
this._occlusionTester.drawMarkers();
if (readPixelBuf) {
const resolutionScale = scene.canvas.resolutionScale;
this._occlusionTester.doOcclusionTest( // Updates Marker "visible" properties
(x, y) => readPixelBuf.read(Math.round(resolutionScale * x), Math.round(resolutionScale * y)));
readPixelBuf.unbind();
}
}
};
this.snapshot = (() => {
let snapshotBound = false;
const snapshotBuffer = new RenderBuffer(gl);
let snapshotCanvas = null;
return {
/**
* Read pixels from the renderer's current output. Performs a force-render first.
* @param pixels
* @param colors
* @param len
* @private
*/
readPixels: (pixels, colors, len) => {
snapshotBuffer.bind();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
imageDirty = true;
this.render();
for (let i = 0; i < len; i++) {
const j = i * 2;
const k = i * 4;
const color = snapshotBuffer.read(pixels[j], pixels[j + 1]);
colors[k] = color[0];
colors[k + 1] = color[1];
colors[k + 2] = color[2];
colors[k + 3] = color[3];
}
snapshotBuffer.unbind();
imageDirty = true;
},
/**
* Enter snapshot mode.
*
* Switches rendering to a hidden snapshot canvas.
*
* Exit snapshot mode using endSnapshot().
*/
beginSnapshot: () => {
snapshotBuffer.setSize([gl.drawingBufferWidth, gl.drawingBufferHeight]);
snapshotBuffer.bind();
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
snapshotBound = true;
},
/**
* Returns an HTMLCanvas containing an image of the snapshot canvas.
*
* - The HTMLCanvas has a CanvasRenderingContext2D.
* - Expects the caller to draw more things on the HTMLCanvas (annotations etc).
*
* @returns {HTMLCanvasElement}
*/
renderSnapshotToCanvas: () => {
const width = snapshotBuffer.buffer.width;
const height = snapshotBuffer.buffer.height;
if ((! snapshotCanvas) || (snapshotCanvas.width !== width) || (snapshotCanvas.height !== height)) {
snapshotCanvas = (function() {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = width;
canvas.height = height;
const pixelData = new Uint8Array(width * height * 4);
const imageData = context.createImageData(width, height);
return {
width: width,
height: height,
getUpdatedCanvas: () => {
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixelData);
// flip verically
const halfHeight = Math.floor(height / 2);
const bytesPerRow = width * 4;
const temp = new Uint8Array(bytesPerRow);
for (let y = 0; y < halfHeight; ++y) {
const topOffset = y * bytesPerRow;
const bottomOffset = (height - y - 1) * bytesPerRow;
temp.set(pixelData.subarray(topOffset, topOffset + bytesPerRow));
pixelData.copyWithin(topOffset, bottomOffset, bottomOffset + bytesPerRow);
pixelData.set(temp, bottomOffset);
}
imageData.data.set(pixelData);
context.resetTransform(); // Prevents strange scale-accumulation effect with html2canvas
context.putImageData(imageData, 0, 0);
return canvas;
}
};
})();
}
if (snapshotBound) {
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
imageDirty = true;
this.render();
imageDirty = true;
}
return snapshotCanvas.getUpdatedCanvas();
},
/**
* Exists snapshot mode.
*
* Switches rendering back to the main canvas.
*/
endSnapshot: () => {
if (snapshotBound) {
snapshotBuffer.unbind();
snapshotBound = false;
}
imageDirty = true;
this.render();
}
};
})();
/**
* Destroys this renderer.
* @private
*/
this.destroy = function () {
drawableTypeInfo = {};
drawables = {};
renderBufferManager.destroy();
saoOcclusionRenderer.destroy();
saoDepthLimitedBlurRenderer.destroy();
if (this._occlusionTester) {
this._occlusionTester.destroy();
}
};
};
export {Renderer};