@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
311 lines (291 loc) • 11.5 kB
JavaScript
import { DepthTexture, LinearFilter, NearestFilter, RGBAFormat, UnsignedByteType, UnsignedShortType, Vector2, WebGLRenderer, WebGLRenderTarget } from 'three';
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import Capabilities from '../core/system/Capabilities';
import RenderingOptions from './RenderingOptions';
import RenderPipeline from './RenderPipeline';
import TextureGenerator from '../utils/TextureGenerator';
import registerChunks from './shader/chunk/registerChunks';
const tmpVec2 = new Vector2();
function createRenderTarget(width, height, type, renderer) {
const result = new WebGLRenderTarget(width, height, {
type,
format: RGBAFormat
});
result.texture.minFilter = TextureGenerator.getCompatibleTextureFilter(LinearFilter, type, renderer);
result.texture.magFilter = NearestFilter;
result.texture.generateMipmaps = false;
result.depthBuffer = true;
result.depthTexture = new DepthTexture(width, height, UnsignedShortType);
return result;
}
/**
* @param options - The options.
* @returns True if the options requires a custom pipeline.
*/
function requiresCustomPipeline(options) {
return options.enableEDL || options.enableInpainting || options.enablePointCloudOcclusion;
}
function createErrorMessage() {
// from Detector.js
const element = document.createElement('div');
element.id = 'webgl-error-message';
element.style.fontFamily = 'monospace';
element.style.fontSize = '13px';
element.style.fontWeight = 'normal';
element.style.textAlign = 'center';
element.style.background = '#fff';
element.style.color = '#000';
element.style.padding = '1.5em';
element.style.width = '400px';
element.style.margin = '5em auto 0';
element.innerHTML = window.WebGLRenderingContext != null ? ['Your graphics card does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br />', 'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.<br>', 'See also <a href="https://www.khronos.org/webgl/wiki/BlacklistsAndWhitelists">graphics card blacklisting</a>'].join('\n') : ['Your browser does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br/>', 'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.<br>', 'You can also try another browser like Firefox or Chrome.'].join('\n');
return element;
}
function createRenderer(target, options) {
// Create renderer
try {
const renderer = new WebGLRenderer({
...options,
canvas: options?.canvas ?? document.createElement('canvas'),
antialias: options?.antialias ?? true,
alpha: options?.alpha ?? true,
logarithmicDepthBuffer: options?.logarithmicDepthBuffer ?? false
});
// Necessary to enable clipping planes per-entity or per-object, rather
// than per-renderer (global) clipping planes.
renderer.localClippingEnabled = true;
return renderer;
} catch (error) {
const msg = 'Failed to create WebGLRenderer';
console.error(msg, error);
target.appendChild(createErrorMessage());
if (error instanceof Error) {
throw new Error(`${msg}: ${error.message}`);
} else {
throw new Error(msg);
}
}
}
class C3DEngine {
_renderTargets = new Map();
clearAlpha = 1;
clearColor = 0x030508;
/**
* @param target - The parent div that will contain the canvas.
* @param options - The options.
*/
constructor(target, options) {
registerChunks();
this.width = target.clientWidth;
this.height = target.clientHeight;
if (options?.renderer instanceof WebGLRenderer) {
this.renderer = options?.renderer;
} else {
this.renderer = createRenderer(target, {
...options?.renderer
});
}
// Don't verify shaders by default (it is very costly)
this.renderer.debug.checkShaderErrors = false;
this.labelRenderer = new CSS2DRenderer();
// Let's allow our canvas to take focus
// The condition below looks weird, but it's correct: querying tabIndex
// returns -1 if not set, but we still need to explicitly set it to force
// the tabindex focus flag to true (see
// https://www.w3.org/TR/html5/editing.html#specially-focusable)
if (this.renderer.domElement.tabIndex === -1) {
this.renderer.domElement.tabIndex = -1;
}
Capabilities.updateCapabilities(this.renderer);
let clearColor = options?.clearColor;
if (clearColor === undefined) {
clearColor = 0x030508;
} else if (clearColor === null) {
this.clearAlpha = 0;
clearColor = 0;
}
this.clearColor = clearColor;
this.renderer.setClearColor(this.clearColor, this.clearAlpha);
this.renderer.clear();
this.renderer.autoClear = false;
// Finalize DOM insertion:
// Ensure display is OK whatever the page layout is
// - By default canvas has `display: inline-block`, which makes it affected by
// its parent's line-height, so it will take more space than it's actual size
// - Setting `display: block` is not enough in flex displays
this.renderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.position = 'absolute';
this.labelRenderer.domElement.style.top = '0';
this.labelRenderer.domElement.style.pointerEvents = 'none';
// Make sure labelRenderer starts a new stacking context, so the labels don't
// stay on top of other components (e.g. inspector, etc.)
this.labelRenderer.domElement.style.zIndex = '0';
// Set default size
this.renderer.setSize(this.width, this.height);
this.labelRenderer.setSize(this.width, this.height);
// Append renderer to the DOM
target.appendChild(this.renderer.domElement);
target.appendChild(this.labelRenderer.domElement);
this._renderPipeline = null;
this.renderingOptions = new RenderingOptions();
}
dispose() {
for (const rt of this._renderTargets.values()) {
rt.dispose();
}
this._renderTargets.clear();
this.labelRenderer.domElement.remove();
this.renderer.domElement.remove();
this.renderer.dispose();
}
onWindowResize(w, h) {
this.width = w;
this.height = h;
for (const rt of this._renderTargets.values()) {
rt.setSize(this.width, this.height);
}
this.renderer.setSize(this.width, this.height);
this.labelRenderer.setSize(this.width, this.height);
}
/**
* Gets the viewport size, in pixels.
*
* @returns The viewport size, in pixels.
*/
getWindowSize(target) {
target = target ?? new Vector2();
return target.set(this.width, this.height);
}
/**
* Renders the scene.
*
* @param scene - The scene to render.
* @param camera - The camera.
*/
render(scene, camera) {
this.renderer.setRenderTarget(null);
const size = this.renderer.getDrawingBufferSize(tmpVec2);
// Rendering into a zero-sized buffer is useless and will lead to WebGL warnings.
if (size.width === 0 || size.height === 0) {
return;
}
this.renderer.setClearColor(this.clearColor, this.clearAlpha);
this.renderer.clear();
if (requiresCustomPipeline(this.renderingOptions)) {
this.renderUsingCustomPipeline(scene, camera);
} else {
this.renderer.render(scene, camera);
}
this.labelRenderer.render(scene, camera);
}
/**
* Use a custom pipeline when post-processing is required.
*
* @param scene - The scene to render.
* @param camera - The camera.
*/
renderUsingCustomPipeline(scene, camera) {
if (!this._renderPipeline) {
this._renderPipeline = new RenderPipeline(this.renderer);
}
this._renderPipeline.render(scene, camera, this.width, this.height, this.renderingOptions);
}
acquireRenderTarget(datatype) {
let renderTarget = this._renderTargets.get(datatype);
if (!renderTarget) {
const newRenderTarget = createRenderTarget(this.width, this.height, datatype, this.renderer);
this._renderTargets.set(datatype, newRenderTarget);
renderTarget = newRenderTarget;
}
return renderTarget;
}
/**
* Renders the scene into a readable buffer.
*
* @param options - Options.
* @returns The buffer. The first pixel in the buffer is the bottom-left pixel.
*/
renderToBuffer(options) {
const zone = options.zone || {
x: 0,
y: 0,
width: this.width,
height: this.height
};
const {
scene,
camera
} = options;
if (options.clearColor != null) {
this.renderer.setClearColor(options.clearColor, 1);
}
const datatype = options.datatype ?? UnsignedByteType;
const renderTarget = this.acquireRenderTarget(datatype);
this.renderToRenderTarget(scene, camera, renderTarget, zone);
// Restore previous value
this.renderer.setClearColor(this.clearColor, this.clearAlpha);
zone.x = Math.max(0, Math.min(zone.x, this.width));
zone.y = Math.max(0, Math.min(zone.y, this.height));
const size = 4 * zone.width * zone.height;
const pixelBuffer = datatype === UnsignedByteType ? new Uint8Array(size) : new Float32Array(size);
this.renderer.readRenderTargetPixels(renderTarget, zone.x, this.height - (zone.y + zone.height), zone.width, zone.height, pixelBuffer);
return pixelBuffer;
}
/**
* Render the scene to a render target.
*
* @param scene - The scene root.
* @param camera - The camera to render.
* @param target - destination render target. Default value: full size
* render target owned by C3DEngine.
* @param zone - partial zone to render (zone x/y uses canvas coordinates)
* Note: target must contain complete zone
* @returns the destination render target
*/
renderToRenderTarget(scene, camera, target, zone) {
if (target == null) {
target = this.acquireRenderTarget(UnsignedByteType);
}
const current = this.renderer.getRenderTarget();
// Don't use setViewport / setScissor on renderer because they would affect
// on screen rendering as well. Instead set them on the render target.
target.viewport.set(0, 0, target.width, target.height);
if (zone != null) {
target.scissor.set(Math.max(0, zone.x), Math.max(target.height - (zone.y + zone.height)), zone.width, zone.height);
target.scissorTest = true;
}
this.renderer.setRenderTarget(target);
this.renderer.clear();
this.renderer.render(scene, camera);
this.renderer.setRenderTarget(current);
target.scissorTest = false;
return target;
}
/**
* Converts the pixel buffer into an image element.
*
* @param pixelBuffer - The 8-bit RGBA buffer.
* @param width - The width of the buffer, in pixels.
* @param height - The height of the buffer, in pixels.
* @returns The image.
*/
static bufferToImage(pixelBuffer, width, height) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('could not acquire 2D rendering context on canvas');
}
// size the canvas to your desired image
canvas.width = width;
canvas.height = height;
const imgData = ctx.getImageData(0, 0, width, height);
imgData.data.set(pixelBuffer);
ctx.putImageData(imgData, 0, 0);
// create a new img object
const image = new Image();
// set the img.src to the canvas data url
image.src = canvas.toDataURL();
return image;
}
}
export default C3DEngine;