Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(particle): Add GPU particle system #204

Merged
merged 3 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@
"minify:es": "uglifyjs dist/orillusion.es.js -o dist/orillusion.es.js -c -m",
"test": "electron test/ci/main.js",
"test:ci": "xvfb-maybe -- electron --enable-unsafe-webgpu --enable-features=Vulkan --use-vulkan=swiftshader --use-webgpu-adapter=swiftshader --no-sandbox test/ci/main.js",
"docs": "npm run docs:core && npm run docs:physics && npm run docs:media && npm run docs:stats",
"docs": "npm run docs:core && npm run docs:physics && npm run docs:media && npm run docs:stats && npm run docs:particle",
"docs:typedoc": "npx typedoc --plugin typedoc-plugin-markdown --plugin ./script/typedoc-plugin-not-exported.js --tsconfig tsconfig.build.json --gitRevision main --hideBreadcrumbs true --allReflectionsHaveOwnDocument true --readme none --excludeInternal --excludePrivate --excludeProtected --sort source-order --out",
"docs:core": "npm run docs:typedoc docs/api src/index.ts",
"docs:physics": "cd packages/physics && npm run docs",
"docs:media": "cd packages/media-extention && npm run docs",
"docs:stats": "cd packages/stats && npm run docs",
"docs:particle": "cd packages/particle && npm run docs",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
},
"devDependencies": {
Expand Down
164 changes: 164 additions & 0 deletions packages/particle/ParticleSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { MaterialBase, ShaderLib, RenderNode, RendererMask, GeometryBase, Ctor, PlaneGeometry, Vector3, View3D, Time } from "@orillusion/core";
import { ParticleMaterial } from "./material/ParticleMaterial";
import { ParticleSimulator } from "./simulator/ParticleSimulator";
import { ParticleDataStructShader } from "./shader/ParticleDataStruct";

/**
* A particle system can simulate and render many small images or geometries, it called particles to produce visual effects
* @group Particle
*/
export class ParticleSystem extends RenderNode {
/**
* whether the animation will auto play
*/
public autoPlay: boolean = true;

/**
* the simulator of particle.
*/
public particleSimulator: ParticleSimulator;

/**
* playing status
*/
public playing: boolean = false;

/**
* animation playing speed
*/
public playSpeed: number = 1.0;

constructor() {
super();
this.alwaysRender = true;
this._rendererMask = RendererMask.Particle;
ShaderLib.register('ParticleDataStruct', ParticleDataStructShader);
}

/**
* material
*/
public get material(): MaterialBase {
return this._materials[0];
}

public set material(value: MaterialBase) {
this.materials = [value];
}

/**
* The geometry of the mesh determines its shape
*/
public get geometry(): GeometryBase {
return this._geometry;
}

public set geometry(value: GeometryBase) {
//this must use super geometry has reference in super
super.geometry = value;
this.object3D.bound = this._geometry.bounds.clone();
if (this._readyPipeline) {
this.initPipeline();
}
}

/**
* Set preheat time(second)
*/
public set preheatTime(value: number) {
this.particleSimulator.preheatTime = value;
}

/**
* Get preheat time(second)
*/
public get preheatTime(): number {
return this.particleSimulator.preheatTime;
}

/**
* Set particle simulator's looping
*/
public set looping(value: boolean) {
this.particleSimulator.looping = value;
}

/**
* Get particle simulator's looping
*/
public get looping(): boolean {
return this.particleSimulator.looping;
}

public init(): void {
super.init();
}

/**
* Set to use the specified particle emulator
* @param c class of particle emulator
*/
public useSimulator<T extends ParticleSimulator>(c: Ctor<T>) {
this.particleSimulator = new c();
this.particleSimulator[`initBuffer`](this);
return this.particleSimulator;
}

/**
* start to play animation, with a speed value
* @param speed playSpeed, see{@link playSpeed}
*/
public play(speed: number = 1.0) {
this.playing = true;
this.playSpeed = speed;
}

/**
* stop playing
*/
public stop(): void {
this.playing = false;
}

public start(): void {
if (!this.geometry) {
this.geometry = new PlaneGeometry(1, 1, 1, 1, Vector3.Z_AXIS);
}

if (!this.material) {
this.material = new ParticleMaterial();
}

this.particleSimulator.build();

if (this.autoPlay) {
this.playing = true;
}

let renderShader = this.material.getShader();
renderShader.setStorageBuffer(`particleGlobalData`, this.particleSimulator.particleGlobalMemory);
renderShader.setStorageBuffer(`particleLocalDatas`, this.particleSimulator.particleLocalMemory);
this.instanceCount = this.particleSimulator.maxParticle;
}

private _frame: number = -1;
private _time: number = 0;
public onCompute(view: View3D, command: GPUCommandEncoder) {
if (this._frame == -1) {
this._frame = Time.frame;
this._time += this.preheatTime;
this.particleSimulator.updateBuffer(this.preheatTime);
this.particleSimulator.compute(command);
return;
}

if (this.playing) {
this._frame = Time.frame;
let delta = Time.delta * 0.001;
delta *= this.playSpeed;
this._time += delta;
this.particleSimulator.updateBuffer(delta);
this.particleSimulator.compute(command);
}
}
}
63 changes: 63 additions & 0 deletions packages/particle/buffer/ParticleBuffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { MemoryInfo, GPUBufferBase } from "@orillusion/core";

/**
* Basic class of particle memory data
* @group Particle
*/
export class ParticleBuffer extends GPUBufferBase {
constructor(size: number, data?: Float32Array) {
super();
if (size > 0) {
this.createBuffer(GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST, size, data);
}
}

public alloc(name: string, byte: number): MemoryInfo {
let node = this.memoryNodes.get(name);
if (!node) {
node = this.memory.allocation_node(byte);
this.memoryNodes.set(name, node);
}
return node;
}

public allocInt8(name: string): MemoryInfo {
return this.alloc(name, 1);
}

public allocUint8(name: string): MemoryInfo {
return this.alloc(name, 1);
}

public allocInt16(name: string): MemoryInfo {
return this.alloc(name, 2);
}

public allocUint16(name: string): MemoryInfo {
return this.alloc(name, 2);
}

public allocInt32(name: string): MemoryInfo {
return this.alloc(name, 4);
}

public allocUint32(name: string): MemoryInfo {
return this.alloc(name, 4);
}

public allocFloat32(name: string): MemoryInfo {
return this.alloc(name, 4);
}

public allocVec2(name: string): MemoryInfo {
return this.alloc(name, 4 * 2);
}

public allocVec3(name: string): MemoryInfo {
return this.alloc(name, 4 * 3);
}

public allocVec4(name: string): MemoryInfo {
return this.alloc(name, 4 * 4);
}
}
91 changes: 91 additions & 0 deletions packages/particle/buffer/ParticleGlobalMemory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { MemoryInfo, Vector3 } from "@orillusion/core";
import { ParticleBuffer } from './ParticleBuffer';

/**
* @internal
* global particle data for all quad
* @group Plugin
*/
export class ParticleGlobalMemory extends ParticleBuffer {
protected _instanceID: MemoryInfo;
protected _maxParticles: MemoryInfo;
protected _time: MemoryInfo;
protected _timeDelta: MemoryInfo;
protected _duration: MemoryInfo;
protected _isLoop: MemoryInfo;
protected _simulatorSpace: MemoryInfo;
protected _retain1: MemoryInfo;
protected _emitterPos: MemoryInfo;
public onChange: boolean = false;

constructor(size: number, data?: Float32Array) {
super(size, data);
this._instanceID = this.allocUint32(`instance_index`);
this._maxParticles = this.allocUint32(`maxParticles`);
this._time = this.allocFloat32(`time`);
this._timeDelta = this.allocFloat32(`timeDelta`);
this._duration = this.allocFloat32(`duration`);
this._isLoop = this.allocFloat32(`isLoop`);
this._simulatorSpace = this.allocUint32(`simulatorSpace`);
this._retain1 = this.allocFloat32(`retain1`);
this._emitterPos = this.allocVec4(`emitterPos`);
}

public setInstanceID(v: number) {
this._instanceID.setUint32(v);
this.onChange = true;
}

public getInstanceID(): number {
return this._instanceID.getUint32();
}

public setMaxParticles(v: number) {
this._maxParticles.setUint32(v);
this.onChange = true;
}

public getMaxParticles(): number {
return this._maxParticles.getUint32();
}

public setTime(v: number) {
this._time.setFloat(v);
}

public getTime(): number {
return this._time.getFloat()
}

public setTimeDelta(v: number) {
this._timeDelta.setFloat(v);
this.onChange = true;
}

public getTimeDelta(): number {
return this._timeDelta.getFloat();
}

public setDuration(v: number) {
this._duration.setFloat(v);
this.onChange = true;
}

public getDuration(): number {
return this._duration.getFloat();
}

public setSimulatorSpace(v: number) {
this._simulatorSpace.setUint32(v);
this.onChange = true;
}

public getSimulatorSpace(): number {
return this._simulatorSpace.getUint32();
}

public setEmitterPos(pos: Vector3) {
this._emitterPos.setXYZ(pos.x, pos.y, pos.z);
this.onChange = true;
}
}
43 changes: 43 additions & 0 deletions packages/particle/buffer/ParticleLocalMemory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Ctor } from "@orillusion/core";
import { ParticleData } from '../data/ParticleData';
import { ParticleBuffer } from './ParticleBuffer';

/**
* @internal
* particle data for each quad
* @group Plugin
*/
export class ParticleLocalMemory extends ParticleBuffer {
public particlesData: ParticleData[] = [];

public onChange: boolean = false;

public allocationParticle<T extends ParticleData>(count: number, c: Ctor<T>): void {
if (this.particlesData.length >= count) {
return
}

for (let i = this.particlesData.length; i < count; i++) {
let pd = c[`generateParticleData`]();
this.particlesData.push(pd);
}
let singleCount = this.particlesData.length > 0 ? this.particlesData[0].totalCount : 0;

let byteSize = Math.max(singleCount * count * 4, 32);
if (this.byteSize == undefined || this.byteSize < byteSize) {
this.createBuffer(GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC, byteSize);
}
this.reset();

for (let i = 0; i < count; i++) {
const pd = this.particlesData[i];
pd.memoryList.forEach((v) => {
this.memory.allocation_memory(v);
});
// pd.memoryList.length = 0;
// pd.memoryList = null;
}

this.onChange = true;
}
}
Loading