@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
458 lines (428 loc) • 15 kB
text/typescript
/**
* Allows to create a shader with GLSL nodes to create the texture values.
*
*
*/
import {
Camera,
Mesh,
PlaneGeometry,
WebGLRenderer,
WebGLRenderTarget,
ShaderMaterial,
Scene,
ClampToEdgeWrapping,
RGBAFormat,
LinearFilter,
NearestFilter,
NoToneMapping,
WebGLArrayRenderTarget,
NoColorSpace,
ColorSpace,
ToneMapping,
OrthographicCamera,
} from 'three';
import {Number2} from '../../../types/GlobalTypes';
import {Constructor, valueof} from '../../../types/GlobalTypes';
import {TypedCopNode} from './_Base';
import {GlobalsGeometryHandler} from '../gl/code/globals/Geometry';
import {GlNodeChildrenMap} from '../../poly/registers/nodes/Gl';
import {BaseGlNodeType} from '../gl/_Base';
import {GlNodeFinder} from '../gl/code/utils/NodeFinder';
import {NodeContext} from '../../poly/NodeContext';
import {IUniforms} from '../../../core/geometry/Material';
import {NodeParamsConfig, ParamConfig} from '../utils/params/ParamsConfig';
import {DataTextureController} from './utils/DataTextureController';
import {CopRendererController} from './utils/RendererController';
import {AssemblerName} from '../../poly/registers/assemblers/_BaseRegister';
import {Poly} from '../../Poly';
import {TexturePersistedConfig} from '../gl/code/assemblers/textures/TexturePersistedConfig';
import {NodeCreateOptions} from '../utils/hierarchy/ChildrenController';
import {BaseNodeType} from '../_Base';
import {CopType} from '../../poly/registers/nodes/types/Cop';
import {TextureParamsController, TextureParamConfig} from './utils/TextureParamsController';
import {isBooleanTrue} from '../../../core/Type';
import FRAGMENT_SHADER from '../gl/code/templates/textures/Default.frag.glsl';
import VERTEX_SHADER from '../gl/code/templates/textures/Default.vert.glsl';
import {handleCopBuilderDependencies} from './utils/BuilderUtils';
import {GlAssemblerController} from '../gl/code/Controller';
import {ShaderAssemblerTexture2DArray} from '../gl/code/assemblers/textures/Texture2DArray';
const RESOLUTION_DEFAULT: Number2 = [128, 128];
function Builder2DArrayCopParamConfig<TBase extends Constructor>(Base: TBase) {
return class Mixin extends Base {
/** @param textures resolution */
resolution = ParamConfig.VECTOR2(RESOLUTION_DEFAULT);
/** @param layers */
layers = ParamConfig.INTEGER(4, {
range: [2, 32],
rangeLocked: [true, false],
});
/** @param use the main camera renderer. This can save memory, but can also lead to colors being affected by the renderer's output color space */
useCameraRenderer = ParamConfig.BOOLEAN(1, {
callback: (node: BaseNodeType) => {
Builder2DArrayCopNode.PARAM_CALLBACK_render(node as Builder2DArrayCopNode);
},
});
/** @param use a data texture instead of a render target, which can be useful when using that texture as and envMap */
// useDataTexture = ParamConfig.BOOLEAN(0);
/** @param force Render */
render = ParamConfig.BUTTON(null, {
callback: (node: BaseNodeType) => {
Builder2DArrayCopNode.PARAM_CALLBACK_render(node as Builder2DArrayCopNode);
},
});
};
}
class Builder2DArrayCopParamsConfig extends TextureParamConfig(Builder2DArrayCopParamConfig(NodeParamsConfig)) {}
const ParamsConfig = new Builder2DArrayCopParamsConfig();
export class Builder2DArrayCopNode extends TypedCopNode<Builder2DArrayCopParamsConfig> {
override paramsConfig = ParamsConfig;
static override type() {
return CopType.BUILDER_2D_ARRAY;
}
override readonly persisted_config: TexturePersistedConfig = new TexturePersistedConfig(this);
protected _assemblerController = this._createAssemblerController();
public override usedAssembler(): Readonly<AssemblerName.GL_TEXTURE_2D_ARRAY> {
return AssemblerName.GL_TEXTURE_2D_ARRAY;
}
protected _createAssemblerController(): GlAssemblerController<ShaderAssemblerTexture2DArray> | undefined {
const assemblerController: GlAssemblerController<ShaderAssemblerTexture2DArray> | undefined =
Poly.assemblersRegister.assembler(this, this.usedAssembler());
if (assemblerController) {
const globalsHandler = new GlobalsGeometryHandler();
assemblerController.setAssemblerGlobalsHandler(globalsHandler);
return assemblerController;
}
}
assemblerController() {
return this._assemblerController;
}
private _textureMesh: Mesh = new Mesh(new PlaneGeometry(2, 2));
private _fragmentShader: string | undefined;
private _uniforms: IUniforms | undefined;
public readonly textureMaterial: ShaderMaterial = new ShaderMaterial({
uniforms: {},
vertexShader: VERTEX_SHADER,
fragmentShader: FRAGMENT_SHADER,
});
private _textureScene: Scene = new Scene();
private _textureCamera: Camera = new OrthographicCamera();
private _renderTarget: WebGLArrayRenderTarget | undefined;
private _dataTextureController: DataTextureController | undefined;
private _rendererController: CopRendererController | undefined;
public readonly textureParamsController: TextureParamsController = new TextureParamsController(this);
protected override _childrenControllerContext = NodeContext.GL;
override initializeNode() {
this._textureMesh.material = this.textureMaterial;
this._textureMesh.scale.multiplyScalar(0.25);
this._textureScene.add(this._textureMesh);
this._textureCamera.position.z = 1;
// this ensures the builder recooks when its children are changed
// and not just when a material that use it requests it
this.addPostDirtyHook('_cook_main_without_inputs_when_dirty', () => {
setTimeout(this._cook_main_without_inputs_when_dirty_bound, 0);
});
// this.dirtyController.addPostDirtyHook(
// '_reset_if_resolution_changed',
// this._reset_if_resolution_changed.bind(this)
// );
// this.params.onParamsCreated('reset', () => {
// this._reset();
// });
}
override createNode<S extends keyof GlNodeChildrenMap>(
node_class: S,
options?: NodeCreateOptions
): GlNodeChildrenMap[S];
override createNode<K extends valueof<GlNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K;
override createNode<K extends valueof<GlNodeChildrenMap>>(
node_class: Constructor<K>,
options?: NodeCreateOptions
): K {
return super.createNode(node_class, options) as K;
}
override children() {
return super.children() as BaseGlNodeType[];
}
override nodesByType<K extends keyof GlNodeChildrenMap>(type: K): GlNodeChildrenMap[K][] {
return super.nodesByType(type) as GlNodeChildrenMap[K][];
}
override childrenAllowed() {
if (this.assemblerController()) {
return super.childrenAllowed();
}
return false;
}
override sceneReadonly() {
return this.assemblerController() == null;
}
private _cook_main_without_inputs_when_dirty_bound = this._cook_main_without_inputs_when_dirty.bind(this);
private async _cook_main_without_inputs_when_dirty() {
await this.cookController.cookMainWithoutInputs();
}
// private _reset_if_resolution_changed(trigger?: CoreGraphNode) {
// if (trigger && trigger.graphNodeId() == this.p.resolution.graphNodeId()) {
// this._reset();
// }
// }
override async cook() {
this.compileIfRequired();
await this._renderOnTarget(true);
}
shaders_by_name() {
return {
fragment: this._fragmentShader,
};
}
compileIfRequired() {
if (this.assemblerController()?.compileRequired()) {
try {
this.compile();
} catch (err) {
const message = (err as any).message || 'failed to compile';
this.states.error.set(message);
}
}
}
private compile() {
const assemblerController = this.assemblerController();
if (!assemblerController) {
return;
}
const outputNodes: BaseGlNodeType[] = GlNodeFinder.findOutputNodes(this);
if (outputNodes.length == 0) {
this.states.error.set('one output node is required');
return;
}
if (outputNodes.length > 1) {
this.states.error.set('only one output node allowed');
return;
}
const outputNode = outputNodes[0];
if (outputNode) {
//const param_nodes = GlNodeFinder.find_param_generating_nodes(this);
const rootNodes = outputNodes; //.concat(param_nodes);
assemblerController.assembler.set_root_nodes(rootNodes);
// main compilation
assemblerController.assembler.updateFragmentShader();
// receives fragment and uniforms
const fragmentShader = assemblerController.assembler.fragment_shader();
const uniforms = assemblerController.assembler.uniforms();
if (fragmentShader && uniforms) {
this._fragmentShader = fragmentShader;
this._uniforms = uniforms;
}
handleCopBuilderDependencies({
node: this,
timeDependent: assemblerController.assembler.uniformsTimeDependent(),
uniforms: undefined,
});
}
if (this._fragmentShader && this._uniforms) {
this.textureMaterial.fragmentShader = this._fragmentShader;
this.textureMaterial.uniforms = this._uniforms;
this.textureMaterial.needsUpdate = true;
this.textureMaterial.uniforms.resolution = {
value: this.pv.resolution,
};
}
assemblerController.post_compile();
}
callbackName() {
return `cop/builder3D_${this.graphNodeId()}`;
}
// private _uniformCallbackName() {
// return `cop/builder_uniforms_${this.graphNodeId()}`;
// }
override dispose() {
super.dispose();
this._renderTarget?.dispose();
this._renderer?.dispose();
this.removeCallbacks();
}
public removeCallbacks() {
const scene = this.scene();
// scene.uniformsController.removeTimeUniform(uniforms);
scene.unRegisterOnBeforeTick(this.callbackName());
}
//
//
// RENDER + RENDER TARGET
//
//
public readonly boundRenderOnTarget = this.renderOnTargetWithoutUpdatingTextureFromParams.bind(this);
async renderOnTargetWithoutUpdatingTextureFromParams() {
this._renderOnTarget(false);
}
private async _renderOnTarget(updateTextureFromParams: boolean) {
await this.createRenderTargetIfRequired();
await this._createRendererIfRequired();
if (this.states.error.active()) {
return;
}
if (!this._renderer) {
console.warn('no renderer');
return;
}
if (!this._uniforms) {
return;
}
this._saveRendererState(this._renderer);
this._prepareRenderer(this._renderer);
const layersCount = this.pv.layers;
for (let i = 0; i < layersCount; i++) {
this._uniforms.uLayer.value = i;
this._setRenderLayer(this._renderer, i);
this._renderer.render(this._textureScene, this._textureCamera);
}
await this._postRender(updateTextureFromParams);
this._restoreRendererState(this._renderer);
}
private async _postRender(updateTextureFromParams: boolean) {
if (this._renderTarget?.texture) {
// if (isBooleanTrue(this.pv.useDataTexture) && this._renderTarget && this._renderer) {
// this._dataTextureController = this._dataTextureController || new DataTextureController();
// const texture = this._dataTextureController.fromRenderTarget(this._renderer, this._renderTarget);
// if (updateTextureFromParams) {
// await this.textureParamsController.update(texture);
// }
// this.setTexture(texture);
// } else {
const texture = this._renderTarget.texture;
if (updateTextureFromParams) {
// await this.textureParamsController.update(texture);
}
this.setTexture(texture);
// }
} else {
this.cookController.endCook();
}
}
private _prevTarget: WebGLRenderTarget | null = null;
private _prevOutputColorSpace: ColorSpace = NoColorSpace;
private _prevToneMapping: ToneMapping = NoToneMapping;
private _saveRendererState(renderer: WebGLRenderer) {
this._prevTarget = renderer.getRenderTarget();
this._prevOutputColorSpace = renderer.outputColorSpace;
this._prevToneMapping = renderer.toneMapping;
}
private _prepareRenderer(renderer: WebGLRenderer) {
if (!this._renderTarget) {
console.warn('no render target');
return;
}
renderer.outputColorSpace = NoColorSpace;
renderer.toneMapping = NoToneMapping;
}
private _setRenderLayer(renderer: WebGLRenderer, layer: number) {
if (!this._renderTarget) {
console.warn('no render target');
return;
}
renderer.setRenderTarget(this._renderTarget, layer);
renderer.clear();
}
private _restoreRendererState(renderer: WebGLRenderer) {
renderer.setRenderTarget(this._prevTarget);
renderer.outputColorSpace = this._prevOutputColorSpace;
renderer.toneMapping = this._prevToneMapping;
}
/*
*
* RENDERER
*
*/
private _renderer: WebGLRenderer | undefined;
private async _createRendererIfRequired() {
if (this._renderer) {
return;
}
if (isBooleanTrue(this.pv.useCameraRenderer)) {
this._rendererController = this._rendererController || new CopRendererController(this);
const foundRenderer = await this._rendererController.waitForRenderer();
if (foundRenderer instanceof WebGLRenderer) {
this._renderer = foundRenderer;
} else {
console.warn('found renderer is not a WebGLRenderer');
}
} else {
this._renderer = Poly.renderersController.linearRenderer();
}
}
private _resetRenderer() {
this._renderer = undefined;
}
renderer() {
return this._renderer;
}
/*
*
* RENDER TARGET
*
*/
async renderTarget() {
return (this._renderTarget =
this._renderTarget ||
(await this._createRenderTarget(this.pv.resolution.x, this.pv.resolution.y, this.pv.layers)));
}
private async createRenderTargetIfRequired() {
if (!this._renderTarget || !this._renderTargetResolutionValid()) {
this._renderTarget = await this._createRenderTarget(
this.pv.resolution.x,
this.pv.resolution.y,
this.pv.layers
);
this._dataTextureController?.reset();
}
}
private _renderTargetResolutionValid() {
if (this._renderTarget) {
const image = this._renderTarget.texture.image;
if (image.width != this.pv.resolution.x || image.height != this.pv.resolution.y) {
return false;
} else {
return true;
}
} else {
return false;
}
}
private async _createRenderTarget(width: number, height: number, depth: number) {
if (this._renderTarget) {
const image = this._renderTarget.texture.image;
console.log(image);
if (image.width == width && image.height == height && image.depth == depth) {
return this._renderTarget;
}
}
const wrapS = ClampToEdgeWrapping;
const wrapT = ClampToEdgeWrapping;
const minFilter = LinearFilter;
const magFilter = NearestFilter;
const renderTarget = new WebGLArrayRenderTarget(width, height, depth);
renderTarget.texture.wrapS = wrapS;
renderTarget.texture.wrapT = wrapT;
renderTarget.texture.minFilter = minFilter;
renderTarget.texture.magFilter = magFilter;
renderTarget.texture.format = RGBAFormat;
renderTarget.stencilBuffer = false;
renderTarget.depthBuffer = false;
// await this.textureParamsController.update(renderTarget.texture);
Poly.warn(`${this.path()}: created WebGLArrayRenderTarget`, this.path(), width, height, depth);
return renderTarget;
}
/*
*
* CALLBACK
*
*/
static PARAM_CALLBACK_render(node: Builder2DArrayCopNode) {
node._renderOnTarget(true);
}
static PARAM_CALLBACK_resetRenderer(node: Builder2DArrayCopNode) {
node._resetRenderer();
}
}