packages/webgl/src/texture.ts
import type { Nullable } from "@thi.ng/api";
import { isArray } from "@thi.ng/checks/is-array";
import { withoutKeysObj } from "@thi.ng/object-utils/without-keys";
import {
TEX_FORMATS,
TextureFilter,
TextureFormat,
TextureRepeat,
TextureTarget,
TextureType,
type ITexture,
type TextureOpts,
} from "./api/texture.js";
import { isGL2Context } from "./checks.js";
import { error } from "./error.js";
const $bind = (op: "bind" | "unbind") => (textures?: ITexture[]) => {
if (!textures) return;
for (let i = textures.length, tex; i-- > 0; ) {
(tex = textures[i]) && tex[op](i);
}
};
export const bindTextures = $bind("bind");
export const unbindTextures = $bind("unbind");
export class Texture implements ITexture {
gl: WebGLRenderingContext;
tex: WebGLTexture;
target!: TextureTarget;
format!: TextureFormat;
filter!: TextureFilter[];
wrap!: TextureRepeat[];
type!: TextureType;
size!: number[];
constructor(gl: WebGLRenderingContext, opts: Partial<TextureOpts> = {}) {
this.gl = gl;
this.tex = gl.createTexture() || error("error creating WebGL texture");
this.configure({
filter: TextureFilter.NEAREST,
wrap: TextureRepeat.CLAMP,
...opts,
});
}
configure(opts: Partial<TextureOpts> = {}, unbind = true) {
const gl = this.gl;
const target = opts.target || this.target || TextureTarget.TEXTURE_2D;
const format = opts.format || this.format || TextureFormat.RGBA;
const decl = TEX_FORMATS[format];
const type = opts.type || this.type || decl.types[0];
!this.target && (this.target = target);
this.format = format;
this.type = type;
gl.bindTexture(this.target, this.tex);
opts.flip !== undefined &&
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, opts.flip ? 1 : 0);
opts.premultiply !== undefined &&
gl.pixelStorei(
gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL,
opts.premultiply ? 1 : 0
);
this.configureImage(target, opts);
opts.mipmap && gl.generateMipmap(target);
this.configureFilter(target, opts);
this.configureWrap(target, opts);
this.configureLOD(target, opts);
this.configureLevels(target, opts);
unbind && gl.bindTexture(this.target, null);
return true;
}
protected configureImage(
target: TextureTarget,
opts: Partial<TextureOpts>
) {
if (opts.image === undefined) return;
target === TextureTarget.TEXTURE_3D
? this.configureImage3d(target, opts)
: this.configureImage2d(target, opts);
}
protected configureImage2d(
target: TextureTarget,
opts: Partial<TextureOpts>
) {
const level = opts.level || 0;
const pos = opts.pos || [0, 0, 0];
const { image, width, height } = opts;
const decl = TEX_FORMATS[this.format];
const baseFormat = decl.format;
const { gl, type, format } = this;
if (width && height) {
if (opts.sub) {
gl.texSubImage2D(
target,
level,
pos[0],
pos[1],
width,
height,
baseFormat,
type,
<ArrayBufferView>image
);
} else {
if (level === 0) {
this.size = [width, height];
}
gl.texImage2D(
target,
level,
format,
width,
height,
0,
baseFormat,
type,
<ArrayBufferView>image
);
}
} else {
if (opts.sub) {
gl.texSubImage2D(
target,
level,
pos[0],
pos[1],
baseFormat,
type,
<TexImageSource>image
);
} else {
if (image != null && level === 0) {
this.size = [(<any>image).width, (<any>image).height];
}
gl.texImage2D(
target,
level,
format,
baseFormat,
type,
<TexImageSource>image
);
}
}
}
protected configureImage3d(
target: TextureTarget,
opts: Partial<TextureOpts>
) {
const { image, width, height, depth } = opts;
if (!(width && height && depth)) return;
const level = opts.level || 0;
const pos = opts.pos || [0, 0, 0];
const decl = TEX_FORMATS[this.format];
const baseFormat = decl.format;
const { gl, type, format } = this;
if (opts.sub) {
(<WebGL2RenderingContext>gl).texSubImage3D(
target,
level,
pos[0],
pos[1],
pos[2],
width,
height,
depth,
baseFormat,
type,
<any>image
);
} else {
if (level === 0) {
this.size = [width, height, depth];
}
(<WebGL2RenderingContext>gl).texImage3D(
target,
level,
format,
width,
height,
depth,
0,
baseFormat,
type,
<any>image
);
}
}
protected configureFilter(
target: TextureTarget,
opts: Partial<TextureOpts>
) {
const gl = this.gl;
const flt = opts.filter || this.filter || TextureFilter.NEAREST;
let t1: GLenum, t2: GLenum;
if (isArray(flt)) {
t1 = flt[0];
t2 = flt[1] || t1;
this.filter = [t1, t2];
} else {
this.filter = [flt, flt, flt];
t1 = t2 = flt;
}
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, t1);
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, t2);
}
protected configureWrap(target: TextureTarget, opts: Partial<TextureOpts>) {
const gl = this.gl;
const wrap = opts.wrap || this.wrap || TextureRepeat.CLAMP;
let t1: GLenum, t2: GLenum, t3: GLenum;
if (isArray(wrap)) {
t1 = wrap[0];
t2 = wrap[1] || t1;
t3 = wrap[2] || t1;
this.wrap = [t1, t2, t3];
} else {
t1 = t2 = t3 = wrap;
this.wrap = [wrap, wrap, wrap];
}
gl.texParameteri(target, gl.TEXTURE_WRAP_S, t1);
gl.texParameteri(target, gl.TEXTURE_WRAP_T, t2);
isGL2Context(gl) &&
target === (<WebGL2RenderingContext>gl).TEXTURE_3D &&
gl.texParameteri(
target,
(<WebGL2RenderingContext>gl).TEXTURE_WRAP_R,
t3
);
}
protected configureLOD(target: TextureTarget, opts: Partial<TextureOpts>) {
const gl = this.gl;
if (opts.lod) {
const [t1, t2] = opts.lod;
t1 &&
gl.texParameterf(
target,
(<WebGL2RenderingContext>gl).TEXTURE_MIN_LOD,
t1
);
t2 &&
gl.texParameterf(
target,
(<WebGL2RenderingContext>gl).TEXTURE_MAX_LOD,
t2
);
}
}
protected configureLevels(
target: TextureTarget,
opts: Partial<TextureOpts>
) {
const gl = this.gl;
if (opts.minMaxLevel) {
const [t1, t2] = opts.minMaxLevel;
gl.texParameteri(
target,
(<WebGL2RenderingContext>gl).TEXTURE_BASE_LEVEL,
t1
);
gl.texParameteri(
target,
(<WebGL2RenderingContext>gl).TEXTURE_MAX_LEVEL,
t2
);
}
}
bind(id = 0) {
const gl = this.gl;
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(this.target, this.tex);
return true;
}
unbind(id = 0) {
const gl = this.gl;
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(this.target, null);
return true;
}
release() {
if (this.tex) {
this.gl.deleteTexture(this.tex);
delete (<any>this).tex;
delete (<any>this).gl;
return true;
}
return false;
}
}
export const defTexture = (
gl: WebGLRenderingContext,
opts?: Partial<TextureOpts>
) => new Texture(gl, opts);
/**
* Creates cube map texture from given 6 `face` texture sources. The
* given options are shared by each each side/face of the cube map. The
* following options are applied to the cube map directly:
*
* - `filter`
* - `mipmap`
*
* The following options are ignored entirely:
*
* - `target`
* - `image`
*
* @param gl -
* @param faces - in order: +x,-x,+y,-y,+z,-z
* @param opts -
*/
export const defTextureCubeMap = (
gl: WebGLRenderingContext,
faces: (ArrayBufferView | TexImageSource)[],
opts: Partial<TextureOpts> = {}
) => {
const tex = new Texture(gl, { target: gl.TEXTURE_CUBE_MAP });
const faceOpts = withoutKeysObj(opts, [
"target",
"image",
"filter",
"mipmap",
]);
for (let i = 0; i < 6; i++) {
faceOpts.target = gl.TEXTURE_CUBE_MAP_POSITIVE_X + i;
faceOpts.image = faces[i];
tex.configure(faceOpts);
}
tex.configure({ filter: opts.filter, mipmap: opts.mipmap });
return tex;
};
/**
* Creates & configure a new float texture.
*
* **Important:** Since each texel will hold 4x 32-bit float values, the
* `data` buffer needs to have a length of at least `4 * width *
* height`.
*
* Under WebGL 1.0, we assume the caller has previously enabled the
* `OES_texture_float` extension.
*
* @param gl - GL context
* @param data - texture data
* @param width - width
* @param height - height
* @param format -
* @param type -
*/
export const defTextureFloat = (
gl: WebGLRenderingContext,
data: Nullable<Float32Array>,
width: number,
height: number,
format?: TextureFormat,
type?: TextureType
) =>
new Texture(gl, {
filter: gl.NEAREST,
wrap: gl.CLAMP_TO_EDGE,
format:
format ||
(isGL2Context(gl) ? TextureFormat.RGBA32F : TextureFormat.RGBA),
type: type || gl.FLOAT,
image: data,
width,
height,
});