@three.ez/batched-mesh-extensions
Version:
Utility extension methods for BatchedMesh
328 lines • 14.7 kB
JavaScript
import { ColorManagement, DataTexture, FloatType, IntType, NoColorSpace, RedFormat, RedIntegerFormat, RGBAFormat, RGBAIntegerFormat, RGFormat, RGIntegerFormat, UnsignedIntType, WebGLUtils } from 'three';
/**
* Calculates the square texture size based on the capacity and pixels per instance.
* This ensures the texture is large enough to store all instances in a square layout.
* @param capacity The maximum number of instances allowed in the texture.
* @param pixelsPerInstance The number of pixels required for each instance.
* @returns The size of the square texture needed to store all the instances.
*/
export function getSquareTextureSize(capacity, pixelsPerInstance) {
return Math.max(pixelsPerInstance, Math.ceil(Math.sqrt(capacity / pixelsPerInstance)) * pixelsPerInstance);
}
/**
* Generates texture information (size, format, type) for a square texture based on the provided parameters.
* @param arrayType The constructor for the TypedArray.
* @param channels The number of channels in the texture.
* @param pixelsPerInstance The number of pixels required for each instance.
* @param capacity The maximum number of instances allowed in the texture.
* @returns An object containing the texture's array, size, format, and data type.
*/
export function getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity) {
if (channels === 3) {
console.warn('"channels" cannot be 3. Set to 4. More info: https://github.com/mrdoob/three.js/pull/23228');
channels = 4;
}
const size = getSquareTextureSize(capacity, pixelsPerInstance);
const array = new arrayType(size * size * channels);
const isFloat = arrayType.name.includes('Float');
const isUnsignedInt = arrayType.name.includes('Uint');
const type = isFloat ? FloatType : (isUnsignedInt ? UnsignedIntType : IntType);
let format;
switch (channels) {
case 1:
format = isFloat ? RedFormat : RedIntegerFormat;
break;
case 2:
format = isFloat ? RGFormat : RGIntegerFormat;
break;
case 4:
format = isFloat ? RGBAFormat : RGBAIntegerFormat;
break;
}
return { array, size, type, format };
}
/**
* A class that extends `DataTexture` to manage a square texture optimized for instances rendering.
* It supports dynamic resizing, partial update based on rows, and allows setting/getting uniforms per instance.
*/
export class SquareDataTexture extends DataTexture {
/**
* @param arrayType The constructor for the TypedArray.
* @param channels The number of channels in the texture.
* @param pixelsPerInstance The number of pixels required for each instance.
* @param capacity The total number of instances.
* @param uniformMap Optional map for handling uniform values.
* @param fetchInFragmentShader Optional flag that determines if uniform values should be fetched in the fragment shader instead of the vertex shader.
*/
constructor(arrayType, channels, pixelsPerInstance, capacity, uniformMap, fetchInFragmentShader) {
if (channels === 3)
channels = 4;
const { array, format, size, type } = getSquareTextureInfo(arrayType, channels, pixelsPerInstance, capacity);
super(array, size, size, format, type);
/**
* Whether to enable partial texture updates by row. If `false`, the entire texture will be updated.
* @default true.
*/
this.partialUpdate = true;
/**
* The maximum number of update calls per frame.
* @default Infinity
*/
this.maxUpdateCalls = Infinity;
this._utils = null; // TODO add it to renderer instead of creating for each texture
this._needsUpdate = false;
this._lastWidth = null;
this._data = array;
this._channels = channels;
this._pixelsPerInstance = pixelsPerInstance;
this._stride = pixelsPerInstance * channels;
this._rowToUpdate = new Array(size);
this._uniformMap = uniformMap;
this._fetchUniformsInFragmentShader = fetchInFragmentShader;
this.needsUpdate = true; // necessary to init texture
}
/**
* Resizes the texture to accommodate a new number of instances.
* @param count The new total number of instances.
*/
resize(count) {
const size = getSquareTextureSize(count, this._pixelsPerInstance);
if (size === this.image.width)
return;
const currentData = this._data;
const channels = this._channels;
this._rowToUpdate.length = size;
const arrayType = currentData.constructor;
const data = new arrayType(size * size * channels);
const minLength = Math.min(currentData.length, data.length);
data.set(new arrayType(currentData.buffer, 0, minLength));
this.dispose();
this.image = { data, height: size, width: size };
this._data = data;
}
/**
* Marks a row of the texture for update during the next render cycle.
* This helps in optimizing texture updates by only modifying the rows that have changed.
* @param index The index of the instance to update.
*/
enqueueUpdate(index) {
this._needsUpdate = true;
if (!this.partialUpdate)
return;
const elementsPerRow = this.image.width / this._pixelsPerInstance;
const rowIndex = Math.floor(index / elementsPerRow);
this._rowToUpdate[rowIndex] = true;
}
/**
* Updates the texture data based on the rows that need updating.
* This method is optimized to only update the rows that have changed, improving performance.
* @param renderer The WebGLRenderer used for rendering.
*/
update(renderer) {
const textureProperties = renderer.properties.get(this);
const versionChanged = this.version > 0 && textureProperties.__version !== this.version;
const sizeChanged = this._lastWidth !== null && this._lastWidth !== this.image.width;
if (!this._needsUpdate || !textureProperties.__webglTexture || versionChanged || sizeChanged) {
this._lastWidth = this.image.width;
this._needsUpdate = false;
return;
}
this._needsUpdate = false;
if (!this.partialUpdate) {
this.needsUpdate = true; // three.js will update the whole texture
return;
}
const rowsInfo = this.getUpdateRowsInfo();
if (rowsInfo.length === 0)
return;
if (rowsInfo.length > this.maxUpdateCalls) {
this.needsUpdate = true; // three.js will update the whole texture
}
else {
this.updateRows(textureProperties, renderer, rowsInfo);
}
this._rowToUpdate.fill(false);
}
// TODO reuse same objects to prevent memory leak
getUpdateRowsInfo() {
const rowsToUpdate = this._rowToUpdate;
const result = [];
for (let i = 0, l = rowsToUpdate.length; i < l; i++) {
if (rowsToUpdate[i]) {
const row = i;
for (; i < l; i++) {
if (!rowsToUpdate[i])
break;
}
result.push({ row, count: i - row });
}
}
return result;
}
updateRows(textureProperties, renderer, info) {
const state = renderer.state;
const gl = renderer.getContext();
// @ts-expect-error Expected 2 arguments, but got 3.
this._utils ?? (this._utils = new WebGLUtils(gl, renderer.extensions, renderer.capabilities)); // third argument is necessary for older three versions
const glFormat = this._utils.convert(this.format);
const glType = this._utils.convert(this.type);
const { data, width } = this.image;
const channels = this._channels;
state.bindTexture(gl.TEXTURE_2D, textureProperties.__webglTexture);
const workingPrimaries = ColorManagement.getPrimaries(ColorManagement.workingColorSpace);
const texturePrimaries = this.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries(this.colorSpace);
const unpackConversion = this.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? gl.NONE : gl.BROWSER_DEFAULT_WEBGL;
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, this.flipY);
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, this.premultiplyAlpha);
gl.pixelStorei(gl.UNPACK_ALIGNMENT, this.unpackAlignment);
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion);
for (const { count, row } of info) {
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, row, width, count, glFormat, glType, data, row * width * channels);
}
if (this.onUpdate)
this.onUpdate(this);
}
/**
* Sets a uniform value at the specified instance ID in the texture.
* @param id The instance ID to set the uniform for.
* @param name The name of the uniform.
* @param value The value to set for the uniform.
*/
setUniformAt(id, name, value) {
const { offset, size } = this._uniformMap.get(name);
const stride = this._stride;
if (size === 1) {
this._data[id * stride + offset] = value;
}
else {
value.toArray(this._data, id * stride + offset);
}
}
/**
* Retrieves a uniform value at the specified instance ID from the texture.
* @param id The instance ID to retrieve the uniform from.
* @param name The name of the uniform.
* @param target Optional target object to store the uniform value.
* @returns The uniform value for the specified instance.
*/
getUniformAt(id, name, target) {
const { offset, size } = this._uniformMap.get(name);
const stride = this._stride;
if (size === 1) {
return this._data[id * stride + offset];
}
return target.fromArray(this._data, id * stride + offset);
}
/**
* Generates the GLSL code for accessing the uniform data stored in the texture.
* @param textureName The name of the texture in the GLSL shader.
* @param indexName The name of the index in the GLSL shader.
* @param indexType The type of the index in the GLSL shader.
* @returns An object containing the GLSL code for the vertex and fragment shaders.
*/
getUniformsGLSL(textureName, indexName, indexType) {
const vertex = this.getUniformsVertexGLSL(textureName, indexName, indexType);
const fragment = this.getUniformsFragmentGLSL(textureName, indexName, indexType);
return { vertex, fragment };
}
getUniformsVertexGLSL(textureName, indexName, indexType) {
if (this._fetchUniformsInFragmentShader) {
return `
flat varying ${indexType} ez_v${indexName};
void main() {
ez_v${indexName} = ${indexName};`;
}
const texelsFetch = this.texelsFetchGLSL(textureName, indexName);
const getFromTexels = this.getFromTexelsGLSL();
const { assignVarying, declareVarying } = this.getVarying();
return `
uniform highp sampler2D ${textureName};
${declareVarying}
void main() {
${texelsFetch}
${getFromTexels}
${assignVarying}`;
}
getUniformsFragmentGLSL(textureName, indexName, indexType) {
if (!this._fetchUniformsInFragmentShader) {
const { declareVarying, getVarying } = this.getVarying();
return `
${declareVarying}
void main() {
${getVarying}`;
}
const texelsFetch = this.texelsFetchGLSL(textureName, `ez_v${indexName}`);
const getFromTexels = this.getFromTexelsGLSL();
return `
uniform highp sampler2D ${textureName};
flat varying ${indexType} ez_v${indexName};
void main() {
${texelsFetch}
${getFromTexels}`;
}
texelsFetchGLSL(textureName, indexName) {
const pixelsPerInstance = this._pixelsPerInstance;
let texelsFetch = `
int size = textureSize(${textureName}, 0).x;
int j = int(${indexName}) * ${pixelsPerInstance};
int x = j % size;
int y = j / size;
`;
for (let i = 0; i < pixelsPerInstance; i++) {
texelsFetch += `vec4 ez_texel${i} = texelFetch(${textureName}, ivec2(x + ${i}, y), 0);\n`;
}
return texelsFetch;
}
getFromTexelsGLSL() {
const uniforms = this._uniformMap;
let getFromTexels = '';
for (const [name, { type, offset, size }] of uniforms) {
const tId = Math.floor(offset / this._channels);
if (type === 'mat3') {
getFromTexels += `mat3 ${name} = mat3(ez_texel${tId}.rgb, vec3(ez_texel${tId}.a, ez_texel${tId + 1}.rg), vec3(ez_texel${tId + 1}.ba, ez_texel${tId + 2}.r));\n`;
}
else if (type === 'mat4') {
getFromTexels += `mat4 ${name} = mat4(ez_texel${tId}, ez_texel${tId + 1}, ez_texel${tId + 2}, ez_texel${tId + 3});\n`;
}
else {
const components = this.getUniformComponents(offset, size);
getFromTexels += `${type} ${name} = ez_texel${tId}.${components};\n`;
}
}
return getFromTexels;
}
getVarying() {
const uniforms = this._uniformMap;
let declareVarying = '';
let assignVarying = '';
let getVarying = '';
for (const [name, { type }] of uniforms) {
declareVarying += `flat varying ${type} ez_v${name};\n`;
assignVarying += `ez_v${name} = ${name};\n`;
getVarying += `${type} ${name} = ez_v${name};\n`;
}
return { declareVarying, assignVarying, getVarying };
}
getUniformComponents(offset, size) {
const startIndex = offset % this._channels;
let components = '';
for (let i = 0; i < size; i++) {
components += componentsArray[startIndex + i];
}
return components;
}
copy(source) {
super.copy(source);
this.partialUpdate = source.partialUpdate;
this.maxUpdateCalls = source.maxUpdateCalls;
this._channels = source._channels;
this._pixelsPerInstance = source._pixelsPerInstance;
this._stride = source._stride;
this._rowToUpdate = source._rowToUpdate;
this._uniformMap = source._uniformMap;
this._fetchUniformsInFragmentShader = source._fetchUniformsInFragmentShader;
return this;
}
}
const componentsArray = ['r', 'g', 'b', 'a'];
//# sourceMappingURL=SquareDataTexture.js.map