ogl
Version:
WebGL Library
400 lines (333 loc) • 14.5 kB
JavaScript
import { Vec3 } from '../math/Vec3.js';
// TODO: Handle context loss https://www.khronos.org/webgl/wiki/HandlingContextLost
// Not automatic - devs to use these methods manually
// gl.colorMask( colorMask, colorMask, colorMask, colorMask );
// gl.clearColor( r, g, b, a );
// gl.stencilMask( stencilMask );
// gl.stencilFunc( stencilFunc, stencilRef, stencilMask );
// gl.stencilOp( stencilFail, stencilZFail, stencilZPass );
// gl.clearStencil( stencil );
const tempVec3 = /* @__PURE__ */ new Vec3();
let ID = 1;
export class Renderer {
constructor({
canvas = document.createElement('canvas'),
width = 300,
height = 150,
dpr = 1,
alpha = false,
depth = true,
stencil = false,
antialias = false,
premultipliedAlpha = false,
preserveDrawingBuffer = false,
powerPreference = 'default',
autoClear = true,
webgl = 2,
} = {}) {
const attributes = { alpha, depth, stencil, antialias, premultipliedAlpha, preserveDrawingBuffer, powerPreference };
this.dpr = dpr;
this.alpha = alpha;
this.color = true;
this.depth = depth;
this.stencil = stencil;
this.premultipliedAlpha = premultipliedAlpha;
this.autoClear = autoClear;
this.id = ID++;
// Attempt WebGL2 unless forced to 1, if not supported fallback to WebGL1
if (webgl === 2) this.gl = canvas.getContext('webgl2', attributes);
this.isWebgl2 = !!this.gl;
if (!this.gl) this.gl = canvas.getContext('webgl', attributes);
if (!this.gl) console.error('unable to create webgl context');
// Attach renderer to gl so that all classes have access to internal state functions
this.gl.renderer = this;
// initialise size values
this.setSize(width, height);
// gl state stores to avoid redundant calls on methods used internally
this.state = {};
this.state.blendFunc = { src: this.gl.ONE, dst: this.gl.ZERO };
this.state.blendEquation = { modeRGB: this.gl.FUNC_ADD };
this.state.cullFace = false;
this.state.frontFace = this.gl.CCW;
this.state.depthMask = true;
this.state.depthFunc = this.gl.LEQUAL;
this.state.premultiplyAlpha = false;
this.state.flipY = false;
this.state.unpackAlignment = 4;
this.state.framebuffer = null;
this.state.viewport = { x: 0, y: 0, width: null, height: null };
this.state.textureUnits = [];
this.state.activeTextureUnit = 0;
this.state.boundBuffer = null;
this.state.uniformLocations = new Map();
this.state.currentProgram = null;
// store requested extensions
this.extensions = {};
// Initialise extra format types
if (this.isWebgl2) {
this.getExtension('EXT_color_buffer_float');
this.getExtension('OES_texture_float_linear');
} else {
this.getExtension('OES_texture_float');
this.getExtension('OES_texture_float_linear');
this.getExtension('OES_texture_half_float');
this.getExtension('OES_texture_half_float_linear');
this.getExtension('OES_element_index_uint');
this.getExtension('OES_standard_derivatives');
this.getExtension('EXT_sRGB');
this.getExtension('WEBGL_depth_texture');
this.getExtension('WEBGL_draw_buffers');
}
this.getExtension('WEBGL_compressed_texture_astc');
this.getExtension('EXT_texture_compression_bptc');
this.getExtension('WEBGL_compressed_texture_s3tc');
this.getExtension('WEBGL_compressed_texture_etc1');
this.getExtension('WEBGL_compressed_texture_pvrtc');
this.getExtension('WEBKIT_WEBGL_compressed_texture_pvrtc');
// Create method aliases using extension (WebGL1) or native if available (WebGL2)
this.vertexAttribDivisor = this.getExtension('ANGLE_instanced_arrays', 'vertexAttribDivisor', 'vertexAttribDivisorANGLE');
this.drawArraysInstanced = this.getExtension('ANGLE_instanced_arrays', 'drawArraysInstanced', 'drawArraysInstancedANGLE');
this.drawElementsInstanced = this.getExtension('ANGLE_instanced_arrays', 'drawElementsInstanced', 'drawElementsInstancedANGLE');
this.createVertexArray = this.getExtension('OES_vertex_array_object', 'createVertexArray', 'createVertexArrayOES');
this.bindVertexArray = this.getExtension('OES_vertex_array_object', 'bindVertexArray', 'bindVertexArrayOES');
this.deleteVertexArray = this.getExtension('OES_vertex_array_object', 'deleteVertexArray', 'deleteVertexArrayOES');
this.drawBuffers = this.getExtension('WEBGL_draw_buffers', 'drawBuffers', 'drawBuffersWEBGL');
// Store device parameters
this.parameters = {};
this.parameters.maxTextureUnits = this.gl.getParameter(this.gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
this.parameters.maxAnisotropy = this.getExtension('EXT_texture_filter_anisotropic')
? this.gl.getParameter(this.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT)
: 0;
}
setSize(width, height) {
this.width = width;
this.height = height;
this.gl.canvas.width = width * this.dpr;
this.gl.canvas.height = height * this.dpr;
if (!this.gl.canvas.style) return;
Object.assign(this.gl.canvas.style, {
width: width + 'px',
height: height + 'px',
});
}
setViewport(width, height, x = 0, y = 0) {
if (this.state.viewport.width === width && this.state.viewport.height === height) return;
this.state.viewport.width = width;
this.state.viewport.height = height;
this.state.viewport.x = x;
this.state.viewport.y = y;
this.gl.viewport(x, y, width, height);
}
setScissor(width, height, x = 0, y = 0) {
this.gl.scissor(x, y, width, height);
}
enable(id) {
if (this.state[id] === true) return;
this.gl.enable(id);
this.state[id] = true;
}
disable(id) {
if (this.state[id] === false) return;
this.gl.disable(id);
this.state[id] = false;
}
setBlendFunc(src, dst, srcAlpha, dstAlpha) {
if (
this.state.blendFunc.src === src &&
this.state.blendFunc.dst === dst &&
this.state.blendFunc.srcAlpha === srcAlpha &&
this.state.blendFunc.dstAlpha === dstAlpha
)
return;
this.state.blendFunc.src = src;
this.state.blendFunc.dst = dst;
this.state.blendFunc.srcAlpha = srcAlpha;
this.state.blendFunc.dstAlpha = dstAlpha;
if (srcAlpha !== undefined) this.gl.blendFuncSeparate(src, dst, srcAlpha, dstAlpha);
else this.gl.blendFunc(src, dst);
}
setBlendEquation(modeRGB, modeAlpha) {
modeRGB = modeRGB || this.gl.FUNC_ADD;
if (this.state.blendEquation.modeRGB === modeRGB && this.state.blendEquation.modeAlpha === modeAlpha) return;
this.state.blendEquation.modeRGB = modeRGB;
this.state.blendEquation.modeAlpha = modeAlpha;
if (modeAlpha !== undefined) this.gl.blendEquationSeparate(modeRGB, modeAlpha);
else this.gl.blendEquation(modeRGB);
}
setCullFace(value) {
if (this.state.cullFace === value) return;
this.state.cullFace = value;
this.gl.cullFace(value);
}
setFrontFace(value) {
if (this.state.frontFace === value) return;
this.state.frontFace = value;
this.gl.frontFace(value);
}
setDepthMask(value) {
if (this.state.depthMask === value) return;
this.state.depthMask = value;
this.gl.depthMask(value);
}
setDepthFunc(value) {
if (this.state.depthFunc === value) return;
this.state.depthFunc = value;
this.gl.depthFunc(value);
}
setStencilMask(value) {
if(this.state.stencilMask === value) return;
this.state.stencilMask = value;
this.gl.stencilMask(value)
}
setStencilFunc(func, ref, mask) {
if((this.state.stencilFunc === func) &&
(this.state.stencilRef === ref) &&
(this.state.stencilFuncMask === mask)
) return;
this.state.stencilFunc = func || this.gl.ALWAYS;
this.state.stencilRef = ref || 0;
this.state.stencilFuncMask = mask || 0;
this.gl.stencilFunc(func || this.gl.ALWAYS, ref || 0, mask || 0);
}
setStencilOp(stencilFail, depthFail, depthPass) {
if(this.state.stencilFail === stencilFail &&
this.state.stencilDepthFail === depthFail &&
this.state.stencilDepthPass === depthPass
) return;
this.state.stencilFail = stencilFail;
this.state.stencilDepthFail = depthFail;
this.state.stencilDepthPass = depthPass;
this.gl.stencilOp(stencilFail, depthFail, depthPass);
}
activeTexture(value) {
if (this.state.activeTextureUnit === value) return;
this.state.activeTextureUnit = value;
this.gl.activeTexture(this.gl.TEXTURE0 + value);
}
bindFramebuffer({ target = this.gl.FRAMEBUFFER, buffer = null } = {}) {
if (this.state.framebuffer === buffer) return;
this.state.framebuffer = buffer;
this.gl.bindFramebuffer(target, buffer);
}
getExtension(extension, webgl2Func, extFunc) {
// if webgl2 function supported, return func bound to gl context
if (webgl2Func && this.gl[webgl2Func]) return this.gl[webgl2Func].bind(this.gl);
// fetch extension once only
if (!this.extensions[extension]) {
this.extensions[extension] = this.gl.getExtension(extension);
}
// return extension if no function requested
if (!webgl2Func) return this.extensions[extension];
// Return null if extension not supported
if (!this.extensions[extension]) return null;
// return extension function, bound to extension
return this.extensions[extension][extFunc].bind(this.extensions[extension]);
}
sortOpaque(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
} else if (a.program.id !== b.program.id) {
return a.program.id - b.program.id;
} else if (a.zDepth !== b.zDepth) {
return a.zDepth - b.zDepth;
} else {
return b.id - a.id;
}
}
sortTransparent(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
}
if (a.zDepth !== b.zDepth) {
return b.zDepth - a.zDepth;
} else {
return b.id - a.id;
}
}
sortUI(a, b) {
if (a.renderOrder !== b.renderOrder) {
return a.renderOrder - b.renderOrder;
} else if (a.program.id !== b.program.id) {
return a.program.id - b.program.id;
} else {
return b.id - a.id;
}
}
getRenderList({ scene, camera, frustumCull, sort }) {
let renderList = [];
if (camera && frustumCull) camera.updateFrustum();
// Get visible
scene.traverse((node) => {
if (!node.visible) return true;
if (!node.draw) return;
if (frustumCull && node.frustumCulled && camera) {
if (!camera.frustumIntersectsMesh(node)) return;
}
renderList.push(node);
});
if (sort) {
const opaque = [];
const transparent = []; // depthTest true
const ui = []; // depthTest false
renderList.forEach((node) => {
// Split into the 3 render groups
if (!node.program.transparent) {
opaque.push(node);
} else if (node.program.depthTest) {
transparent.push(node);
} else {
ui.push(node);
}
node.zDepth = 0;
// Only calculate z-depth if renderOrder unset and depthTest is true
if (node.renderOrder !== 0 || !node.program.depthTest || !camera) return;
// update z-depth
node.worldMatrix.getTranslation(tempVec3);
tempVec3.applyMatrix4(camera.projectionViewMatrix);
node.zDepth = tempVec3.z;
});
opaque.sort(this.sortOpaque);
transparent.sort(this.sortTransparent);
ui.sort(this.sortUI);
renderList = opaque.concat(transparent, ui);
}
return renderList;
}
render({ scene, camera, target = null, update = true, sort = true, frustumCull = true, clear }) {
if (target === null) {
// make sure no render target bound so draws to canvas
this.bindFramebuffer();
this.setViewport(this.width * this.dpr, this.height * this.dpr);
} else {
// bind supplied render target and update viewport
this.bindFramebuffer(target);
this.setViewport(target.width, target.height);
}
if (clear || (this.autoClear && clear !== false)) {
// Ensure depth buffer writing is enabled so it can be cleared
if (this.depth && (!target || target.depth)) {
this.enable(this.gl.DEPTH_TEST);
this.setDepthMask(true);
}
// Same for stencil
if(this.stencil || (!target || target.stencil)) {
this.enable(this.gl.STENCIL_TEST);
this.setStencilMask(0xff)
}
this.gl.clear(
(this.color ? this.gl.COLOR_BUFFER_BIT : 0) |
(this.depth ? this.gl.DEPTH_BUFFER_BIT : 0) |
(this.stencil ? this.gl.STENCIL_BUFFER_BIT : 0)
);
}
// updates all scene graph matrices
if (update) scene.updateMatrixWorld();
// Update camera separately, in case not in scene graph
if (camera) camera.updateMatrixWorld();
// Get render list - entails culling and sorting
const renderList = this.getRenderList({ scene, camera, frustumCull, sort });
renderList.forEach((node) => {
node.draw({ camera });
});
}
}