molstar
Version:
A comprehensive macromolecular library.
558 lines (557 loc) • 20.6 kB
JavaScript
/**
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ValueCell } from '../../mol-util';
import { idFactory } from '../../mol-util/id-factory';
import { isWebGL2 } from './compat';
import { isPromiseLike } from '../../mol-util/type-helpers';
import { objectForEach } from '../../mol-util/object';
import { isPowerOfTwo } from '../../mol-math/misc';
const getNextTextureId = idFactory();
export function getTarget(gl, kind) {
switch (kind) {
case 'image-uint8': return gl.TEXTURE_2D;
case 'image-float32': return gl.TEXTURE_2D;
case 'image-float16': return gl.TEXTURE_2D;
case 'image-depth': return gl.TEXTURE_2D;
}
if (isWebGL2(gl)) {
switch (kind) {
case 'image-int32': return gl.TEXTURE_2D;
case 'volume-uint8': return gl.TEXTURE_3D;
case 'volume-float32': return gl.TEXTURE_3D;
case 'volume-float16': return gl.TEXTURE_3D;
}
}
throw new Error(`unknown texture kind '${kind}'`);
}
export function getFormat(gl, format, type) {
switch (format) {
case 'alpha':
if (isWebGL2(gl) && (type === 'float' || type === 'fp16'))
return gl.RED;
else if (isWebGL2(gl) && type === 'int')
return gl.RED_INTEGER;
else
return gl.ALPHA;
case 'rgb':
if (isWebGL2(gl) && type === 'int')
return gl.RGB_INTEGER;
return gl.RGB;
case 'rg':
if (isWebGL2(gl) && (type === 'float' || type === 'fp16'))
return gl.RG;
else if (isWebGL2(gl) && type === 'int')
return gl.RG_INTEGER;
else
throw new Error('texture format "rg" requires webgl2 and type "float" or int"');
case 'rgba':
if (isWebGL2(gl) && type === 'int')
return gl.RGBA_INTEGER;
return gl.RGBA;
case 'depth': return gl.DEPTH_COMPONENT;
}
}
export function getInternalFormat(gl, format, type) {
if (isWebGL2(gl)) {
switch (format) {
case 'alpha':
switch (type) {
case 'ubyte': return gl.ALPHA;
case 'float': return gl.R32F;
case 'fp16': return gl.R16F;
case 'int': return gl.R32I;
}
case 'rg':
switch (type) {
case 'ubyte': return gl.RG;
case 'float': return gl.RG32F;
case 'fp16': return gl.RG16F;
case 'int': return gl.RG32I;
}
case 'rgb':
switch (type) {
case 'ubyte': return gl.RGB;
case 'float': return gl.RGB32F;
case 'fp16': return gl.RGB16F;
case 'int': return gl.RGB32I;
}
case 'rgba':
switch (type) {
case 'ubyte': return gl.RGBA;
case 'float': return gl.RGBA32F;
case 'fp16': return gl.RGBA16F;
case 'int': return gl.RGBA32I;
}
case 'depth':
switch (type) {
case 'ushort': return gl.DEPTH_COMPONENT16;
case 'float': return gl.DEPTH_COMPONENT32F;
}
}
}
return getFormat(gl, format, type);
}
function getByteCount(format, type, width, height, depth) {
const bpe = getFormatSize(format) * getTypeSize(type);
return bpe * width * height * (depth || 1);
}
function getFormatSize(format) {
switch (format) {
case 'alpha': return 1;
case 'rg': return 2;
case 'rgb': return 3;
case 'rgba': return 4;
case 'depth': return 4;
}
}
function getTypeSize(type) {
switch (type) {
case 'ubyte': return 1;
case 'ushort': return 2;
case 'float': return 4;
case 'fp16': return 2;
case 'int': return 4;
}
}
export function getType(gl, extensions, type) {
switch (type) {
case 'ubyte': return gl.UNSIGNED_BYTE;
case 'ushort': return gl.UNSIGNED_SHORT;
case 'float': return gl.FLOAT;
case 'fp16':
if (extensions.textureHalfFloat)
return extensions.textureHalfFloat.HALF_FLOAT;
else
throw new Error('extension "texture_half_float" unavailable');
case 'int':
if (isWebGL2(gl))
return gl.INT;
else
throw new Error('texture type "int" requires webgl2');
}
}
export function getFilter(gl, type) {
switch (type) {
case 'nearest': return gl.NEAREST;
case 'linear': return gl.LINEAR;
}
}
export function getAttachment(gl, extensions, attachment) {
switch (attachment) {
case 'depth': return gl.DEPTH_ATTACHMENT;
case 'stencil': return gl.STENCIL_ATTACHMENT;
case 'color0':
case 0: return gl.COLOR_ATTACHMENT0;
}
if (extensions.drawBuffers) {
switch (attachment) {
case 'color1':
case 1: return extensions.drawBuffers.COLOR_ATTACHMENT1;
case 'color2':
case 2: return extensions.drawBuffers.COLOR_ATTACHMENT2;
case 'color3':
case 3: return extensions.drawBuffers.COLOR_ATTACHMENT3;
case 'color4':
case 4: return extensions.drawBuffers.COLOR_ATTACHMENT4;
case 'color5':
case 5: return extensions.drawBuffers.COLOR_ATTACHMENT5;
case 'color6':
case 6: return extensions.drawBuffers.COLOR_ATTACHMENT6;
case 'color7':
case 7: return extensions.drawBuffers.COLOR_ATTACHMENT7;
}
}
throw new Error('unknown texture attachment');
}
function isImage(x) {
return typeof HTMLImageElement !== 'undefined' && (x instanceof HTMLImageElement);
}
function isTexture2d(x, target, gl) {
return target === gl.TEXTURE_2D;
}
function isTexture3d(x, target, gl) {
return target === gl.TEXTURE_3D;
}
function getTexture(gl) {
const texture = gl.createTexture();
if (texture === null) {
throw new Error('Could not create WebGL texture');
}
return texture;
}
export function createTexture(gl, extensions, kind, _format, _type, _filter) {
const id = getNextTextureId();
let texture = getTexture(gl);
// check texture kind and type compatability
if ((kind.endsWith('float32') && _type !== 'float') ||
(kind.endsWith('float16') && _type !== 'fp16') ||
(kind.endsWith('uint8') && _type !== 'ubyte') ||
(kind.endsWith('int32') && _type !== 'int') ||
(kind.endsWith('depth') && _type !== 'ushort' && _type !== 'float')) {
throw new Error(`texture kind '${kind}' and type '${_type}' are incompatible`);
}
if (!extensions.depthTexture && _format === 'depth') {
throw new Error(`extension 'WEBGL_depth_texture' needed for 'depth' texture format`);
}
const target = getTarget(gl, kind);
const filter = getFilter(gl, _filter);
const format = getFormat(gl, _format, _type);
const internalFormat = getInternalFormat(gl, _format, _type);
const type = getType(gl, extensions, _type);
function init() {
gl.bindTexture(target, texture);
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter);
// clamp-to-edge needed for non-power-of-two textures in webgl
gl.texParameteri(target, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(target, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.bindTexture(target, null);
}
init();
let width = 0, height = 0, depth = 0;
let loadedData;
let hasMipmap = false;
let destroyed = false;
function define(_width, _height, _depth) {
if (_width === 0 || _height === 0 || (isWebGL2(gl) && target === gl.TEXTURE_3D && _depth === 0)) {
throw new Error('empty textures are not allowed');
}
if (width === _width && height === _height && depth === (_depth || 0))
return;
width = _width, height = _height, depth = _depth || 0;
gl.bindTexture(target, texture);
if (target === gl.TEXTURE_2D) {
gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null);
}
else if (isWebGL2(gl) && target === gl.TEXTURE_3D && depth !== undefined) {
gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null);
}
else {
throw new Error('unknown texture target');
}
}
define(1, 1, isWebGL2(gl) && target === gl.TEXTURE_3D ? 1 : 0);
function load(data, sub = false) {
if (data.width === 0 || data.height === 0 || (!isImage(data) && isWebGL2(gl) && isTexture3d(data, target, gl) && data.depth === 0)) {
throw new Error('empty textures are not allowed');
}
gl.bindTexture(target, texture);
// unpack alignment of 1 since we use textures only for data
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
if (isImage(data)) {
width = data.width, height = data.height;
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, data);
}
else if (isTexture2d(data, target, gl)) {
const _filter = data.filter ? getFilter(gl, data.filter) : filter;
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, _filter);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, _filter);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY);
if (sub) {
gl.texSubImage2D(target, 0, 0, 0, data.width, data.height, format, type, data.array);
}
else {
width = data.width, height = data.height;
gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, data.array);
}
}
else if (isWebGL2(gl) && isTexture3d(data, target, gl)) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
if (sub) {
gl.texSubImage3D(target, 0, 0, 0, 0, data.width, data.height, data.depth, format, type, data.array);
}
else {
width = data.width, height = data.height, depth = data.depth;
gl.texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, data.array);
}
}
else {
throw new Error('unknown texture target');
}
gl.bindTexture(target, null);
loadedData = data;
}
function mipmap() {
if (target !== gl.TEXTURE_2D) {
throw new Error('mipmap only supported for 2d textures');
}
if (isWebGL2(gl) || (isPowerOfTwo(width) && isPowerOfTwo(height))) {
gl.bindTexture(target, texture);
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.generateMipmap(target);
gl.bindTexture(target, null);
hasMipmap = true;
}
else {
throw new Error('mipmap unsupported for non-power-of-two textures and webgl1');
}
}
function attachFramebuffer(framebuffer, attachment, layer) {
framebuffer.bind();
if (target === gl.TEXTURE_2D) {
gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), gl.TEXTURE_2D, texture, 0);
}
else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
if (layer === undefined)
throw new Error('need `layer` to attach 3D texture');
gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), texture, 0, layer);
}
else {
throw new Error('unknown/unsupported texture target');
}
}
return {
id,
target,
format,
internalFormat,
type,
filter,
getWidth: () => width,
getHeight: () => height,
getDepth: () => depth,
getByteCount: () => getByteCount(_format, _type, width, height, depth),
define,
load,
mipmap,
bind: (id) => {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, texture);
},
unbind: (id) => {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, null);
},
attachFramebuffer,
detachFramebuffer: (framebuffer, attachment) => {
framebuffer.bind();
if (target === gl.TEXTURE_2D) {
gl.framebufferTexture2D(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), gl.TEXTURE_2D, null, 0);
}
else if (isWebGL2(gl) && target === gl.TEXTURE_3D) {
gl.framebufferTextureLayer(gl.FRAMEBUFFER, getAttachment(gl, extensions, attachment), null, 0, 0);
}
else {
throw new Error('unknown texture target');
}
},
reset: () => {
texture = getTexture(gl);
init();
const [_width, _height, _depth] = [width, height, depth];
width = 0, height = 0, depth = 0; // set to zero to trigger resize
define(_width, _height, _depth);
if (loadedData)
load(loadedData);
if (hasMipmap)
mipmap();
},
destroy: () => {
if (destroyed)
return;
gl.deleteTexture(texture);
destroyed = true;
}
};
}
export function createTextures(ctx, schema, values) {
const { resources } = ctx;
const textures = [];
Object.keys(schema).forEach(k => {
const spec = schema[k];
if (spec.type === 'texture') {
const value = values[k];
if (value) {
if (spec.kind === 'texture') {
textures[textures.length] = [k, value.ref.value];
}
else {
const texture = resources.texture(spec.kind, spec.format, spec.dataType, spec.filter);
texture.load(value.ref.value);
textures[textures.length] = [k, texture];
}
}
}
});
return textures;
}
/**
* Loads an image from a url to a textures and triggers update asynchronously.
* This will not work on node.js without a polyfill for `HTMLImageElement`.
*/
export function loadImageTexture(src, cell, texture) {
const img = new Image();
img.onload = function () {
texture.load(img);
ValueCell.update(cell, texture);
};
img.src = src;
}
export function getCubeTarget(gl, side) {
switch (side) {
case 'nx': return gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
case 'ny': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
case 'nz': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
case 'px': return gl.TEXTURE_CUBE_MAP_POSITIVE_X;
case 'py': return gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
case 'pz': return gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
}
}
export function createCubeTexture(gl, faces, mipmaps, onload) {
const target = gl.TEXTURE_CUBE_MAP;
const filter = gl.LINEAR;
const internalFormat = gl.RGBA;
const format = gl.RGBA;
const type = gl.UNSIGNED_BYTE;
let size = 0;
let texture = gl.createTexture();
gl.bindTexture(target, texture);
function load(cubeTarget, level, image, isReset) {
if (size === 0)
size = image.width;
gl.bindTexture(target, texture);
gl.texImage2D(cubeTarget, level, internalFormat, size, size, 0, format, type, null);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
gl.bindTexture(target, texture);
gl.texImage2D(cubeTarget, level, internalFormat, format, type, image);
loadedCount += 1;
if (loadedCount === 6) {
if (!destroyed) {
if (mipmaps) {
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.generateMipmap(target);
}
else {
gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter);
}
gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter);
}
if (!isReset)
onload === null || onload === void 0 ? void 0 : onload(destroyed);
}
}
const facesData = [];
let loadedCount = 0;
objectForEach(faces, (source, side) => {
if (!source)
return;
const level = 0;
const cubeTarget = getCubeTarget(gl, side);
const image = new Image();
if (source instanceof File) {
image.src = URL.createObjectURL(source);
}
else if (isPromiseLike(source)) {
source.then(blob => {
image.src = URL.createObjectURL(blob);
});
}
else {
image.src = source;
}
facesData.push({ cubeTarget, level, image });
image.addEventListener('load', () => {
load(cubeTarget, level, image, false);
});
image.addEventListener('error', () => {
onload === null || onload === void 0 ? void 0 : onload(true);
});
});
let destroyed = false;
return {
id: getNextTextureId(),
target,
format,
internalFormat,
type,
filter,
getWidth: () => size,
getHeight: () => size,
getDepth: () => 0,
getByteCount: () => {
return getByteCount('rgba', 'ubyte', size, size, 0) * 6 * (mipmaps ? 2 : 1);
},
define: () => { },
load: () => { },
mipmap: () => { },
bind: (id) => {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, texture);
},
unbind: (id) => {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, null);
},
attachFramebuffer: () => { },
detachFramebuffer: () => { },
reset: () => {
texture = getTexture(gl);
gl.bindTexture(target, texture);
loadedCount = 0;
for (const { cubeTarget, level, image } of facesData) {
load(cubeTarget, level, image, true);
}
},
destroy: () => {
if (destroyed)
return;
gl.deleteTexture(texture);
destroyed = true;
},
};
}
//
const NullTextureFormat = -1;
export function isNullTexture(texture) {
return texture.format === NullTextureFormat;
}
export function createNullTexture(gl) {
var _a;
const target = (_a = gl === null || gl === void 0 ? void 0 : gl.TEXTURE_2D) !== null && _a !== void 0 ? _a : 3553;
return {
id: getNextTextureId(),
target,
format: NullTextureFormat,
internalFormat: 0,
type: 0,
filter: 0,
getWidth: () => 0,
getHeight: () => 0,
getDepth: () => 0,
getByteCount: () => 0,
define: () => { },
load: () => { },
mipmap: () => { },
bind: (id) => {
if (gl) {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, null);
}
},
unbind: (id) => {
if (gl) {
gl.activeTexture(gl.TEXTURE0 + id);
gl.bindTexture(target, null);
}
},
attachFramebuffer: () => {
throw new Error('cannot attach null-texture to a framebuffer');
},
detachFramebuffer: () => {
throw new Error('cannot detach null-texture from a framebuffer');
},
reset: () => { },
destroy: () => { },
};
}