UNPKG

polygonjs-engine

Version:

node-based webgl 3D engine https://polygonjs.com

472 lines (418 loc) 15.3 kB
import {Vector2} from 'three/src/math/Vector2'; import {MathUtils} from 'three/src/math/MathUtils'; import {InstancedBufferAttribute} from 'three/src/core/InstancedBufferAttribute'; import {DataTexture} from 'three/src/textures/DataTexture'; import {BufferAttribute} from 'three/src/core/BufferAttribute'; import {CoreGroup} from '../../../../../core/geometry/Group'; import {GlConstant} from '../../../../../core/geometry/GlConstant'; import {CoreMath} from '../../../../../core/math/_Module'; import {GlobalsTextureHandler} from '../../../gl/code/globals/Texture'; import {GPUComputationRenderer, GPUComputationRendererVariable} from './GPUComputationRenderer'; import {ParticlesSystemGpuSopNode} from '../../ParticlesSystemGpu'; import {WebGLRenderer} from 'three/src/renderers/WebGLRenderer'; import {Poly} from '../../../../Poly'; import {CorePoint} from '../../../../../core/geometry/Point'; import {ShaderName} from '../../../utils/shaders/ShaderName'; import {TextureAllocationsController} from '../../../gl/code/utils/TextureAllocationsController'; import {GlParamConfig} from '../../../gl/code/utils/ParamConfig'; import {ShaderMaterial} from 'three/src/materials/ShaderMaterial'; import {CoreGraphNode} from '../../../../../core/graph/CoreGraphNode'; import {FloatType, HalfFloatType} from 'three/src/constants'; export enum ParticlesDataType { FLOAT = 'float', HALF_FLOAT = 'half', } export const PARTICLE_DATA_TYPES: ParticlesDataType[] = [ParticlesDataType.FLOAT, ParticlesDataType.HALF_FLOAT]; const DATA_TYPE_BY_ENUM = { [ParticlesDataType.FLOAT]: FloatType, [ParticlesDataType.HALF_FLOAT]: HalfFloatType, }; export class ParticlesSystemGpuComputeController { protected _gpu_compute: GPUComputationRenderer | undefined; protected _simulation_restart_required: boolean = false; protected _renderer: WebGLRenderer | undefined; protected _particles_core_group: CoreGroup | undefined; protected _points: CorePoint[] = []; private variables_by_name: Map<ShaderName, GPUComputationRendererVariable> = new Map(); private _all_variables: GPUComputationRendererVariable[] = []; private _created_textures_by_name: Map<ShaderName, DataTexture> = new Map(); private _shaders_by_name: Map<ShaderName, string> | undefined; protected _last_simulated_frame: number | undefined; protected _last_simulated_time: number | undefined; protected _delta_time: number = 0; private _used_textures_size: Vector2 = new Vector2(); private _persisted_texture_allocations_controller: TextureAllocationsController | undefined; constructor(private node: ParticlesSystemGpuSopNode) {} dispose() { if (this._graph_node) { this._graph_node.dispose(); } } set_persisted_texture_allocation_controller(controller: TextureAllocationsController) { this._persisted_texture_allocations_controller = controller; } set_shaders_by_name(shaders_by_name: Map<ShaderName, string>) { this._shaders_by_name = shaders_by_name; this.reset_gpu_compute(); } all_variables() { return this._all_variables; } async init(core_group: CoreGroup) { this.init_particle_group_points(core_group); await this.create_gpu_compute(); } getCurrentRenderTarget(shader_name: ShaderName) { const variable = this.variables_by_name.get(shader_name); if (variable) { return this._gpu_compute?.getCurrentRenderTarget(variable); } } init_particle_group_points(core_group: CoreGroup) { this.reset_gpu_compute(); if (!core_group) { return; } this._particles_core_group = core_group; this._points = this._get_points() || []; } compute_similation_if_required() { const frame = this.node.scene().frame(); const start_frame: number = this.node.pv.startFrame; if (frame >= start_frame) { if (this._last_simulated_frame == null) { this._last_simulated_frame = start_frame - 1; } if (this._last_simulated_time == null) { this._last_simulated_time = this.node.scene().time(); } if (frame > this._last_simulated_frame) { this._compute_simulation(frame - this._last_simulated_frame); } } } private _compute_simulation(iterations_count = 1) { if (!this._gpu_compute || this._last_simulated_time == null) { return; } this.update_simulation_material_uniforms(); for (let i = 0; i < iterations_count; i++) { this._gpu_compute.compute(); } this.node.render_controller.update_render_material_uniforms(); this._last_simulated_frame = this.node.scene().frame(); const time = this.node.scene().time(); this._delta_time = time - this._last_simulated_time; this._last_simulated_time = time; } private _data_type() { const data_type_name = PARTICLE_DATA_TYPES[this.node.pv.dataType]; return DATA_TYPE_BY_ENUM[data_type_name]; } async create_gpu_compute() { if (this.node.pv.auto_textures_size) { const nearest_power_of_two = CoreMath.nearestPower2(Math.sqrt(this._points.length)); this._used_textures_size.x = Math.min(nearest_power_of_two, this.node.pv.maxTexturesSize.x); this._used_textures_size.y = Math.min(nearest_power_of_two, this.node.pv.maxTexturesSize.y); } else { if ( !( MathUtils.isPowerOfTwo(this.node.pv.texturesSize.x) && MathUtils.isPowerOfTwo(this.node.pv.texturesSize.y) ) ) { this.node.states.error.set('texture size must be a power of 2'); return; } const max_particles_count = this.node.pv.texturesSize.x * this.node.pv.texturesSize.y; if (this._points.length > max_particles_count) { this.node.states.error.set( `max particles is set to (${this.node.pv.texturesSize.x}x${this.node.pv.texturesSize.y}=) ${max_particles_count}` ); return; } this._used_textures_size.copy(this.node.pv.texturesSize); } this._force_time_dependent(); this._init_particles_uvs(); // we need to recreate the material if the texture allocation changes this.node.render_controller.reset_render_material(); const renderer = await Poly.renderersController.waitForRenderer(); if (renderer) { this._renderer = renderer; } else { this.node.states.error.set('no renderer found'); } if (!this._renderer) { return; } const compute = new GPUComputationRenderer( this._used_textures_size.x, this._used_textures_size.y, this._renderer ); compute.setDataType(this._data_type()); this._gpu_compute = (<unknown>compute) as GPUComputationRenderer; if (!this._gpu_compute) { this.node.states.error.set('failed to create the GPUComputationRenderer'); return; } this._last_simulated_frame = undefined; // document.body.style = '' // document.body.appendChild( renderer.domElement ); this.variables_by_name.forEach((variable, shader_name) => { variable.renderTargets[0].dispose(); variable.renderTargets[1].dispose(); this.variables_by_name.delete(shader_name); }); // for (let shader_name of Object.keys(this._shaders_by_name)) { this._all_variables = []; this._shaders_by_name?.forEach((shader, shader_name) => { if (this._gpu_compute) { const variable = this._gpu_compute.addVariable( `texture_${shader_name}`, shader, this._created_textures_by_name.get(shader_name)! ); this.variables_by_name.set(shader_name, variable); this._all_variables.push(variable); } }); this.variables_by_name?.forEach((variable, shader_name) => { if (this._gpu_compute) { this._gpu_compute.setVariableDependencies( variable, this._all_variables // currently all depend on all ); } }); this._create_texture_render_targets(); this._fill_textures(); this.create_simulation_material_uniforms(); var error = this._gpu_compute.init(); if (error !== null) { console.error(error); this.node.states.error.set(error); } } private _graph_node: CoreGraphNode | undefined; private _force_time_dependent() { // using force_time_dependent would force the whole node to recook, // but that would also trigger the obj geo node to update its display node. // A better way is to just recompute the sim only, outside of the cook method. // But we need to be sure that on first frame, we are still recooking the whole node // this.node.states.time_dependent.force_time_dependent(); if (!this._graph_node) { this._graph_node = new CoreGraphNode(this.node.scene(), 'gpu_compute'); this._graph_node.addGraphInput(this.node.scene().timeController.graph_node); this._graph_node.addPostDirtyHook('on_time_change', this._on_graph_node_dirty.bind(this)); } } private _on_graph_node_dirty() { if (this.node.is_on_frame_start()) { this.node.setDirty(); return; } else { this.compute_similation_if_required(); } } private create_simulation_material_uniforms() { const assemblerController = this.node.assemblerController; const assembler = assemblerController?.assembler; if (!assembler && !this._persisted_texture_allocations_controller) { return; } const all_materials: ShaderMaterial[] = []; this.variables_by_name.forEach((variable, shader_name) => { // const uniforms = variable.material.uniforms; all_materials.push(variable.material); }); for (let material of all_materials) { material.uniforms[GlConstant.TIME] = {value: this.node.scene().time()}; material.uniforms[GlConstant.DELTA_TIME] = {value: this.node.scene().time()}; } if (assembler) { for (let material of all_materials) { for (let param_config of assembler.param_configs()) { material.uniforms[param_config.uniform_name] = param_config.uniform; } } } else { const persisted_data = this.node.persisted_config.loaded_data(); if (persisted_data) { const persisted_uniforms = this.node.persisted_config.uniforms(); if (persisted_uniforms) { const param_uniform_pairs = persisted_data.param_uniform_pairs; for (let pair of param_uniform_pairs) { const param_name = pair[0]; const uniform_name = pair[1]; const param = this.node.params.get(param_name); const uniform = persisted_uniforms[uniform_name]; for (let material of all_materials) { material.uniforms[uniform_name] = uniform; } if (param && uniform) { param.options.set_option('callback', () => { for (let material of all_materials) { GlParamConfig.callback(param, material.uniforms[uniform_name]); } }); } } } } } } private update_simulation_material_uniforms() { for (let variable of this._all_variables) { variable.material.uniforms[GlConstant.TIME].value = this.node.scene().time(); variable.material.uniforms[GlConstant.DELTA_TIME].value = this._delta_time; } } private _init_particles_uvs() { var uvs = new Float32Array(this._points.length * 2); let p = 0; var cmptr = 0; for (var j = 0; j < this._used_textures_size.x; j++) { for (var i = 0; i < this._used_textures_size.y; i++) { uvs[p++] = i / (this._used_textures_size.x - 1); uvs[p++] = j / (this._used_textures_size.y - 1); cmptr += 2; if (cmptr >= uvs.length) { break; } } } const uv_attrib_name = GlobalsTextureHandler.UV_ATTRIB; if (this._particles_core_group) { for (let core_geometry of this._particles_core_group.coreGeometries()) { const geometry = core_geometry.geometry(); const attribute_constructor = core_geometry.markedAsInstance() ? InstancedBufferAttribute : BufferAttribute; geometry.setAttribute(uv_attrib_name, new attribute_constructor(uvs, 2)); } } } created_textures_by_name() { return this._created_textures_by_name; } private _fill_textures() { const assemblerController = this.node.assemblerController; const assembler = assemblerController?.assembler; const texture_allocations_controller = assembler ? assembler.texture_allocations_controller : this._persisted_texture_allocations_controller; if (!texture_allocations_controller) { return; } this._created_textures_by_name.forEach((texture, shader_name) => { const texture_allocation = texture_allocations_controller.allocation_for_shader_name(shader_name); if (!texture_allocation) { return; } const texture_variables = texture_allocation.variables; if (!texture_variables) { return; } const array = texture.image.data; for (let texture_variable of texture_variables) { const texture_position = texture_variable.position; let variable_name = texture_variable.name(); const first_point = this._points[0]; if (first_point) { const has_attrib = first_point.hasAttrib(variable_name); if (has_attrib) { const attrib_size = first_point.attribSize(variable_name); let cmptr = texture_position; for (let point of this._points) { if (attrib_size == 1) { const val: number = point.attribValue(variable_name) as number; array[cmptr] = val; } else { (point.attribValue(variable_name) as Vector2).toArray(array, cmptr); } cmptr += 4; } } } } }); } reset_gpu_compute() { this._gpu_compute = undefined; this._simulation_restart_required = true; } set_restart_not_required() { this._simulation_restart_required = false; } reset_gpu_compute_and_set_dirty() { this.reset_gpu_compute(); this.node.setDirty(); } reset_particle_groups() { this._particles_core_group = undefined; } get initialized(): boolean { return this._particles_core_group != null && this._gpu_compute != null; } private _create_texture_render_targets() { this._created_textures_by_name.forEach((texture, shader_name) => { texture.dispose(); }); this._created_textures_by_name.clear(); this.variables_by_name.forEach((texture_variable, shader_name) => { if (this._gpu_compute) { this._created_textures_by_name.set(shader_name, this._gpu_compute.createTexture()); } }); } restart_simulation_if_required() { if (this._simulation_restart_required) { this._restart_simulation(); } } private _restart_simulation() { this._last_simulated_time = undefined; this._create_texture_render_targets(); const points = this._get_points(); // TODO: typescript - not sure that's right if (!points) { return; } this._fill_textures(); this.variables_by_name.forEach((variable, shader_name) => { const texture = this._created_textures_by_name.get(shader_name); if (this._gpu_compute && texture) { this._gpu_compute.renderTexture(texture, variable.renderTargets[0]); this._gpu_compute.renderTexture(texture, variable.renderTargets[1]); } }); } // if we have a mix of marked_as_instance and non marked_as_instance // we take all geos that are the type that comes first private _get_points() { if (!this._particles_core_group) { return; } let geometries = this._particles_core_group.coreGeometries(); const first_geometry = geometries[0]; if (first_geometry) { const type = first_geometry.markedAsInstance(); const selected_geometries = []; for (let geometry of geometries) { if (geometry.markedAsInstance() == type) { selected_geometries.push(geometry); } } const points = []; for (let geometry of selected_geometries) { for (let point of geometry.points()) { points.push(point); } } return points; } else { return []; } } }