@openhps/core
Version:
Open Hybrid Positioning System - Core component
1,525 lines (1,405 loc) • 61.1 kB
JavaScript
import GLSLNodeBuilder from './nodes/GLSLNodeBuilder.js';
import Backend from '../common/Backend.js';
import { getCacheKey } from '../common/RenderContext.js';
import WebGLAttributeUtils from './utils/WebGLAttributeUtils.js';
import WebGLState from './utils/WebGLState.js';
import WebGLUtils from './utils/WebGLUtils.js';
import WebGLTextureUtils from './utils/WebGLTextureUtils.js';
import WebGLExtensions from './utils/WebGLExtensions.js';
import WebGLCapabilities from './utils/WebGLCapabilities.js';
import { GLFeatureName } from './utils/WebGLConstants.js';
import { WebGLBufferRenderer } from './WebGLBufferRenderer.js';
import { warnOnce } from '../../utils.js';
import { WebGLCoordinateSystem } from '../../constants.js';
import WebGLTimestampQueryPool from './utils/WebGLTimestampQueryPool.js';
/**
* A backend implementation targeting WebGL 2.
*
* @private
* @augments Backend
*/
class WebGLBackend extends Backend {
/**
* WebGLBackend options.
*
* @typedef {Object} WebGLBackend~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. Set this parameter to any other integer value than 0 to overwrite the default.
* @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
* @property {WebGL2RenderingContext} [context=undefined] - A WebGL 2 rendering context.
*/
/**
* Constructs a new WebGPU backend.
*
* @param {WebGLBackend~Options} [parameters] - The configuration parameter.
*/
constructor(parameters = {}) {
super(parameters);
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWebGLBackend = true;
/**
* A reference to a backend module holding shader attribute-related
* utility functions.
*
* @type {?WebGLAttributeUtils}
* @default null
*/
this.attributeUtils = null;
/**
* A reference to a backend module holding extension-related
* utility functions.
*
* @type {?WebGLExtensions}
* @default null
*/
this.extensions = null;
/**
* A reference to a backend module holding capability-related
* utility functions.
*
* @type {?WebGLCapabilities}
* @default null
*/
this.capabilities = null;
/**
* A reference to a backend module holding texture-related
* utility functions.
*
* @type {?WebGLTextureUtils}
* @default null
*/
this.textureUtils = null;
/**
* A reference to a backend module holding renderer-related
* utility functions.
*
* @type {?WebGLBufferRenderer}
* @default null
*/
this.bufferRenderer = null;
/**
* A reference to the rendering context.
*
* @type {?WebGL2RenderingContext}
* @default null
*/
this.gl = null;
/**
* A reference to a backend module holding state-related
* utility functions.
*
* @type {?WebGLState}
* @default null
*/
this.state = null;
/**
* A reference to a backend module holding common
* utility functions.
*
* @type {?WebGLUtils}
* @default null
*/
this.utils = null;
/**
* Dictionary for caching VAOs.
*
* @type {Object<string,WebGLVertexArrayObject>}
*/
this.vaoCache = {};
/**
* Dictionary for caching transform feedback objects.
*
* @type {Object<string,WebGLTransformFeedback>}
*/
this.transformFeedbackCache = {};
/**
* Controls if `gl.RASTERIZER_DISCARD` should be enabled or not.
* Only relevant when using compute shaders.
*
* @type {boolean}
* @default false
*/
this.discard = false;
/**
* A reference to the `EXT_disjoint_timer_query_webgl2` extension. `null` if the
* device does not support the extension.
*
* @type {?EXTDisjointTimerQueryWebGL2}
* @default null
*/
this.disjoint = null;
/**
* A reference to the `KHR_parallel_shader_compile` extension. `null` if the
* device does not support the extension.
*
* @type {?KHRParallelShaderCompile}
* @default null
*/
this.parallel = null;
/**
* A reference to the current render context.
*
* @private
* @type {RenderContext}
* @default null
*/
this._currentContext = null;
/**
* A unique collection of bindings.
*
* @private
* @type {WeakSet}
*/
this._knownBindings = new WeakSet();
/**
* Whether the device supports framebuffers invalidation or not.
*
* @private
* @type {boolean}
*/
this._supportsInvalidateFramebuffer = typeof navigator === 'undefined' ? false : /OculusBrowser/g.test(navigator.userAgent);
/**
* The target framebuffer when rendering with
* the WebXR device API.
*
* @private
* @type {WebGLFramebuffer}
* @default null
*/
this._xrFramebuffer = null;
}
/**
* Initializes the backend so it is ready for usage.
*
* @param {Renderer} renderer - The renderer.
*/
init(renderer) {
super.init(renderer);
//
const parameters = this.parameters;
const contextAttributes = {
antialias: renderer.samples > 0,
alpha: true,
// always true for performance reasons
depth: renderer.depth,
stencil: renderer.stencil
};
const glContext = parameters.context !== undefined ? parameters.context : renderer.domElement.getContext('webgl2', contextAttributes);
function onContextLost(event) {
event.preventDefault();
const contextLossInfo = {
api: 'WebGL',
message: event.statusMessage || 'Unknown reason',
reason: null,
originalEvent: event
};
renderer.onDeviceLost(contextLossInfo);
}
this._onContextLost = onContextLost;
renderer.domElement.addEventListener('webglcontextlost', onContextLost, false);
this.gl = glContext;
this.extensions = new WebGLExtensions(this);
this.capabilities = new WebGLCapabilities(this);
this.attributeUtils = new WebGLAttributeUtils(this);
this.textureUtils = new WebGLTextureUtils(this);
this.bufferRenderer = new WebGLBufferRenderer(this);
this.state = new WebGLState(this);
this.utils = new WebGLUtils(this);
this.extensions.get('EXT_color_buffer_float');
this.extensions.get('WEBGL_clip_cull_distance');
this.extensions.get('OES_texture_float_linear');
this.extensions.get('EXT_color_buffer_half_float');
this.extensions.get('WEBGL_multisampled_render_to_texture');
this.extensions.get('WEBGL_render_shared_exponent');
this.extensions.get('WEBGL_multi_draw');
this.disjoint = this.extensions.get('EXT_disjoint_timer_query_webgl2');
this.parallel = this.extensions.get('KHR_parallel_shader_compile');
}
/**
* The coordinate system of the backend.
*
* @type {number}
* @readonly
*/
get coordinateSystem() {
return WebGLCoordinateSystem;
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU.
*
* @async
* @param {StorageBufferAttribute} attribute - The storage buffer attribute.
* @return {Promise<ArrayBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync(attribute) {
return await this.attributeUtils.getArrayBufferAsync(attribute);
}
/**
* Can be used to synchronize CPU operations with GPU tasks. So when this method is called,
* the CPU waits for the GPU to complete its operation (e.g. a compute task).
*
* @async
* @return {Promise} A Promise that resolves when synchronization has been finished.
*/
async waitForGPU() {
await this.utils._clientWaitAsync();
}
/**
* Ensures the backend is XR compatible.
*
* @async
* @return {Promise} A Promise that resolve when the renderer is XR compatible.
*/
async makeXRCompatible() {
const attributes = this.gl.getContextAttributes();
if (attributes.xrCompatible !== true) {
await this.gl.makeXRCompatible();
}
}
/**
* Sets the XR rendering destination.
*
* @param {WebGLFramebuffer} xrFramebuffer - The XR framebuffer.
*/
setXRTarget(xrFramebuffer) {
this._xrFramebuffer = xrFramebuffer;
}
/**
* Configures the given XR render target with external textures.
*
* This method is only relevant when using the WebXR Layers API.
*
* @param {XRRenderTarget} renderTarget - The XR render target.
* @param {WebGLTexture} colorTexture - A native color texture.
* @param {?WebGLTexture} [depthTexture=null] - A native depth texture.
*/
setXRRenderTargetTextures(renderTarget, colorTexture, depthTexture = null) {
const gl = this.gl;
this.set(renderTarget.texture, {
textureGPU: colorTexture,
glInternalFormat: gl.RGBA8
}); // see #24698 why RGBA8 and not SRGB8_ALPHA8 is used
if (depthTexture !== null) {
const glInternalFormat = renderTarget.stencilBuffer ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;
this.set(renderTarget.depthTexture, {
textureGPU: depthTexture,
glInternalFormat: glInternalFormat
});
// The multisample_render_to_texture extension doesn't work properly if there
// are midframe flushes and an external depth texture.
if (this.extensions.has('WEBGL_multisampled_render_to_texture') === true && renderTarget.autoAllocateDepthBuffer) {
console.warn('THREE.WebGLBackend: Render-to-texture extension was disabled because an external texture was provided');
}
renderTarget.autoAllocateDepthBuffer = false;
}
}
/**
* Inits a time stamp query for the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
initTimestampQuery(renderContext) {
if (!this.disjoint || !this.trackTimestamp) return;
const type = renderContext.isComputeNode ? 'compute' : 'render';
if (!this.timestampQueryPool[type]) {
// TODO: Variable maxQueries?
this.timestampQueryPool[type] = new WebGLTimestampQueryPool(this.gl, type, 2048);
}
const timestampQueryPool = this.timestampQueryPool[type];
const baseOffset = timestampQueryPool.allocateQueriesForContext(renderContext);
if (baseOffset !== null) {
timestampQueryPool.beginQuery(renderContext);
}
}
// timestamp utils
/**
* Prepares the timestamp buffer.
*
* @param {RenderContext} renderContext - The render context.
*/
prepareTimestampBuffer(renderContext) {
if (!this.disjoint || !this.trackTimestamp) return;
const type = renderContext.isComputeNode ? 'compute' : 'render';
const timestampQueryPool = this.timestampQueryPool[type];
timestampQueryPool.endQuery(renderContext);
}
/**
* Returns the backend's rendering context.
*
* @return {WebGL2RenderingContext} The rendering context.
*/
getContext() {
return this.gl;
}
/**
* This method is executed at the beginning of a render call and prepares
* the WebGL state for upcoming render calls
*
* @param {RenderContext} renderContext - The render context.
*/
beginRender(renderContext) {
const {
state,
gl
} = this;
const renderContextData = this.get(renderContext);
//
if (renderContext.viewport) {
this.updateViewport(renderContext);
} else {
state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
if (renderContext.scissor) {
const {
x,
y,
width,
height
} = renderContext.scissorValue;
state.scissor(x, renderContext.height - height - y, width, height);
}
//
this.initTimestampQuery(renderContext);
renderContextData.previousContext = this._currentContext;
this._currentContext = renderContext;
this._setFramebuffer(renderContext);
this.clear(renderContext.clearColor, renderContext.clearDepth, renderContext.clearStencil, renderContext, false);
const occlusionQueryCount = renderContext.occlusionQueryCount;
if (occlusionQueryCount > 0) {
// Get a reference to the array of objects with queries. The renderContextData property
// can be changed by another render pass before the async reading of all previous queries complete
renderContextData.currentOcclusionQueries = renderContextData.occlusionQueries;
renderContextData.currentOcclusionQueryObjects = renderContextData.occlusionQueryObjects;
renderContextData.lastOcclusionObject = null;
renderContextData.occlusionQueries = new Array(occlusionQueryCount);
renderContextData.occlusionQueryObjects = new Array(occlusionQueryCount);
renderContextData.occlusionQueryIndex = 0;
}
}
/**
* This method is executed at the end of a render call and finalizes work
* after draw calls.
*
* @param {RenderContext} renderContext - The render context.
*/
finishRender(renderContext) {
const {
gl,
state
} = this;
const renderContextData = this.get(renderContext);
const previousContext = renderContextData.previousContext;
state.resetVertexState();
const occlusionQueryCount = renderContext.occlusionQueryCount;
if (occlusionQueryCount > 0) {
if (occlusionQueryCount > renderContextData.occlusionQueryIndex) {
gl.endQuery(gl.ANY_SAMPLES_PASSED);
}
this.resolveOccludedAsync(renderContext);
}
const textures = renderContext.textures;
if (textures !== null) {
for (let i = 0; i < textures.length; i++) {
const texture = textures[i];
if (texture.generateMipmaps) {
this.generateMipmaps(texture);
}
}
}
this._currentContext = previousContext;
if (renderContext.textures !== null && renderContext.renderTarget) {
const renderTargetContextData = this.get(renderContext.renderTarget);
const {
samples
} = renderContext.renderTarget;
if (samples > 0 && this._useMultisampledRTT(renderContext.renderTarget) === false) {
const fb = renderTargetContextData.framebuffers[renderContext.getCacheKey()];
const mask = gl.COLOR_BUFFER_BIT;
const msaaFrameBuffer = renderTargetContextData.msaaFrameBuffer;
const textures = renderContext.textures;
state.bindFramebuffer(gl.READ_FRAMEBUFFER, msaaFrameBuffer);
state.bindFramebuffer(gl.DRAW_FRAMEBUFFER, fb);
for (let i = 0; i < textures.length; i++) {
// TODO Add support for MRT
if (renderContext.scissor) {
const {
x,
y,
width,
height
} = renderContext.scissorValue;
const viewY = renderContext.height - height - y;
gl.blitFramebuffer(x, viewY, x + width, viewY + height, x, viewY, x + width, viewY + height, mask, gl.NEAREST);
if (this._supportsInvalidateFramebuffer === true) {
gl.invalidateSubFramebuffer(gl.READ_FRAMEBUFFER, renderTargetContextData.invalidationArray, x, viewY, width, height);
}
} else {
gl.blitFramebuffer(0, 0, renderContext.width, renderContext.height, 0, 0, renderContext.width, renderContext.height, mask, gl.NEAREST);
if (this._supportsInvalidateFramebuffer === true) {
gl.invalidateFramebuffer(gl.READ_FRAMEBUFFER, renderTargetContextData.invalidationArray);
}
}
}
}
}
if (previousContext !== null) {
this._setFramebuffer(previousContext);
if (previousContext.viewport) {
this.updateViewport(previousContext);
} else {
state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
}
this.prepareTimestampBuffer(renderContext);
}
/**
* This method processes the result of occlusion queries and writes it
* into render context data.
*
* @async
* @param {RenderContext} renderContext - The render context.
*/
resolveOccludedAsync(renderContext) {
const renderContextData = this.get(renderContext);
// handle occlusion query results
const {
currentOcclusionQueries,
currentOcclusionQueryObjects
} = renderContextData;
if (currentOcclusionQueries && currentOcclusionQueryObjects) {
const occluded = new WeakSet();
const {
gl
} = this;
renderContextData.currentOcclusionQueryObjects = null;
renderContextData.currentOcclusionQueries = null;
const check = () => {
let completed = 0;
// check all queries and requeue as appropriate
for (let i = 0; i < currentOcclusionQueries.length; i++) {
const query = currentOcclusionQueries[i];
if (query === null) continue;
if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
if (gl.getQueryParameter(query, gl.QUERY_RESULT) === 0) occluded.add(currentOcclusionQueryObjects[i]);
currentOcclusionQueries[i] = null;
gl.deleteQuery(query);
completed++;
}
}
if (completed < currentOcclusionQueries.length) {
requestAnimationFrame(check);
} else {
renderContextData.occluded = occluded;
}
};
check();
}
}
/**
* Returns `true` if the given 3D object is fully occluded by other
* 3D objects in the scene.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object3D} object - The 3D object to test.
* @return {boolean} Whether the 3D object is fully occluded or not.
*/
isOccluded(renderContext, object) {
const renderContextData = this.get(renderContext);
return renderContextData.occluded && renderContextData.occluded.has(object);
}
/**
* Updates the viewport with the values from the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
updateViewport(renderContext) {
const {
state
} = this;
const {
x,
y,
width,
height
} = renderContext.viewportValue;
state.viewport(x, renderContext.height - height - y, width, height);
}
/**
* Defines the scissor test.
*
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest(boolean) {
const state = this.state;
state.setScissorTest(boolean);
}
/**
* Returns the clear color and alpha into a single
* color object.
*
* @return {Color4} The clear color.
*/
getClearColor() {
const clearColor = super.getClearColor();
// Since the canvas is always created with alpha: true,
// WebGL must always premultiply the clear color.
clearColor.r *= clearColor.a;
clearColor.g *= clearColor.a;
clearColor.b *= clearColor.a;
return clearColor;
}
/**
* Performs a clear operation.
*
* @param {boolean} color - Whether the color buffer should be cleared or not.
* @param {boolean} depth - Whether the depth buffer should be cleared or not.
* @param {boolean} stencil - Whether the stencil buffer should be cleared or not.
* @param {?Object} [descriptor=null] - The render context of the current set render target.
* @param {boolean} [setFrameBuffer=true] - TODO.
*/
clear(color, depth, stencil, descriptor = null, setFrameBuffer = true) {
const {
gl,
renderer
} = this;
if (descriptor === null) {
const clearColor = this.getClearColor();
descriptor = {
textures: null,
clearColorValue: clearColor
};
}
//
let clear = 0;
if (color) clear |= gl.COLOR_BUFFER_BIT;
if (depth) clear |= gl.DEPTH_BUFFER_BIT;
if (stencil) clear |= gl.STENCIL_BUFFER_BIT;
if (clear !== 0) {
let clearColor;
if (descriptor.clearColorValue) {
clearColor = descriptor.clearColorValue;
} else {
clearColor = this.getClearColor();
}
const clearDepth = renderer.getClearDepth();
const clearStencil = renderer.getClearStencil();
if (depth) this.state.setDepthMask(true);
if (descriptor.textures === null) {
gl.clearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a);
gl.clear(clear);
} else {
if (setFrameBuffer) this._setFramebuffer(descriptor);
if (color) {
for (let i = 0; i < descriptor.textures.length; i++) {
if (i === 0) {
gl.clearBufferfv(gl.COLOR, i, [clearColor.r, clearColor.g, clearColor.b, clearColor.a]);
} else {
gl.clearBufferfv(gl.COLOR, i, [0, 0, 0, 1]);
}
}
}
if (depth && stencil) {
gl.clearBufferfi(gl.DEPTH_STENCIL, 0, clearDepth, clearStencil);
} else if (depth) {
gl.clearBufferfv(gl.DEPTH, 0, [clearDepth]);
} else if (stencil) {
gl.clearBufferiv(gl.STENCIL, 0, [clearStencil]);
}
}
}
}
/**
* This method is executed at the beginning of a compute call and
* prepares the state for upcoming compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
beginCompute(computeGroup) {
const {
state,
gl
} = this;
state.bindFramebuffer(gl.FRAMEBUFFER, null);
this.initTimestampQuery(computeGroup);
}
/**
* Executes a compute command for the given compute node.
*
* @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node.
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {ComputePipeline} pipeline - The compute pipeline.
*/
compute(computeGroup, computeNode, bindings, pipeline) {
const {
state,
gl
} = this;
if (this.discard === false) {
// required here to handle async behaviour of render.compute()
gl.enable(gl.RASTERIZER_DISCARD);
this.discard = true;
}
const {
programGPU,
transformBuffers,
attributes
} = this.get(pipeline);
const vaoKey = this._getVaoKey(attributes);
const vaoGPU = this.vaoCache[vaoKey];
if (vaoGPU === undefined) {
this._createVao(attributes);
} else {
state.setVertexState(vaoGPU);
}
state.useProgram(programGPU);
this._bindUniforms(bindings);
const transformFeedbackGPU = this._getTransformFeedback(transformBuffers);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedbackGPU);
gl.beginTransformFeedback(gl.POINTS);
if (attributes[0].isStorageInstancedBufferAttribute) {
gl.drawArraysInstanced(gl.POINTS, 0, 1, computeNode.count);
} else {
gl.drawArrays(gl.POINTS, 0, computeNode.count);
}
gl.endTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
// switch active buffers
for (let i = 0; i < transformBuffers.length; i++) {
const dualAttributeData = transformBuffers[i];
if (dualAttributeData.pbo) {
this.textureUtils.copyBufferToTexture(dualAttributeData.transformBuffer, dualAttributeData.pbo);
}
dualAttributeData.switchBuffers();
}
}
/**
* This method is executed at the end of a compute call and
* finalizes work after compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
finishCompute(computeGroup) {
const gl = this.gl;
this.discard = false;
gl.disable(gl.RASTERIZER_DISCARD);
this.prepareTimestampBuffer(computeGroup);
if (this._currentContext) {
this._setFramebuffer(this._currentContext);
}
}
/**
* Executes a draw command for the given render object.
*
* @param {RenderObject} renderObject - The render object to draw.
* @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process.
*/
draw(renderObject /*, info*/) {
const {
object,
pipeline,
material,
context,
hardwareClippingPlanes
} = renderObject;
const {
programGPU
} = this.get(pipeline);
const {
gl,
state
} = this;
const contextData = this.get(context);
const drawParams = renderObject.getDrawParameters();
if (drawParams === null) return;
//
this._bindUniforms(renderObject.getBindings());
const frontFaceCW = object.isMesh && object.matrixWorld.determinant() < 0;
state.setMaterial(material, frontFaceCW, hardwareClippingPlanes);
state.useProgram(programGPU);
// vertex state
const renderObjectData = this.get(renderObject);
let vaoGPU = renderObjectData.staticVao;
if (vaoGPU === undefined || renderObjectData.geometryId !== renderObject.geometry.id) {
const vaoKey = this._getVaoKey(renderObject.getAttributes());
vaoGPU = this.vaoCache[vaoKey];
if (vaoGPU === undefined) {
let staticVao;
({
vaoGPU,
staticVao
} = this._createVao(renderObject.getAttributes()));
if (staticVao) {
renderObjectData.staticVao = vaoGPU;
renderObjectData.geometryId = renderObject.geometry.id;
}
}
}
const index = renderObject.getIndex();
const indexGPU = index !== null ? this.get(index).bufferGPU : null;
state.setVertexState(vaoGPU, indexGPU);
//
const lastObject = contextData.lastOcclusionObject;
if (lastObject !== object && lastObject !== undefined) {
if (lastObject !== null && lastObject.occlusionTest === true) {
gl.endQuery(gl.ANY_SAMPLES_PASSED);
contextData.occlusionQueryIndex++;
}
if (object.occlusionTest === true) {
const query = gl.createQuery();
gl.beginQuery(gl.ANY_SAMPLES_PASSED, query);
contextData.occlusionQueries[contextData.occlusionQueryIndex] = query;
contextData.occlusionQueryObjects[contextData.occlusionQueryIndex] = object;
}
contextData.lastOcclusionObject = object;
}
//
const renderer = this.bufferRenderer;
if (object.isPoints) renderer.mode = gl.POINTS;else if (object.isLineSegments) renderer.mode = gl.LINES;else if (object.isLine) renderer.mode = gl.LINE_STRIP;else if (object.isLineLoop) renderer.mode = gl.LINE_LOOP;else {
if (material.wireframe === true) {
state.setLineWidth(material.wireframeLinewidth * this.renderer.getPixelRatio());
renderer.mode = gl.LINES;
} else {
renderer.mode = gl.TRIANGLES;
}
}
//
const {
vertexCount,
instanceCount
} = drawParams;
let {
firstVertex
} = drawParams;
renderer.object = object;
if (index !== null) {
firstVertex *= index.array.BYTES_PER_ELEMENT;
const indexData = this.get(index);
renderer.index = index.count;
renderer.type = indexData.type;
} else {
renderer.index = 0;
}
const draw = () => {
if (object.isBatchedMesh) {
if (object._multiDrawInstances !== null) {
// @deprecated, r174
warnOnce('THREE.WebGLBackend: renderMultiDrawInstances has been deprecated and will be removed in r184. Append to renderMultiDraw arguments and use indirection.');
renderer.renderMultiDrawInstances(object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances);
} else if (!this.hasFeature('WEBGL_multi_draw')) {
warnOnce('THREE.WebGLRenderer: WEBGL_multi_draw not supported.');
} else {
renderer.renderMultiDraw(object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount);
}
} else if (instanceCount > 1) {
renderer.renderInstances(firstVertex, vertexCount, instanceCount);
} else {
renderer.render(firstVertex, vertexCount);
}
};
if (renderObject.camera.isArrayCamera && renderObject.camera.cameras.length > 0) {
const cameraData = this.get(renderObject.camera);
const cameras = renderObject.camera.cameras;
const cameraIndex = renderObject.getBindingGroup('cameraIndex').bindings[0];
if (cameraData.indexesGPU === undefined || cameraData.indexesGPU.length !== cameras.length) {
const data = new Uint32Array([0, 0, 0, 0]);
const indexesGPU = [];
for (let i = 0, len = cameras.length; i < len; i++) {
const bufferGPU = gl.createBuffer();
data[0] = i;
gl.bindBuffer(gl.UNIFORM_BUFFER, bufferGPU);
gl.bufferData(gl.UNIFORM_BUFFER, data, gl.STATIC_DRAW);
indexesGPU.push(bufferGPU);
}
cameraData.indexesGPU = indexesGPU; // TODO: Create a global library for this
}
const cameraIndexData = this.get(cameraIndex);
const pixelRatio = this.renderer.getPixelRatio();
for (let i = 0, len = cameras.length; i < len; i++) {
const subCamera = cameras[i];
if (object.layers.test(subCamera.layers)) {
const vp = subCamera.viewport;
const x = vp.x * pixelRatio;
const y = vp.y * pixelRatio;
const width = vp.width * pixelRatio;
const height = vp.height * pixelRatio;
state.viewport(Math.floor(x), Math.floor(renderObject.context.height - height - y), Math.floor(width), Math.floor(height));
state.bindBufferBase(gl.UNIFORM_BUFFER, cameraIndexData.index, cameraData.indexesGPU[i]);
draw();
}
}
} else {
draw();
}
}
/**
* Explain why always null is returned.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render pipeline requires an update or not.
*/
needsRenderUpdate( /*renderObject*/
) {
return false;
}
/**
* Explain why no cache key is computed.
*
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getRenderCacheKey( /*renderObject*/
) {
return '';
}
// textures
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @param {Texture} texture - The texture to create a default texture for.
*/
createDefaultTexture(texture) {
this.textureUtils.createDefaultTexture(texture);
}
/**
* Defines a texture on the GPU for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
createTexture(texture, options) {
this.textureUtils.createTexture(texture, options);
}
/**
* Uploads the updated texture data to the GPU.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture(texture, options) {
this.textureUtils.updateTexture(texture, options);
}
/**
* Generates mipmaps for the given texture.
*
* @param {Texture} texture - The texture.
*/
generateMipmaps(texture) {
this.textureUtils.generateMipmaps(texture);
}
/**
* Destroys the GPU data for the given texture object.
*
* @param {Texture} texture - The texture.
*/
destroyTexture(texture) {
this.textureUtils.destroyTexture(texture);
}
/**
* Returns texture data as a typed array.
*
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The face index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer(texture, x, y, width, height, faceIndex) {
return this.textureUtils.copyTextureToBuffer(texture, x, y, width, height, faceIndex);
}
/**
* This method does nothing since WebGL 2 has no concept of samplers.
*
* @param {Texture} texture - The texture to create the sampler for.
*/
createSampler( /*texture*/
) {
//console.warn( 'Abstract class.' );
}
/**
* This method does nothing since WebGL 2 has no concept of samplers.
*
* @param {Texture} texture - The texture to destroy the sampler for.
*/
destroySampler( /*texture*/) {}
// node builder
/**
* Returns a node builder for the given render object.
*
* @param {RenderObject} object - The render object.
* @param {Renderer} renderer - The renderer.
* @return {GLSLNodeBuilder} The node builder.
*/
createNodeBuilder(object, renderer) {
return new GLSLNodeBuilder(object, renderer);
}
// program
/**
* Creates a shader program from the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
createProgram(program) {
const gl = this.gl;
const {
stage,
code
} = program;
const shader = stage === 'fragment' ? gl.createShader(gl.FRAGMENT_SHADER) : gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(shader, code);
gl.compileShader(shader);
this.set(program, {
shaderGPU: shader
});
}
/**
* Destroys the shader program of the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
destroyProgram(program) {
this.delete(program);
}
/**
* Creates a render pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @param {Array<Promise>} promises - An array of compilation promises which are used in `compileAsync()`.
*/
createRenderPipeline(renderObject, promises) {
const gl = this.gl;
const pipeline = renderObject.pipeline;
// Program
const {
fragmentProgram,
vertexProgram
} = pipeline;
const programGPU = gl.createProgram();
const fragmentShader = this.get(fragmentProgram).shaderGPU;
const vertexShader = this.get(vertexProgram).shaderGPU;
gl.attachShader(programGPU, fragmentShader);
gl.attachShader(programGPU, vertexShader);
gl.linkProgram(programGPU);
this.set(pipeline, {
programGPU,
fragmentShader,
vertexShader
});
if (promises !== null && this.parallel) {
const p = new Promise((resolve /*, reject*/) => {
const parallel = this.parallel;
const checkStatus = () => {
if (gl.getProgramParameter(programGPU, parallel.COMPLETION_STATUS_KHR)) {
this._completeCompile(renderObject, pipeline);
resolve();
} else {
requestAnimationFrame(checkStatus);
}
};
checkStatus();
});
promises.push(p);
return;
}
this._completeCompile(renderObject, pipeline);
}
/**
* Formats the source code of error messages.
*
* @private
* @param {string} string - The code.
* @param {number} errorLine - The error line.
* @return {string} The formatted code.
*/
_handleSource(string, errorLine) {
const lines = string.split('\n');
const lines2 = [];
const from = Math.max(errorLine - 6, 0);
const to = Math.min(errorLine + 6, lines.length);
for (let i = from; i < to; i++) {
const line = i + 1;
lines2.push(`${line === errorLine ? '>' : ' '} ${line}: ${lines[i]}`);
}
return lines2.join('\n');
}
/**
* Gets the shader compilation errors from the info log.
*
* @private
* @param {WebGL2RenderingContext} gl - The rendering context.
* @param {WebGLShader} shader - The WebGL shader object.
* @param {string} type - The shader type.
* @return {string} The shader errors.
*/
_getShaderErrors(gl, shader, type) {
const status = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
const errors = gl.getShaderInfoLog(shader).trim();
if (status && errors === '') return '';
const errorMatches = /ERROR: 0:(\d+)/.exec(errors);
if (errorMatches) {
const errorLine = parseInt(errorMatches[1]);
return type.toUpperCase() + '\n\n' + errors + '\n\n' + this._handleSource(gl.getShaderSource(shader), errorLine);
} else {
return errors;
}
}
/**
* Logs shader compilation errors.
*
* @private
* @param {WebGLProgram} programGPU - The WebGL program.
* @param {WebGLShader} glFragmentShader - The fragment shader as a native WebGL shader object.
* @param {WebGLShader} glVertexShader - The vertex shader as a native WebGL shader object.
*/
_logProgramError(programGPU, glFragmentShader, glVertexShader) {
if (this.renderer.debug.checkShaderErrors) {
const gl = this.gl;
const programLog = gl.getProgramInfoLog(programGPU).trim();
if (gl.getProgramParameter(programGPU, gl.LINK_STATUS) === false) {
if (typeof this.renderer.debug.onShaderError === 'function') {
this.renderer.debug.onShaderError(gl, programGPU, glVertexShader, glFragmentShader);
} else {
// default error reporting
const vertexErrors = this._getShaderErrors(gl, glVertexShader, 'vertex');
const fragmentErrors = this._getShaderErrors(gl, glFragmentShader, 'fragment');
console.error('THREE.WebGLProgram: Shader Error ' + gl.getError() + ' - ' + 'VALIDATE_STATUS ' + gl.getProgramParameter(programGPU, gl.VALIDATE_STATUS) + '\n\n' + 'Program Info Log: ' + programLog + '\n' + vertexErrors + '\n' + fragmentErrors);
}
} else if (programLog !== '') {
console.warn('THREE.WebGLProgram: Program Info Log:', programLog);
}
}
}
/**
* Completes the shader program setup for the given render object.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {RenderPipeline} pipeline - The render pipeline.
*/
_completeCompile(renderObject, pipeline) {
const {
state,
gl
} = this;
const pipelineData = this.get(pipeline);
const {
programGPU,
fragmentShader,
vertexShader
} = pipelineData;
if (gl.getProgramParameter(programGPU, gl.LINK_STATUS) === false) {
this._logProgramError(programGPU, fragmentShader, vertexShader);
}
state.useProgram(programGPU);
// Bindings
const bindings = renderObject.getBindings();
this._setupBindings(bindings, programGPU);
//
this.set(pipeline, {
programGPU
});
}
/**
* Creates a compute pipeline for the given compute node.
*
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
*/
createComputePipeline(computePipeline, bindings) {
const {
state,
gl
} = this;
// Program
const fragmentProgram = {
stage: 'fragment',
code: '#version 300 es\nprecision highp float;\nvoid main() {}'
};
this.createProgram(fragmentProgram);
const {
computeProgram
} = computePipeline;
const programGPU = gl.createProgram();
const fragmentShader = this.get(fragmentProgram).shaderGPU;
const vertexShader = this.get(computeProgram).shaderGPU;
const transforms = computeProgram.transforms;
const transformVaryingNames = [];
const transformAttributeNodes = [];
for (let i = 0; i < transforms.length; i++) {
const transform = transforms[i];
transformVaryingNames.push(transform.varyingName);
transformAttributeNodes.push(transform.attributeNode);
}
gl.attachShader(programGPU, fragmentShader);
gl.attachShader(programGPU, vertexShader);
gl.transformFeedbackVaryings(programGPU, transformVaryingNames, gl.SEPARATE_ATTRIBS);
gl.linkProgram(programGPU);
if (gl.getProgramParameter(programGPU, gl.LINK_STATUS) === false) {
this._logProgramError(programGPU, fragmentShader, vertexShader);
}
state.useProgram(programGPU);
// Bindings
this._setupBindings(bindings, programGPU);
const attributeNodes = computeProgram.attributes;
const attributes = [];
const transformBuffers = [];
for (let i = 0; i < attributeNodes.length; i++) {
const attribute = attributeNodes[i].node.attribute;
attributes.push(attribute);
if (!this.has(attribute)) this.attributeUtils.createAttribute(attribute, gl.ARRAY_BUFFER);
}
for (let i = 0; i < transformAttributeNodes.length; i++) {
const attribute = transformAttributeNodes[i].attribute;
if (!this.has(attribute)) this.attributeUtils.createAttribute(attribute, gl.ARRAY_BUFFER);
const attributeData = this.get(attribute);
transformBuffers.push(attributeData);
}
//
this.set(computePipeline, {
programGPU,
transformBuffers,
attributes
});
}
/**
* Creates bindings from the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {number} cacheIndex - The cache index.
* @param {number} version - The version.
*/
createBindings(bindGroup, bindings /*, cacheIndex, version*/) {
if (this._knownBindings.has(bindings) === false) {
this._knownBindings.add(bindings);
let uniformBuffers = 0;
let textures = 0;
for (const bindGroup of bindings) {
this.set(bindGroup, {
textures: textures,
uniformBuffers: uniformBuffers
});
for (const binding of bindGroup.bindings) {
if (binding.isUniformBuffer) uniformBuffers++;
if (binding.isSampledTexture) textures++;
}
}
}
this.updateBindings(bindGroup, bindings);
}
/**
* Updates the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {number} cacheIndex - The cache index.
* @param {number} version - The version.
*/
updateBindings(bindGroup /*, bindings, cacheIndex, version*/) {
const {
gl
} = this;
const bindGroupData = this.get(bindGroup);
let i = bindGroupData.uniformBuffers;
let t = bindGroupData.textures;
for (const binding of bindGroup.bindings) {
if (binding.isUniformsGroup || binding.isUniformBuffer) {
const data = binding.buffer;
const bufferGPU = gl.createBuffer();
gl.bindBuffer(gl.UNIFORM_BUFFER, bufferGPU);
gl.bufferData(gl.UNIFORM_BUFFER, data, gl.DYNAMIC_DRAW);
this.set(binding, {
index: i++,
bufferGPU
});
} else if (binding.isSampledTexture) {
const {
textureGPU,
glTextureType
} = this.get(binding.texture);
this.set(binding, {
index: t++,
textureGPU,
glTextureType
});
}
}
}
/**
* Updates a buffer binding.
*
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding(binding) {
const gl = this.gl;
if (binding.isUniformsGroup || binding.isUniformBuffer) {
const bindingData = this.get(binding);
const bufferGPU = bindingData.bufferGPU;
const data = binding.buffer;
gl.bindBuffer(gl.UNIFORM_BUFFER, bufferGPU);
gl.bufferData(gl.UNIFORM_BUFFER, data, gl.DYNAMIC_DRAW);
}
}
// attributes
/**
* Creates the GPU buffer of an indexed shader attribute.
*
* @param {BufferAttribute} attribute - The indexed buffer attribute.
*/
createIndexAttribute(attribute) {
const gl = this.gl;
this.attributeUtils.createAttribute(attribute, gl.ELEMENT_ARRAY_BUFFER);
}
/**
* Creates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createAttribute(attribute) {
if (this.has(attribute)) return;
const gl = this.gl;
this.attributeUtils.createAttribute(attribute, gl.ARRAY_BUFFER);
}
/**
* Creates the GPU buffer of a storage attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createStorageAttribute(attribute) {
if (this.has(attribute)) return;
const gl = this.gl;
this.attributeUtils.createAttribute(attribute, gl.ARRAY_BUFFER);
}
/**
* Updates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to update.
*/
updateAttribute(attribute) {
this.attributeUtils.updateAttribute(attribute);
}
/**
* Destroys the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to destroy.
*/
destroyAttribute(attribute) {
this.attributeUtils.destroyAttribute(attribute);
}
/**
* Checks if the given feature is supported by the backend.
*
* @param {string} name - The feature's name.
* @return {boolean} Whether the feature is supported or not.
*/
hasFeature(name) {
const keysMatching = Object.keys(GLFeatureName).filter(key => GLFeatureName[key] === name);
const extensions = this.extensions;
for (let i = 0; i < keysMatching.length; i++) {
if (extensions.has(keysMatching[i])) return true;
}
return false;
}
/**
* Returns the maximum anisotropy texture filtering value.
*
* @return {number} The maximum anisotropy texture filtering value.
*/
getMaxAnisotropy() {
return this.capabilities.getMaxAnisotropy();
}
/**
* Copies data of the given source texture to the given destination texture.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {?(Box3|Box2)} [srcRegion=null] - The region of the source texture to copy.
* @param {?(Vector2|Vector3)} [dstPosition=null] - The destination position of the copy.
* @param {number} [srcLevel=0] - The source mip level to copy from.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture(srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0) {
this.textureUtils.copyTextureToTexture(srcTexture, dstTexture, srcRegion, dstPosition, srcLevel, dstLevel);
}
/**
* Copies the current bound framebuffer to the given texture.
*
* @param {Texture} texture - The destination texture.
* @param {RenderContext} renderContext - The render context.
* @param {Vector4} rectangle - A four dimensional vector defining the origin and dimension of the copy.
*/
copyFramebufferToTexture(texture, renderContext, rectangle) {
this.textureUtils.copyFramebufferToTexture(texture, renderContext, rectangle);
}
/**
* Configures the active framebuffer from the given render context.
*
* @private
* @param {RenderContext} descriptor - The render context.
*/
_setFramebuffer(descriptor) {
const {
gl,
state
} = this;
let currentFrameBuffer = null;
if (descriptor.textures !== null) {
const renderTarget = descriptor.renderTarget;
const renderTargetContextData = this.get(renderTarget);
const {
samples,
depthBuffer,
stencilBuffer
} = renderTarget;
const isCube = renderTarget.isWebGLCubeRenderTarget === true;
const isRenderTarget3D = renderTarget.isRenderTarget3D === true;
const isRenderTargetArray = renderTarget.isRenderTargetArray === true;
const isXRRenderTarget = renderTarget.isXRRenderTarget === true;
const hasExternalTextures = isXRRenderTarget === true && renderTarget.hasExternalTextures === true;
let msaaFb = renderTargetContextData.msaaFrameBuffer;
let depthRenderbuffer = renderTargetContextData.depthRenderbuffer;
const multisampledRTTExt = this.extensions.get('WEBGL_multisampled_render_to_texture');
const useMultisampledRTT = this._useMultisampledRTT(renderTarget);
const cacheKey = getCacheKey(descriptor);
let fb;
if (isCube) {
renderTargetContextData.cubeFramebuffers || (renderTargetContextData.cubeFramebuffers = {});
fb = renderTargetContextData.cubeFramebuffers[cacheKey];
} else if (isXRRenderTarget && hasExternalTextures === false) {
fb = this._xrFramebuffer;
} else {
renderTargetContextData.framebuffers || (renderTargetContextData.framebuffers = {});
fb = renderTargetContextData.framebuffers[cacheKey];
}
if (fb === undefined) {
fb = gl.createFramebuffer();
state.bindFramebuffer(gl.FRAMEBUFFER, fb);
const textures = descriptor.textures;
if (isCube) {
renderTargetContextData.cubeFramebuffers[cacheKey] = fb;
const {
textureGPU
} = this.get(textures[0]);
const cubeFace = this.renderer._activeCubeFace;
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + cubeFace, textureGPU, 0);
} else {
renderTargetContextData.framebuffers[cacheKey] = fb;
for (let i = 0; i < textures.length; i++) {
const texture = textures[i];
const textureData = this.get(texture);
textureData.renderTarget = descriptor.renderTarget;