/*
* Copyright 2026 Michael Gevlich
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { BaseFramebuffer } from "./BaseFramebuffer";
import { Clock } from "../Clock";
import { cons } from "../cons";
import { createEvents } from "../Events";
import type { EventsHandler } from "../Events";
import { getUrlParam, isEmpty } from "../utils/shared";
import type { TypedArray } from "../utils/shared";
import { ImageCanvas } from "../ImageCanvas";
import { Vec2 } from "../math/Vec2";
import type { NumberArray2 } from "../math/Vec2";
import { ProgramController } from "./ProgramController";
import { Program } from "./Program";
import { Stack } from "../Stack";
import { throttle } from "../utils/shared";
export type WebGLContextExt = { type: string } & WebGL2RenderingContext;
export type WebGLBufferExt = { numItems: number; itemSize: number } & WebGLBuffer;
export type WebGLTextureExt = { default?: boolean } & WebGLTexture;
export type ImageSource = HTMLCanvasElement | ImageBitmap | ImageData | HTMLImageElement | HTMLVideoElement;
type CreateTextureFunc = (
image: ImageSource,
internalFormat?: number | null,
texParami?: number | null,
texture?: WebGLTextureExt
) => WebGLTextureExt | null;
export interface IHandlerParameters {
anisotropy?: number;
width?: number;
height?: number;
pixelRatio?: number;
context?: {
stencil?: boolean;
alpha?: boolean;
antialias?: boolean;
premultipliedAlpha?: boolean;
preserveDrawingBuffer?: boolean;
};
extensions?: string[];
autoActivate?: boolean;
}
export interface Texture3DParams {
nx: string;
px: string;
py: string;
ny: string;
pz: string;
nz: string;
}
export interface IDefaultTextureParams {
color?: string;
url?: string;
}
const vendorPrefixes = ["", "WEBKIT_", "MOZ_"];
const CONTEXT_TYPE = "webgl2";
// Maximal mipmap levels
const MAX_LEVELS = 2;
/**
* @typedef {"visibilitychange" | "resize"} HandlerEventType
*/
/**
* A WebGL handler for accessing low-level WebGL capabilities.
* @class
* @param {string | HTMLCanvasElement} canvasTarget - Canvas element target.
* or undefined creates hidden canvas and handler becomes hidden.
* @param {Object} [params] - Handler options:
* @param {number} [params.anisotropy] - Anisotropy filter degree. 8 is default.
* @param {number} [params.width] - Hidden handler width. 256 is default.
* @param {number} [params.height] - Hidden handler height. 256 is default.
* @param {Array.<string>} [params.extensions] - Additional WebGL extension list. Available by default: EXT_texture_filter_anisotropic.
*/
class Handler {
protected _throttledDrawFrame: () => void;
/**
* Events.
* @public
* @type {EventsHandler<Array<HandlerEventType>>}
*/
public events: EventsHandler<["visibilitychange", "resize"]>;
/**
* Application default timer.
* @public
* @type {Clock}
*/
public defaultClock: Clock;
/**
* Custom timers.
* @protected
* @type{Clock[]}
*/
protected _clocks: Clock[];
/**
* Draw frame time in milliseconds.
* @public
* @type {number}
*/
public deltaTime: number;
public prevDeltaTime: number;
/**
* WebGL rendering canvas element.
* @public
* @type {HTMLCanvasElement | null}
*/
public canvas: HTMLCanvasElement | null;
/**
* WebGL context.
* @public
* @type {WebGLContextExt | null}
*/
public gl: WebGLContextExt | null;
/**
* Shader program controller list.
* @public
* @type {Record<string, ProgramController>}
*/
public programs: Record<string, ProgramController>;
/**
* Current active shader program controller.
* @public
* @type {ProgramController}
*/
public activeProgram: ProgramController | null;
/**
* Handler parameters.
* @private
* @type {Object}
*/
protected _params: {
anisotropy: number;
width: number;
height: number;
pixelRatio: number;
context: {
stencil?: boolean;
alpha?: boolean;
antialias?: boolean;
premultipliedAlpha?: boolean;
preserveDrawingBuffer?: boolean;
};
extensions: string[];
};
//public _oneByHeight: number;
/**
* Current WebGL extensions. Becomes here after context initialization.
* @public
* @type {Record<string, any>}
*/
public extensions: Record<string, any>;
/**
* HTML Canvas target.
* @private
* @type {string | HTMLCanvasElement | undefined}
*/
protected _canvasTarget: string | HTMLCanvasElement | undefined;
protected _lastAnimationFrameTime: number;
protected _initialized: boolean;
/**
* Animation frame function assigned from outside(Ex. from Renderer).
* @private
* @type {Function}
*/
protected _frameCallback: Function;
protected _canvasSize: NumberArray2;
public transparentTexture: WebGLTextureExt | null;
public defaultTexture: WebGLTextureExt | null;
public framebufferStack = new Stack<BaseFramebuffer>();
public createTexture: Record<string, CreateTextureFunc>;
public createTextureDefault: CreateTextureFunc;
public ONCANVASRESIZE: Function | null;
public intersectionObserver?: IntersectionObserver;
public resizeObserver?: ResizeObserver;
protected _requestAnimationFrameId: number = 0;
protected _clipControlZeroToOne: boolean = false;
public clipControl: ((origin: number, depth: number) => void) | undefined = undefined;
constructor(canvasTarget: string | HTMLCanvasElement | undefined, params: IHandlerParameters = {}) {
this.events = createEvents<["visibilitychange", "resize"]>(["visibilitychange", "resize"]);
this._throttledDrawFrame = this.drawFrame;
this.defaultClock = new Clock();
this._clocks = [];
this.prevDeltaTime = 0;
this.deltaTime = 0;
this.canvas = null;
this.gl = null;
this.programs = {};
this.activeProgram = null;
this._canvasSize = [0, 0];
this._params = {
anisotropy: params.anisotropy || 4,
width: params.width || 256,
height: params.height || 256,
pixelRatio: getUrlParam("og_dpi") || params.pixelRatio || 1.0,
extensions: params.extensions || [],
context: params.context || {}
};
//this._oneByHeight = 1.0 / (this._params.height * this._params.pixelRatio);
this.extensions = {};
this._canvasTarget = canvasTarget;
this._lastAnimationFrameTime = 0;
this._initialized = false;
this._frameCallback = function () {};
this.transparentTexture = null;
this.defaultTexture = null;
this.framebufferStack = new Stack();
this.createTexture = {
NEAREST: this.createTexture_n.bind(this),
LINEAR: this.createTexture_l.bind(this),
MIPMAP: this.createTexture_mm.bind(this),
ANISOTROPIC: this.createTexture_a.bind(this)
};
this.createTextureDefault = this.createTexture.NEAREST;
this.ONCANVASRESIZE = null;
this._createCanvas();
if (params.autoActivate || isEmpty(params.autoActivate)) {
this.initialize();
}
}
public set frameDelay(delay: number) {
if (delay === 0) {
this._throttledDrawFrame = this.drawFrame;
} else {
this._throttledDrawFrame = throttle(this.drawFrame, delay);
}
}
public isInitialized(): boolean {
return this._initialized;
}
protected _createCanvas() {
if (this._canvasTarget) {
if (this._canvasTarget instanceof HTMLElement) {
this.canvas = this._canvasTarget;
} else {
this.canvas = (document.getElementById(this._canvasTarget) ||
document.querySelector(this._canvasTarget)) as HTMLCanvasElement;
}
} else {
this.canvas = document.createElement("canvas");
this.canvas.width = this._params.width;
this.canvas.height = this._params.height;
}
}
/**
* The return value is null if the extension is not supported, or an extension object otherwise.
* @param {WebGL2RenderingContext | null} gl - WebGl context pointer.
* @param {string} name - Extension name.
* @returns {any} -
*/
static getExtension(gl: WebGL2RenderingContext | null, name: string): any | undefined {
if (!gl) return;
for (let i in vendorPrefixes) {
let ext = gl.getExtension(vendorPrefixes[i] + name);
if (ext) {
return ext;
}
}
}
/**
* Returns a drawing context on the canvas, or null if the context identifier is not supported.
* @param {HTMLCanvasElement} canvas - HTML canvas object.
* @param {any} [contextAttributes] - See canvas.getContext contextAttributes.
* @returns {WebGLContextExt | null} -
*/
static getContext(canvas: HTMLCanvasElement, contextAttributes?: any): WebGLContextExt | null {
let ctx: WebGLContextExt | null = null;
try {
ctx = canvas.getContext(CONTEXT_TYPE, contextAttributes) as WebGLContextExt;
if (ctx) {
ctx.type = CONTEXT_TYPE;
}
} catch (ex) {
cons.logErr("exception during the GL context initialization");
}
if (!ctx) {
cons.logErr("could not initialise WebGL");
}
return ctx;
}
/**
* Sets animation frame function.
* @public
* @param {Function} callback - Frame callback.
*/
public setFrameCallback(callback: Function) {
callback && (this._frameCallback = callback);
}
/**
* Creates an empty immutable 2D texture (WebGL2).
* @public
* @param {number} [width=1] - Texture width in pixels.
* @param {number} [height=1] - Texture height in pixels.
* @param {string} [filter="NEAREST"] - GL_TEXTURE_MIN_FILTER and GL_TEXTURE_MAG_FILTER value.
* @param {string} [internalFormat="RGBA8"] - Sized internal format (e.g. "RGBA8", "RGBA16F", "R16F").
* @param {string} [param="CLAMP_TO_EDGE"] - GL_TEXTURE_WRAP_S/T value.
* @param {number} [levels=1] - Number of mipmap levels (immutable storage).
* @returns {WebGLTexture | null} - Created WebGL texture object.
*/
public createEmptyTexture2DExt(
width: number = 1,
height: number = 1,
filter: string = "NEAREST",
internalFormat: string = "RGBA8",
param: string = "CLAMP_TO_EDGE",
levels: number = 1
): WebGLTexture | null {
const gl = this.gl as WebGL2RenderingContext;
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texStorage2D(gl.TEXTURE_2D, levels, (gl as any)[internalFormat.toUpperCase()], width, height);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, (gl as any)[filter.toUpperCase()]);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, (gl as any)[filter.toUpperCase()]);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, (gl as any)[param.toUpperCase()]);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, (gl as any)[param.toUpperCase()]);
gl.bindTexture(gl.TEXTURE_2D, null);
return texture;
}
/**
* Creates Empty NEAREST filtered texture.
* @public
* @param {number} width - Empty texture width.
* @param {number} height - Empty texture height.
* @param {number} [internalFormat] - Internal texture format, `gl.RGBA` by default.
* @param {number} [texParami] - Wrap mode for S/T axes, `gl.CLAMP_TO_EDGE` by default.
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createEmptyTexture_n(
width: number,
height: number,
internalFormat?: number | null,
texParami?: number | null
): WebGLTexture | null {
let gl = this.gl!;
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat || gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null!);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates empty LINEAR filtered texture.
* @public
* @param {number} width - Empty texture width.
* @param {number} height - Empty texture height.
* @param {number} [internalFormat]
* @param {number} [texParami]
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createEmptyTexture_l(
width: number,
height: number,
internalFormat?: number | null,
texParami?: number | null
): WebGLTexture | null {
let gl = this.gl!;
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat || gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null!);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates NEAREST filter texture.
* @public
* @param {ImageSource} image - Image or Canvas object.
* @param {number} [internalFormat]
* @param {number} [texParami]
* @param {WebGLTexture | null} [texture]
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createTexture_n(
image: ImageSource,
internalFormat?: number | null,
texParami?: number | null,
texture: WebGLTexture | null = null
): WebGLTextureExt | null {
let gl = this.gl!;
texture = texture || gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
//gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texStorage2D(gl.TEXTURE_2D, 1, internalFormat || gl.RGBA8, image.width, image.height);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates LINEAR filter texture.
* @public
* @param {ImageSource} image - Image or Canvas object.
* @param {number} [internalFormat]
* @param {number} [texParami]
* @param {WebGLTexture | null} [texture]
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createTexture_l(
image: ImageSource,
internalFormat?: number | null,
texParami?: number | null,
texture: WebGLTexture | null = null
): WebGLTextureExt | null {
let gl = this.gl!;
texture = texture || gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
//gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texStorage2D(gl.TEXTURE_2D, 1, internalFormat || gl.RGBA8, image.width, image.height);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates MIPMAP filter texture.
* @public
* @param {ImageSource} image - Image or Canvas object.
* @param {number} [internalFormat]
* @param {number} [texParami]
* @param {WebGLTexture | null} [texture]
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createTexture_mm(
image: ImageSource,
internalFormat?: number | null,
texParami?: number | null,
texture: WebGLTexture | null = null
): WebGLTextureExt | null {
let gl = this.gl!;
texture = texture || gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texStorage2D(gl.TEXTURE_2D, MAX_LEVELS, internalFormat || gl.RGBA8, image.width, image.height);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates ANISOTROPY filter texture.
* @public
* @param {ImageSource} image - Image or Canvas object.
* @param {number} [internalFormat]
* @param {number} [texParami]
* @param {WebGLTexture | null} [texture]
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public createTexture_a(
image: ImageSource,
internalFormat?: number | null,
texParami?: number | null,
texture: WebGLTexture | null = null
): WebGLTextureExt | null {
let gl = this.gl!;
texture = texture || gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
//gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
gl.texStorage2D(gl.TEXTURE_2D, MAX_LEVELS, internalFormat || gl.RGBA8, image.width, image.height);
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, image.width, image.height, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.texParameterf(
gl.TEXTURE_2D,
this.extensions.EXT_texture_filter_anisotropic.TEXTURE_MAX_ANISOTROPY_EXT,
this._params.anisotropy
);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, texParami || gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, texParami || gl.CLAMP_TO_EDGE);
gl.bindTexture(gl.TEXTURE_2D, null!);
return texture;
}
/**
* Creates cube texture.
* @public
* @param {Texture3DParams} params - Face image urls:
* @param {string} params.px - Positive X or right image url.
* @param {string} params.nx - Negative X or left image url.
* @param {string} params.py - Positive Y or up image url.
* @param {string} params.ny - Negative Y or bottom image url.
* @param {string} params.pz - Positive Z or face image url.
* @param {string} params.nz - Negative Z or back image url.
* @returns {WebGLTexture | null} - WebGL texture object.
*/
public loadCubeMapTexture(params: Texture3DParams): WebGLTextureExt | null {
let gl = this.gl!;
let texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
let faces: [string, number][] = [
[params.px, gl.TEXTURE_CUBE_MAP_POSITIVE_X],
[params.nx, gl.TEXTURE_CUBE_MAP_NEGATIVE_X],
[params.py, gl.TEXTURE_CUBE_MAP_POSITIVE_Y],
[params.ny, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y],
[params.pz, gl.TEXTURE_CUBE_MAP_POSITIVE_Z],
[params.nz, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z]
];
let imageCanvas = new ImageCanvas();
imageCanvas.fillEmpty();
let emptyImage = imageCanvas.getImage();
for (let i = 0; i < faces.length; i++) {
let face = faces[i][1];
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, emptyImage);
}
for (let i = 0; i < faces.length; i++) {
let face = faces[i][1];
let image = new Image();
image.crossOrigin = "";
image.onload = (function (texture: WebGLTexture | null, face: number, image: HTMLImageElement) {
return function () {
if (gl && texture) {
gl.bindTexture(gl.TEXTURE_CUBE_MAP, texture);
//gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.texImage2D(face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
}
};
})(texture, face, image);
image.src = faces[i][0];
}
return texture;
}
/**
* Adds shader program to the handler.
* @public
* @param {Program} program - Shader program.
* @param {boolean} [activate] - If false program will not compile.
* @return {Program} -
*/
public addProgram(program: Program, activate: boolean = false): Program {
if (!this.programs[program.name]) {
let sc = new ProgramController(this, program);
this.programs[program.name] = sc;
this._initProgramController(sc);
if (!activate) {
sc._activated = false;
}
} else {
console.warn(`Shader program: "${program.name}" already exists.`);
}
return program;
}
/**
* Removes shader program from handler.
* @public
* @param {string} name - Shader program name.
*/
public removeProgram(name: string) {
this.programs[name] && this.programs[name].remove();
}
/**
* Adds shader programs to the handler.
* @public
* @param {Array.<Program>} programsArr - Shader program array.
*/
public addPrograms(programsArr: Program[]) {
for (let i = 0; i < programsArr.length; i++) {
this.addProgram(programsArr[i]);
}
}
/**
* Used in addProgram
* @protected
* @param {ProgramController} sc - Program controller
*/
protected _initProgramController(sc: ProgramController) {
if (this._initialized) {
sc.initialize();
if (!this.activeProgram) {
this.activeProgram = sc;
sc.activate();
} else {
sc.deactivate();
this.activeProgram._program.enableAttribArrays();
this.activeProgram._program.use();
}
}
}
/**
* Used in init function.
* @private
*/
protected _initPrograms() {
for (let p in this.programs) {
this._initProgramController(this.programs[p]);
}
}
/**
* Initialize additional WebGL extensions.
* @public
* @param {string} extensionStr - Extension name.
* @param {boolean} showLog - Show logging.
* @return {any} -
*/
public initializeExtension(extensionStr: string, showLog: boolean = false): any {
if (!(this.extensions && this.extensions[extensionStr])) {
let ext = Handler.getExtension(this.gl, extensionStr);
if (ext) {
this.extensions[extensionStr] = ext;
} else if (showLog) {
console.warn("og.webgl.Handler: extension '" + extensionStr + "' doesn't initialize.");
}
}
return this.extensions && this.extensions[extensionStr];
}
/**
* Main function that initialize handler.
* @public
*/
public initialize() {
if (this._initialized) return;
if (!this.canvas) return;
this.gl = Handler.getContext(this.canvas, this._params.context);
if (!this.gl) return;
this._initialized = true;
/** Sets default extensions */
this._params.extensions.push("EXT_texture_filter_anisotropic");
this._params.extensions.push("EXT_color_buffer_float");
this._params.extensions.push("OES_texture_float_linear");
this._params.extensions.push("EXT_clip_control");
//this._params.extensions.push("WEBGL_draw_buffers");
let i = this._params.extensions.length;
while (i--) {
this.initializeExtension(this._params.extensions[i], true);
}
//
// Init clipControl function
//
const clipControlExt = this.extensions.EXT_clip_control;
if (clipControlExt && clipControlExt.clipControlEXT) {
this.clipControl = clipControlExt.clipControlEXT.bind(clipControlExt);
}
if (!this.extensions.EXT_texture_filter_anisotropic) {
this.createTextureDefault = this.createTexture.MIPMAP;
} else {
this.createTextureDefault = this.createTexture.ANISOTROPIC;
}
/** Initializing shaders and rendering parameters*/
this._initPrograms();
this._setDefaults();
this.intersectionObserver = new IntersectionObserver(
(entries) => {
this._toggleVisibilityChange(entries[entries.length - 1].isIntersecting);
},
{ threshold: 0 }
);
this.intersectionObserver.observe(this.canvas);
this.resizeObserver = new ResizeObserver((entries) => {
this._toggleVisibilityChange(entries[0].contentRect.width !== 0 && entries[0].contentRect.height !== 0);
});
this.resizeObserver.observe(this.canvas);
document.addEventListener("visibilitychange", () => {
this._toggleVisibilityChange(document.visibilityState === "visible");
});
}
protected _toggleVisibilityChange(visibility: boolean) {
if (visibility) {
this.start();
this.ONCANVASRESIZE && this.ONCANVASRESIZE();
this.events.dispatch(this.events.visibilitychange, true);
} else {
this.events.dispatch(this.events.visibilitychange, false);
this.stop();
}
}
/**
* Sets default gl render parameters. Used in init function.
* @protected
*/
protected _setDefaults() {
let gl = this.gl;
if (!gl) return;
if (!this.canvas) return;
gl.depthFunc(gl.LESS);
gl.enable(gl.DEPTH_TEST);
this.setSize(this.canvas.clientWidth || this._params.width, this.canvas.clientHeight || this._params.height);
gl.frontFace(gl.CCW);
gl.cullFace(gl.BACK);
gl.enable(gl.CULL_FACE);
gl.disable(gl.BLEND);
this.createDefaultTexture({ color: "rgba(0,0,0,0.0)" }, (t: WebGLTextureExt) => {
this.transparentTexture = t;
});
this.createDefaultTexture({ color: "rgba(255, 255, 255, 1.0)" }, (t: WebGLTextureExt) => {
this.defaultTexture = t;
});
}
/**
* Returns true when clip-control depth range is currently ZERO_TO_ONE.
* @public
* @returns {boolean} -
*/
public get isClipControlZeroToOne(): boolean {
return this._clipControlZeroToOne;
}
/**
* Switches clip-control depth range between ZERO_TO_ONE and NEGATIVE_ONE_TO_ONE.
* If EXT_clip_control is unavailable, the internal ZERO_TO_ONE flag is reset to false.
* @public
* @param {boolean} useZeroToOne - True sets ZERO_TO_ONE, false sets NEGATIVE_ONE_TO_ONE.
*/
public setClipControlZeroToOne(useZeroToOne: boolean) {
if (!this.clipControl) {
this._clipControlZeroToOne = false;
return;
}
const ext = this.extensions.EXT_clip_control!;
if (this._clipControlZeroToOne !== useZeroToOne) {
this.clipControl(ext.LOWER_LEFT_EXT, useZeroToOne ? ext.ZERO_TO_ONE_EXT : ext.NEGATIVE_ONE_TO_ONE_EXT);
this._clipControlZeroToOne = useZeroToOne;
}
}
public getCanvasSize(): NumberArray2 {
return this._canvasSize;
}
/**
* Creates ARRAY_BUFFER storage for frequently updated data.
* @public
* @param {number} itemSize - Number of scalar components per item.
* @param {number} numItems - Number of items.
* @param {number} [usage=STREAM_DRAW] - GL usage hint (STATIC_DRAW, DYNAMIC_DRAW or STREAM_DRAW).
* @param {number} [bytes=4] - Bytes per scalar component.
* @return {WebGLBufferExt} -
*/
public createStreamArrayBuffer(
itemSize: number,
numItems: number,
usage?: number,
bytes: number = 4
): WebGLBufferExt {
let gl = this.gl!;
let buffer: WebGLBufferExt = gl.createBuffer() as WebGLBufferExt;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, numItems * itemSize * bytes, usage || gl.STREAM_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null!);
buffer.itemSize = itemSize;
buffer.numItems = numItems;
return buffer;
}
/**
* Uploads data to an existing ARRAY_BUFFER via bufferSubData.
* @public
* @param {WebGLBufferExt} buffer - Target ARRAY_BUFFER.
* @param {TypedArray} array - Source data to upload.
* @param {number} [offset=0] - Byte offset in the target buffer.
* @return {WebGLBufferExt} -
*/
public setStreamArrayBuffer(buffer: WebGLBufferExt, array: TypedArray, offset: number = 0): WebGLBufferExt {
let gl = this.gl!;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, array);
gl.bindBuffer(gl.ARRAY_BUFFER, null!);
return buffer;
}
/**
* Creates and initializes ARRAY_BUFFER from a typed array.
* @public
* @param {TypedArray} array - Source data.
* @param {number} itemSize - Number of scalar components per item.
* @param {number} [numItems] - Number of items (computed from array length when omitted).
* @param {number} [usage=STATIC_DRAW] - GL usage hint (STATIC_DRAW, DYNAMIC_DRAW or STREAM_DRAW).
* @return {WebGLBufferExt} -
*/
public createArrayBuffer(array: TypedArray, itemSize: number, numItems?: number, usage?: number): WebGLBufferExt {
let gl = this.gl!;
let buffer: WebGLBufferExt = gl.createBuffer() as WebGLBufferExt;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, array, usage || gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null!);
buffer.itemSize = itemSize;
buffer.numItems = numItems || array.length / itemSize;
return buffer;
}
/**
* Creates ARRAY_BUFFER storage with a specific byte length and no initial data.
* @public
* @param {number} size - Buffer size in bytes.
* @param {number} [usage=STATIC_DRAW] - GL usage hint (STATIC_DRAW, DYNAMIC_DRAW or STREAM_DRAW).
* @return {WebGLBufferExt} -
*/
public createArrayBufferLength(size: number, usage?: number): WebGLBufferExt {
let gl = this.gl!;
let buffer: WebGLBufferExt = gl.createBuffer() as WebGLBufferExt;
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, size, usage || gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null!);
buffer.itemSize = 1;
buffer.numItems = size;
return buffer;
}
/**
* Creates ELEMENT ARRAY buffer.
* @public
* @param {TypedArray} array - Input array.
* @param {number} itemSize - Array item size.
* @param {number} numItems - Items quantity.
* @param {number} [usage=STATIC_DRAW] - Parameter of the bufferData call can be one of STATIC_DRAW, DYNAMIC_DRAW, or STREAM_DRAW.
* @return {Object} -
*/
public createElementArrayBuffer(
array: TypedArray,
itemSize: number,
numItems?: number,
usage?: number
): WebGLBufferExt {
let gl = this.gl!;
let buffer = gl.createBuffer() as WebGLBufferExt;
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, array, usage || gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null!);
buffer.itemSize = itemSize;
buffer.numItems = numItems || array.length;
return buffer;
}
/**
* Sets handler canvas size.
* @public
* @param {number} w - Canvas width.
* @param {number} h - Canvas height.
*/
public setSize(w: number, h: number) {
this._params.width = w;
this._params.height = h;
if (this.canvas) {
this.canvas.width = w * this._params.pixelRatio;
this.canvas.height = h * this._params.pixelRatio;
this._canvasSize[0] = this.canvas.width;
this._canvasSize[1] = this.canvas.height;
//this._oneByHeight = 1.0 / this.canvas.height;
this.gl && this.gl.viewport(0, 0, w, h);
this.ONCANVASRESIZE && this.ONCANVASRESIZE(this.canvas);
this.events.dispatch(this.events.resize, this);
}
}
public get pixelRatio(): number {
return this._params.pixelRatio;
}
public set pixelRatio(pr: number) {
this._params.pixelRatio = pr;
this.setSize(this._params.width, this._params.height);
}
/**
* Returns context screen width.
* @public
* @returns {number} -
*/
public getWidth(): number {
return this.canvas ? this.canvas.width : 0;
}
/**
* Returns context screen height.
* @public
* @returns {number} -
*/
public getHeight(): number {
return this.canvas ? this.canvas.height : 0;
}
/**
* Returns canvas aspect ratio.
* @public
* @returns {number} -
*/
public getClientAspect(): number {
return this.canvas ? this.canvas.clientWidth / this.canvas.clientHeight : 0;
}
/**
* Returns canvas center coordinates.
* @public
* @returns {number} -
*/
public getCenter(): Vec2 {
let c = this.canvas;
return c ? new Vec2(Math.round(c.width * 0.5), Math.round(c.height * 0.5)) : new Vec2();
}
/**
* Draw single frame.
* @public
*/
public drawFrame = () => {
/** Calculating frame time */
let now = window.performance.now();
let prevDeltaTime = this.deltaTime;
//Make some filter:)
this.deltaTime = (now - this._lastAnimationFrameTime + this.prevDeltaTime) * 0.5;
if (this.deltaTime > 3) {
this.deltaTime = 3;
} else if (this.deltaTime < 1) {
this.deltaTime = 1;
}
this.prevDeltaTime = prevDeltaTime;
this._lastAnimationFrameTime = now;
this.defaultClock.tick(this.deltaTime);
for (let i = 0; i < this._clocks.length; i++) {
this._clocks[i].tick(this.deltaTime);
}
/** Canvas resize checking */
let canvas = this.canvas!;
if (
Math.floor(canvas.clientWidth * this._params.pixelRatio) !== canvas.width ||
Math.floor(canvas.clientHeight * this._params.pixelRatio) !== canvas.height
) {
if (canvas.clientWidth === 0 || canvas.clientHeight === 0) {
this.stop();
} else if (!document.hidden) {
this.start();
this.setSize(canvas.clientWidth, canvas.clientHeight);
}
}
/** Draw frame */
this._frameCallback();
};
/**
* Clearing gl frame.
* @public
*/
public clearFrame() {
let gl = this.gl!;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
}
/**
* Starts animation loop.
* @public
*/
public start() {
if (!this._requestAnimationFrameId && this._initialized) {
this._animationFrameCallback();
}
}
public stop() {
if (this._requestAnimationFrameId) {
window.cancelAnimationFrame(this._requestAnimationFrameId);
this._requestAnimationFrameId = 0;
}
}
public isStopped(): boolean {
return !this._requestAnimationFrameId;
}
/**
* Check is gl context type equals webgl2
* @public
*/
public isWebGl2(): boolean {
return this.gl ? this.gl.type === "webgl2" : false;
}
/**
* Make animation.
* @protected
*/
protected _animationFrameCallback() {
this._requestAnimationFrameId = window.requestAnimationFrame(() => {
this._throttledDrawFrame();
this._requestAnimationFrameId && this._animationFrameCallback();
});
}
/**
* Creates a default 2x2 texture and passes it to callback.
* If `params.color` is set, a solid color texture is created.
* If `params.url` is set, the image is loaded asynchronously.
* Otherwise a fallback gray texture is created.
* @public
* @param {IDefaultTextureParams | null} params - Texture source parameters.
* @param {function(WebGLTextureExt): void} success - Callback with created texture.
*/
public createDefaultTexture(params: IDefaultTextureParams | null, success: (texture: WebGLTextureExt) => void) {
let imgCnv;
let texture;
if (params && params.color) {
imgCnv = new ImageCanvas(2, 2);
imgCnv.fillColor(params.color);
texture = this.createTexture_n(imgCnv.getCanvas())!;
texture.default = true;
success(texture);
} else if (params && params.url) {
let img = new Image();
let that = this;
img.onload = function () {
texture = that.createTextureDefault(img)!;
texture.default = true;
success(texture);
};
img.src = params.url;
} else {
imgCnv = new ImageCanvas(2, 2);
imgCnv.fillColor("#C5C5C5");
texture = this.createTexture_n(imgCnv.getCanvas())!;
texture.default = true;
success(texture);
}
}
/**
* Deletes texture if it is not marked as default.
* @public
* @param {WebGLTextureExt | null | undefined} texture - Texture to delete.
*/
public deleteTexture(texture: WebGLTextureExt | null | undefined) {
if (texture && !texture.default && this.gl) {
this.gl.deleteTexture(texture);
}
}
/**
* Releases handler resources, WebGL objects, observers and canvas.
* @public
*/
public destroy() {
this.resizeObserver?.disconnect();
this.intersectionObserver?.disconnect();
this.stop();
//
// Dispose shaders
//
for (let p in this.programs) {
this.removeProgram(p);
}
//
// Clear WebGL context
//
let gl = this.gl;
if (gl) {
gl.deleteTexture(this.transparentTexture as WebGLTexture);
this.transparentTexture = null;
gl.deleteTexture(this.defaultTexture as WebGLTexture);
this.defaultTexture = null;
this.framebufferStack = new Stack();
//
// Clear attrib pointers
//
let numAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
let tmp = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, tmp);
for (let ii = 0; ii < numAttribs; ++ii) {
gl.disableVertexAttribArray(ii);
gl.vertexAttribPointer(ii, 4, gl.FLOAT, false, 0, 0);
gl.vertexAttrib1f(ii, 0);
}
gl.deleteBuffer(tmp);
//
// Clear all possible textures
//
let numTextureUnits = gl.getParameter(gl.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!);
}
//
// Hard reset
//
gl.activeTexture(gl.TEXTURE0);
gl.useProgram(null!);
gl.bindBuffer(gl.ARRAY_BUFFER, null!);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null!);
gl.bindFramebuffer(gl.FRAMEBUFFER, null!);
gl.bindRenderbuffer(gl.RENDERBUFFER, null!);
gl.disable(gl.BLEND);
gl.disable(gl.CULL_FACE);
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.DITHER);
gl.disable(gl.SCISSOR_TEST);
gl.blendColor(0, 0, 0, 0);
gl.blendEquation(gl.FUNC_ADD);
gl.blendFunc(gl.ONE, gl.ZERO);
gl.clearColor(0, 0, 0, 0);
gl.clearDepth(1);
gl.clearStencil(-1);
}
//
// Destroy canvas
//
if (this.canvas) {
if (this.canvas.parentNode) {
this.canvas.parentNode.removeChild(this.canvas);
}
this.canvas.width = 1;
this.canvas.height = 1;
this.canvas = null;
}
this.gl = null;
this._initialized = false;
}
public addClock(clock: Clock) {
if (!clock.__handler) {
clock.__handler = this;
this._clocks.push(clock);
}
}
public addClocks(clockArr: Clock[]) {
for (let i = 0; i < clockArr.length; i++) {
this.addClock(clockArr[i]);
}
}
public removeClock(clock: Clock) {
if (clock.__handler) {
let c = this._clocks;
let i = c.length;
while (i--) {
if (c[i].isEqual(clock)) {
clock.__handler = null;
c.splice(i, 1);
break;
}
}
}
}
// var loadTextureData = function(textureName, callback) {
// const xhr = new XMLHttpRequest();
// xhr.open('GET', textureName);
// xhr.responseType = 'arraybuffer';
// xhr.onload = (event) => {
// const data = new DataView(xhr.response);
// const array =
// new Float32Array(data.byteLength / Float32Array.BYTES_PER_ELEMENT);
// for (let i = 0; i < array.length; ++i) {
// array[i] = data.getFloat32(i * Float32Array.BYTES_PER_ELEMENT, true);
// }
// callback(array);
// };
// xhr.send();
// }
// loadTextureData('transmittance.dat', (data) => {
//
// let gl = this.renderer.handler.gl;
//
// const texture = gl.createTexture();
// gl.activeTexture(gl.TEXTURE0);
// gl.bindTexture(gl.TEXTURE_2D, texture);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// gl.texImage2D(gl.TEXTURE_2D, 0, gl.getExtension('OES_texture_float_linear') ? gl.RGBA32F : gl.RGBA16F,
// TRANSMITTANCE_TEXTURE_WIDTH, TRANSMITTANCE_TEXTURE_HEIGHT, 0, gl.RGBA,
// gl.FLOAT, data);
//
// this.transmittanceTextureBrn = texture;
// });
}
export { Handler };