@pnext/three-loader
Version:
Potree loader for ThreeJS, converted and adapted to Typescript.
761 lines (659 loc) • 24.9 kB
text/typescript
import {
AdditiveBlending,
BufferGeometry,
Camera,
Color,
LessEqualDepth,
Material,
NearestFilter,
NoBlending,
PerspectiveCamera,
RawShaderMaterial,
Scene,
Texture,
Vector2,
Vector3,
Vector4,
WebGLRenderer,
} from 'three';
import {
DEFAULT_HIGHLIGHT_COLOR,
DEFAULT_MAX_POINT_SIZE,
DEFAULT_MIN_POINT_SIZE,
DEFAULT_RGB_BRIGHTNESS,
DEFAULT_RGB_CONTRAST,
DEFAULT_RGB_GAMMA,
PERSPECTIVE_CAMERA,
} from '../constants';
import { PointCloudOctree } from '../point-cloud-octree';
import { PointCloudOctreeNode } from '../point-cloud-octree-node';
import { byLevelAndIndex } from '../utils/utils';
import { DEFAULT_CLASSIFICATION } from './classification';
import { ClipMode, IClipBox } from './clipping';
import {
NormalFilteringMode,
PointCloudMixingMode,
PointColorType,
PointOpacityType,
PointShape,
PointSizeType,
TreeType,
} from './enums';
import { SPECTRAL } from './gradients';
import {
generateClassificationTexture,
generateDataTexture,
generateGradientTexture,
} from './texture-generation';
import { IClassification, IGradient, IUniform } from './types';
export interface IPointCloudMaterialParameters {
size: number;
minSize: number;
maxSize: number;
treeType: TreeType;
colorRgba: boolean;
}
export interface IPointCloudMaterialUniforms {
bbSize: IUniform<[number, number, number]>;
blendDepthSupplement: IUniform<number>;
blendHardness: IUniform<number>;
classificationLUT: IUniform<Texture>;
clipBoxCount: IUniform<number>;
clipBoxes: IUniform<Float32Array>;
clipExtent: IUniform<[number, number, number, number]>;
depthMap: IUniform<Texture | null>;
diffuse: IUniform<[number, number, number]>;
fov: IUniform<number>;
gradient: IUniform<Texture>;
heightMax: IUniform<number>;
heightMin: IUniform<number>;
intensityBrightness: IUniform<number>;
intensityContrast: IUniform<number>;
intensityGamma: IUniform<number>;
intensityRange: IUniform<[number, number]>;
level: IUniform<number>;
maxSize: IUniform<number>;
minSize: IUniform<number>;
octreeSize: IUniform<number>;
opacity: IUniform<number>;
pcIndex: IUniform<number>;
rgbBrightness: IUniform<number>;
rgbContrast: IUniform<number>;
rgbGamma: IUniform<number>;
screenHeight: IUniform<number>;
screenWidth: IUniform<number>;
size: IUniform<number>;
spacing: IUniform<number>;
toModel: IUniform<number[]>;
transition: IUniform<number>;
uColor: IUniform<Color>;
visibleNodes: IUniform<Texture>;
vnStart: IUniform<number>;
wClassification: IUniform<number>;
wElevation: IUniform<number>;
wIntensity: IUniform<number>;
wReturnNumber: IUniform<number>;
wRGB: IUniform<number>;
wSourceID: IUniform<number>;
opacityAttenuation: IUniform<number>;
filterByNormalThreshold: IUniform<number>;
highlightedPointCoordinate: IUniform<Vector3>;
highlightedPointColor: IUniform<Vector4>;
enablePointHighlighting: IUniform<boolean>;
highlightedPointScale: IUniform<number>;
normalFilteringMode: IUniform<number>;
backgroundMap: IUniform<Texture | null>;
pointCloudID: IUniform<number>;
pointCloudMixAngle: IUniform<number>;
stripeDistanceX: IUniform<number>;
stripeDistanceY: IUniform<number>;
stripeDivisorX: IUniform<number>;
stripeDivisorY: IUniform<number>;
pointCloudMixingMode: IUniform<number>;
renderDepth: IUniform<boolean>;
}
const TREE_TYPE_DEFS = {
[TreeType.OCTREE]: 'tree_type_octree',
[TreeType.KDTREE]: 'tree_type_kdtree',
};
const SIZE_TYPE_DEFS = {
[PointSizeType.FIXED]: 'fixed_point_size',
[PointSizeType.ATTENUATED]: 'attenuated_point_size',
[PointSizeType.ADAPTIVE]: 'adaptive_point_size',
};
const OPACITY_DEFS = {
[PointOpacityType.ATTENUATED]: 'attenuated_opacity',
[PointOpacityType.FIXED]: 'fixed_opacity',
};
const SHAPE_DEFS = {
[PointShape.SQUARE]: 'square_point_shape',
[PointShape.CIRCLE]: 'circle_point_shape',
[PointShape.PARABOLOID]: 'paraboloid_point_shape',
};
const COLOR_DEFS = {
[PointColorType.RGB]: 'color_type_rgb',
[PointColorType.COLOR]: 'color_type_color',
[PointColorType.DEPTH]: 'color_type_depth',
[PointColorType.HEIGHT]: 'color_type_height',
[PointColorType.INTENSITY]: 'color_type_intensity',
[PointColorType.INTENSITY_GRADIENT]: 'color_type_intensity_gradient',
[PointColorType.LOD]: 'color_type_lod',
[PointColorType.POINT_INDEX]: 'color_type_point_index',
[PointColorType.CLASSIFICATION]: 'color_type_classification',
[PointColorType.RETURN_NUMBER]: 'color_type_return_number',
[PointColorType.SOURCE]: 'color_type_source',
[PointColorType.NORMAL]: 'color_type_normal',
[PointColorType.PHONG]: 'color_type_phong',
[PointColorType.RGB_HEIGHT]: 'color_type_rgb_height',
[PointColorType.COMPOSITE]: 'color_type_composite',
};
const CLIP_MODE_DEFS = {
[ClipMode.DISABLED]: 'clip_disabled',
[ClipMode.CLIP_OUTSIDE]: 'clip_outside',
[ClipMode.HIGHLIGHT_INSIDE]: 'clip_highlight_inside',
[ClipMode.CLIP_HORIZONTALLY]: 'clip_horizontally',
[ClipMode.CLIP_VERTICALLY]: 'clip_vertically',
[ClipMode.CLIP_INSIDE]: 'clip_inside',
};
export class PointCloudMaterial extends RawShaderMaterial {
private static helperVec3 = new Vector3();
private static helperVec2 = new Vector2();
/**
* Use the drawing buffer size instead of the dom client width and height when passing the screen height and screen width uniforms to the
* shader. This is useful if you have offscreen canvases (which in some browsers return 0 as client width and client height).
*/
useDrawingBufferSize = false;
lights = false;
fog = false;
colorRgba = false;
numClipBoxes: number = 0;
clipBoxes: IClipBox[] = [];
visibleNodesTexture: Texture | undefined;
visibleNodeTextureOffsets = new Map<string, number>();
private _gradient = SPECTRAL;
private gradientTexture: Texture | undefined = generateGradientTexture(this._gradient);
private _classification: IClassification = DEFAULT_CLASSIFICATION;
private classificationTexture: Texture | undefined = generateClassificationTexture(
this._classification,
);
uniforms: IPointCloudMaterialUniforms & Record<string, IUniform<any>> = {
bbSize: makeUniform('fv', [0, 0, 0] as [number, number, number]),
blendDepthSupplement: makeUniform('f', 0.0),
blendHardness: makeUniform('f', 2.0),
classificationLUT: makeUniform('t', this.classificationTexture || new Texture()),
clipBoxCount: makeUniform('f', 0),
clipBoxes: makeUniform('Matrix4fv', [] as any),
clipExtent: makeUniform('fv', [0.0, 0.0, 1.0, 1.0] as [number, number, number, number]),
depthMap: makeUniform('t', null),
diffuse: makeUniform('fv', [1, 1, 1] as [number, number, number]),
fov: makeUniform('f', 1.0),
gradient: makeUniform('t', this.gradientTexture || new Texture()),
heightMax: makeUniform('f', 1.0),
heightMin: makeUniform('f', 0.0),
intensityBrightness: makeUniform('f', 0),
intensityContrast: makeUniform('f', 0),
intensityGamma: makeUniform('f', 1),
intensityRange: makeUniform('fv', [0, 65000] as [number, number]),
isLeafNode: makeUniform('b', 0),
level: makeUniform('f', 0.0),
maxSize: makeUniform('f', DEFAULT_MAX_POINT_SIZE),
minSize: makeUniform('f', DEFAULT_MIN_POINT_SIZE),
octreeSize: makeUniform('f', 0),
opacity: makeUniform('f', 1.0),
pcIndex: makeUniform('f', 0),
rgbBrightness: makeUniform('f', DEFAULT_RGB_BRIGHTNESS),
rgbContrast: makeUniform('f', DEFAULT_RGB_CONTRAST),
rgbGamma: makeUniform('f', DEFAULT_RGB_GAMMA),
screenHeight: makeUniform('f', 1.0),
screenWidth: makeUniform('f', 1.0),
size: makeUniform('f', 1),
spacing: makeUniform('f', 1.0),
toModel: makeUniform('Matrix4f', []),
transition: makeUniform('f', 0.5),
uColor: makeUniform('c', new Color(0xffffff)),
// @ts-ignore
visibleNodes: makeUniform('t', this.visibleNodesTexture || new Texture()),
vnStart: makeUniform('f', 0.0),
wClassification: makeUniform('f', 0),
wElevation: makeUniform('f', 0),
wIntensity: makeUniform('f', 0),
wReturnNumber: makeUniform('f', 0),
wRGB: makeUniform('f', 1),
wSourceID: makeUniform('f', 0),
opacityAttenuation: makeUniform('f', 1),
filterByNormalThreshold: makeUniform('f', 0),
highlightedPointCoordinate: makeUniform('fv', new Vector3()),
highlightedPointColor: makeUniform('fv', DEFAULT_HIGHLIGHT_COLOR.clone()),
enablePointHighlighting: makeUniform('b', true),
highlightedPointScale: makeUniform('f', 2.0),
backgroundMap: makeUniform('t', null),
normalFilteringMode: makeUniform('i', NormalFilteringMode.ABSOLUTE_NORMAL_FILTERING_MODE),
pointCloudID: makeUniform('f', 2),
pointCloudMixingMode: makeUniform('i', PointCloudMixingMode.CHECKBOARD),
stripeDistanceX: makeUniform('f', 5),
stripeDistanceY: makeUniform('f', 5),
stripeDivisorX: makeUniform('f', 2),
stripeDivisorY: makeUniform('f', 2),
pointCloudMixAngle: makeUniform('f', 31),
renderDepth: makeUniform('bool', false),
};
bbSize!: [number, number, number];
clipExtent!: [number, number, number, number];
depthMap!: Texture | undefined;
fov!: number;
heightMax!: number;
heightMin!: number;
intensityBrightness!: number;
intensityContrast!: number;
intensityGamma!: number;
intensityRange!: [number, number];
maxSize!: number;
minSize!: number;
octreeSize!: number;
opacity!: number;
rgbBrightness!: number;
rgbContrast!: number;
rgbGamma!: number;
screenHeight!: number;
screenWidth!: number;
size!: number;
spacing!: number;
transition!: number;
color!: Color;
weightClassification!: number;
weightElevation!: number;
weightIntensity!: number;
weightReturnNumber!: number;
weightRGB!: number;
weightSourceID!: number;
opacityAttenuation!: number;
filterByNormalThreshold!: number;
highlightedPointCoordinate!: Vector3;
highlightedPointColor!: Vector4;
enablePointHighlighting!: boolean;
highlightedPointScale!: number;
normalFilteringMode!: number;
backgroundMap!: Texture | undefined;
pointCloudID!: number;
pointCloudMixingMode!: number;
stripeDistanceX!: number;
stripeDistanceY!: number;
stripeDivisorX!: number;
stripeDivisorY!: number;
pointCloudMixAngle!: number;
renderDepth!: boolean;
useClipBox: boolean = false;
weighted: boolean = false;
pointColorType: PointColorType = PointColorType.RGB;
pointSizeType: PointSizeType = PointSizeType.ADAPTIVE;
clipMode: ClipMode = ClipMode.DISABLED;
useEDL: boolean = false;
shape: PointShape = PointShape.SQUARE;
treeType: TreeType = TreeType.OCTREE;
pointOpacityType: PointOpacityType = PointOpacityType.FIXED;
useFilterByNormal: boolean = false;
useTextureBlending: boolean = false;
usePointCloudMixing: boolean = false;
highlightPoint: boolean = false;
attributes = {
position: { type: 'fv', value: [] },
color: { type: 'fv', value: [] },
normal: { type: 'fv', value: [] },
intensity: { type: 'f', value: [] },
classification: { type: 'f', value: [] },
returnNumber: { type: 'f', value: [] },
numberOfReturns: { type: 'f', value: [] },
pointSourceID: { type: 'f', value: [] },
indices: { type: 'fv', value: [] },
};
constructor(parameters: Partial<IPointCloudMaterialParameters> = {}) {
super();
const tex = (this.visibleNodesTexture = generateDataTexture(2048, 1, new Color(0xffffff)));
tex.minFilter = NearestFilter;
tex.magFilter = NearestFilter;
this.setUniform('visibleNodes', tex);
this.treeType = getValid(parameters.treeType, TreeType.OCTREE);
this.size = getValid(parameters.size, 1.0);
this.minSize = getValid(parameters.minSize, 2.0);
this.maxSize = getValid(parameters.maxSize, 50.0);
this.colorRgba = Boolean(parameters.colorRgba);
this.classification = DEFAULT_CLASSIFICATION;
this.defaultAttributeValues.normal = [0, 0, 0];
this.defaultAttributeValues.classification = [0, 0, 0];
this.defaultAttributeValues.indices = [0, 0, 0, 0];
this.vertexColors = true;
this.updateShaderSource();
}
dispose(): void {
super.dispose();
if (this.gradientTexture) {
this.gradientTexture.dispose();
this.gradientTexture = undefined;
}
if (this.visibleNodesTexture) {
this.visibleNodesTexture.dispose();
this.visibleNodesTexture = undefined;
}
this.clearVisibleNodeTextureOffsets();
if (this.classificationTexture) {
this.classificationTexture.dispose();
this.classificationTexture = undefined;
}
if (this.depthMap) {
this.depthMap.dispose();
this.depthMap = undefined;
}
if (this.backgroundMap) {
this.backgroundMap.dispose();
this.backgroundMap = undefined;
}
}
clearVisibleNodeTextureOffsets(): void {
this.visibleNodeTextureOffsets.clear();
}
updateShaderSource(): void {
this.vertexShader = this.applyDefines(require('./shaders/pointcloud.vert').default);
this.fragmentShader = this.applyDefines(require('./shaders/pointcloud.frag').default);
if (this.opacity === 1.0) {
this.blending = NoBlending;
this.transparent = false;
this.depthTest = true;
this.depthWrite = true;
this.depthFunc = LessEqualDepth;
} else if (this.opacity < 1.0 && !this.useEDL) {
this.blending = AdditiveBlending;
this.transparent = true;
this.depthTest = false;
this.depthWrite = true;
}
if (this.weighted) {
this.blending = AdditiveBlending;
this.transparent = true;
this.depthTest = true;
this.depthWrite = false;
this.depthFunc = LessEqualDepth;
}
this.needsUpdate = true;
}
applyDefines(shaderSrc: string): string {
const parts: string[] = [];
function define(value: string | undefined) {
if (value) {
parts.push(`#define ${value}`);
}
}
define(TREE_TYPE_DEFS[this.treeType]);
define(SIZE_TYPE_DEFS[this.pointSizeType]);
define(SHAPE_DEFS[this.shape]);
define(COLOR_DEFS[this.pointColorType]);
define(CLIP_MODE_DEFS[this.clipMode]);
define(OPACITY_DEFS[this.pointOpacityType]);
// We only perform gamma and brightness/contrast calculations per point if values are specified.
if (
this.rgbGamma !== DEFAULT_RGB_GAMMA ||
this.rgbBrightness !== DEFAULT_RGB_BRIGHTNESS ||
this.rgbContrast !== DEFAULT_RGB_CONTRAST
) {
define('use_rgb_gamma_contrast_brightness');
}
if (this.useFilterByNormal) {
define('use_filter_by_normal');
}
if (this.useEDL) {
define('use_edl');
}
if (this.weighted) {
define('weighted_splats');
}
if (this.numClipBoxes > 0) {
define('use_clip_box');
}
if (this.highlightPoint) {
define('highlight_point');
}
if (this.useTextureBlending) {
define('use_texture_blending');
}
if (this.usePointCloudMixing) {
define('use_point_cloud_mixing');
}
if (this.colorRgba) {
define('color_rgba');
}
define('MAX_POINT_LIGHTS 0');
define('MAX_DIR_LIGHTS 0');
parts.push(shaderSrc);
return parts.join('\n');
}
setPointCloudMixingMode(mode: PointCloudMixingMode) {
this.pointCloudMixingMode = mode;
}
getPointCloudMixingMode(): PointCloudMixingMode {
if (this.pointCloudMixingMode === PointCloudMixingMode.STRIPES) {
return PointCloudMixingMode.STRIPES;
}
return PointCloudMixingMode.CHECKBOARD;
}
setClipBoxes(clipBoxes: IClipBox[]): void {
if (!clipBoxes) {
return;
}
this.clipBoxes = clipBoxes;
const doUpdate =
this.numClipBoxes !== clipBoxes.length && (clipBoxes.length === 0 || this.numClipBoxes === 0);
this.numClipBoxes = clipBoxes.length;
this.setUniform('clipBoxCount', this.numClipBoxes);
if (doUpdate) {
this.updateShaderSource();
}
const clipBoxesLength = this.numClipBoxes * 16;
const clipBoxesArray = new Float32Array(clipBoxesLength);
for (let i = 0; i < this.numClipBoxes; i++) {
clipBoxesArray.set(clipBoxes[i].inverse.elements, 16 * i);
}
for (let i = 0; i < clipBoxesLength; i++) {
if (isNaN(clipBoxesArray[i])) {
clipBoxesArray[i] = Infinity;
}
}
this.setUniform('clipBoxes', clipBoxesArray);
}
get gradient(): IGradient {
return this._gradient;
}
set gradient(value: IGradient) {
if (this._gradient !== value) {
this._gradient = value;
this.gradientTexture = generateGradientTexture(this._gradient);
this.setUniform('gradient', this.gradientTexture);
}
}
get classification(): IClassification {
return this._classification;
}
set classification(value: IClassification) {
const copy: IClassification = {} as any;
for (const key of Object.keys(value)) {
copy[key] = value[key].clone();
}
let isEqual = false;
if (this._classification === undefined) {
isEqual = false;
} else {
isEqual = Object.keys(copy).length === Object.keys(this._classification).length;
for (const key of Object.keys(copy)) {
isEqual = isEqual && this._classification[key] !== undefined;
isEqual = isEqual && copy[key].equals(this._classification[key]);
}
}
if (!isEqual) {
this._classification = copy;
this.recomputeClassification();
}
}
private recomputeClassification(): void {
this.classificationTexture = generateClassificationTexture(this._classification);
this.setUniform('classificationLUT', this.classificationTexture);
}
get elevationRange(): [number, number] {
return [this.heightMin, this.heightMax];
}
set elevationRange(value: [number, number]) {
this.heightMin = value[0];
this.heightMax = value[1];
}
getUniform<K extends keyof IPointCloudMaterialUniforms>(
name: K,
): IPointCloudMaterialUniforms[K]['value'] {
return this.uniforms === undefined ? (undefined as any) : this.uniforms[name].value;
}
setUniform<K extends keyof IPointCloudMaterialUniforms>(
name: K,
value: IPointCloudMaterialUniforms[K]['value'],
): void {
if (this.uniforms === undefined) {
return;
}
const uObj = this.uniforms[name];
if (uObj.type === 'c') {
(uObj.value as Color).copy(value as Color);
} else if (value !== uObj.value) {
uObj.value = value;
}
}
updateMaterial(
octree: PointCloudOctree,
visibleNodes: PointCloudOctreeNode[],
camera: Camera,
renderer: WebGLRenderer,
): void {
const pixelRatio = renderer.getPixelRatio();
if (camera.type === PERSPECTIVE_CAMERA) {
this.fov = (camera as PerspectiveCamera).fov * (Math.PI / 180);
} else {
this.fov = Math.PI / 2; // will result in slope = 1 in the shader
}
const renderTarget = renderer.getRenderTarget();
if (renderTarget !== null) {
this.screenWidth = renderTarget.width;
this.screenHeight = renderTarget.height;
} else {
this.screenWidth = renderer.domElement.clientWidth * pixelRatio;
this.screenHeight = renderer.domElement.clientHeight * pixelRatio;
}
if (this.useDrawingBufferSize) {
renderer.getDrawingBufferSize(PointCloudMaterial.helperVec2);
this.screenWidth = PointCloudMaterial.helperVec2.width;
this.screenHeight = PointCloudMaterial.helperVec2.height;
}
const maxScale = Math.max(octree.scale.x, octree.scale.y, octree.scale.z);
this.spacing = octree.pcoGeometry.spacing * maxScale;
this.octreeSize = octree.pcoGeometry.boundingBox.getSize(PointCloudMaterial.helperVec3).x;
if (
this.pointSizeType === PointSizeType.ADAPTIVE ||
this.pointColorType === PointColorType.LOD
) {
this.updateVisibilityTextureData(visibleNodes);
}
}
private updateVisibilityTextureData(nodes: PointCloudOctreeNode[]) {
nodes.sort(byLevelAndIndex);
const data = new Uint8Array(nodes.length * 4);
const offsetsToChild = new Array(nodes.length).fill(Infinity);
this.visibleNodeTextureOffsets.clear();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
this.visibleNodeTextureOffsets.set(node.name, i);
if (i > 0) {
const parentName = node.name.slice(0, -1);
const parentOffset = this.visibleNodeTextureOffsets.get(parentName)!;
const parentOffsetToChild = i - parentOffset;
offsetsToChild[parentOffset] = Math.min(offsetsToChild[parentOffset], parentOffsetToChild);
// tslint:disable:no-bitwise
const offset = parentOffset * 4;
data[offset] = data[offset] | (1 << node.index);
data[offset + 1] = offsetsToChild[parentOffset] >> 8;
data[offset + 2] = offsetsToChild[parentOffset] % 256;
// tslint:enable:no-bitwise
}
data[i * 4 + 3] = node.name.length;
}
const texture = this.visibleNodesTexture;
if (texture) {
texture.image.data.set(data);
texture.needsUpdate = true;
}
}
static makeOnBeforeRender(
octree: PointCloudOctree,
node: PointCloudOctreeNode,
pcIndex?: number,
) {
return (
_renderer: WebGLRenderer,
_scene: Scene,
_camera: Camera,
_geometry: BufferGeometry,
material: Material,
) => {
const pointCloudMaterial = material as PointCloudMaterial;
const materialUniforms = pointCloudMaterial.uniforms;
materialUniforms.level.value = node.level;
materialUniforms.isLeafNode.value = node.isLeafNode;
const vnStart = pointCloudMaterial.visibleNodeTextureOffsets.get(node.name);
if (vnStart !== undefined) {
materialUniforms.vnStart.value = vnStart;
}
materialUniforms.pcIndex.value =
pcIndex !== undefined ? pcIndex : octree.visibleNodes.indexOf(node);
// Note: when changing uniforms in onBeforeRender, the flag uniformsNeedUpdate has to be
// set to true to instruct ThreeJS to upload them. See also
// https://github.com/mrdoob/three.js/issues/9870#issuecomment-368750182.
// Remove the cast to any after updating to Three.JS >= r113
(material as any) /*ShaderMaterial*/.uniformsNeedUpdate = true;
};
}
}
function makeUniform<T>(type: string, value: T): IUniform<T> {
return { type, value };
}
function getValid<T>(a: T | undefined, b: T): T {
return a === undefined ? b : a;
}
// tslint:disable:no-invalid-this
function uniform<K extends keyof IPointCloudMaterialUniforms>(
uniformName: K,
requireSrcUpdate: boolean = false,
): PropertyDecorator {
return (target: Object, propertyKey: string | symbol): void => {
Object.defineProperty(target, propertyKey, {
get() {
return this.getUniform(uniformName);
},
set(value: any) {
if (value !== this.getUniform(uniformName)) {
this.setUniform(uniformName, value);
if (requireSrcUpdate) {
this.updateShaderSource();
}
}
},
});
};
}
function requiresShaderUpdate() {
return (target: Object, propertyKey: string | symbol): void => {
const fieldName = `_${propertyKey.toString()}`;
Object.defineProperty(target, propertyKey, {
get() {
return this[fieldName];
},
set(value: any) {
if (value !== this[fieldName]) {
this[fieldName] = value;
this.updateShaderSource();
}
},
});
};
}