-
-
Notifications
You must be signed in to change notification settings - Fork 193
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: GPU particles implementation (#3212)
GPU Particle Implementation! Added GPU particle implementation for MANY MANY particles in the simulation, similar to the existing CPU particle implementation. Note `maxParticles` is new for GPU particles. var particles = new ex.GpuParticleEmitter({ pos: ex.vec(300, 500), maxParticles: 10_000, emitRate: 1000, radius: 100, emitterType: ex.EmitterType.Circle, particle: { beginColor: ex.Color.Orange, endColor: ex.Color.Purple, focus: ex.vec(0, -400), focusAccel: 1000, startSize: 100, endSize: 0, life: 3000, minSpeed: -100, maxSpeed: 100, angularVelocity: 2, randomRotation: true, transform: ex.ParticleTransform.Local } });
- Loading branch information
Showing
27 changed files
with
1,207 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<!doctype html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>GPU Particles</title> | ||
</head> | ||
<body> | ||
<script src="../../lib/excalibur.js"></script> | ||
<script src="./index.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
var game = new ex.Engine({ | ||
width: 1000, | ||
height: 1000, | ||
displayMode: ex.DisplayMode.FitScreen | ||
}); | ||
|
||
var swordImg = new ex.ImageSource('https://cdn.rawgit.com/excaliburjs/Excalibur/7dd48128/assets/sword.png'); | ||
|
||
var particles = new ex.GpuParticleEmitter({ | ||
pos: ex.vec(300, 500), | ||
maxParticles: 10_000, | ||
emitRate: 1000, | ||
radius: 100, | ||
width: 200, | ||
height: 100, | ||
emitterType: ex.EmitterType.Rectangle, | ||
particle: { | ||
acc: ex.vec(0, -100), | ||
// opacity: 0.1, | ||
beginColor: ex.Color.Orange, | ||
endColor: ex.Color.Purple, | ||
// fade: true, | ||
focus: ex.vec(0, -400), | ||
focusAccel: 1000, | ||
startSize: 100, | ||
endSize: 0, | ||
life: 3000, | ||
minSpeed: -100, | ||
maxSpeed: 100, | ||
angularVelocity: 2, | ||
randomRotation: true, | ||
transform: ex.ParticleTransform.Local | ||
// graphic: swordImg.toSprite() | ||
} | ||
}); | ||
|
||
game.input.pointers.primary.on('move', (evt) => { | ||
particles.pos.x = evt.worldPos.x; | ||
particles.pos.y = evt.worldPos.y; | ||
}); | ||
|
||
particles.isEmitting = true; | ||
game.add(particles); | ||
|
||
game.add( | ||
new ex.Actor({ | ||
width: 200, | ||
height: 100, | ||
color: ex.Color.Red, | ||
pos: ex.vec(400, 400) | ||
}) | ||
); | ||
|
||
// var particles2 = new ex.GpuParticleEmitter({ | ||
// pos: ex.vec(700, 500), | ||
// particle: { | ||
// beginColor: ex.Color.Blue, | ||
// endColor: ex.Color.Rose, | ||
// fade: true, | ||
// startSize: 50, | ||
// endSize: 20 | ||
// } | ||
// }); | ||
// game.add(particles2); | ||
|
||
game.start(new ex.Loader([swordImg])); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
src/engine/Graphics/Context/particle-renderer/particle-fragment.glsl
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
#version 300 es | ||
precision mediump float; | ||
|
||
uniform sampler2D graphic; | ||
uniform bool useTexture; | ||
uniform float maxLifeMs; | ||
|
||
uniform vec4 beginColor; | ||
uniform vec4 endColor; | ||
uniform bool fade; | ||
uniform float startOpacity; | ||
|
||
in float finalRotation; | ||
in float finalLifeMs; | ||
out vec4 fragColor; | ||
|
||
void main(){ | ||
|
||
float lifePct = finalLifeMs / maxLifeMs; | ||
|
||
if (useTexture) { | ||
/** Draw texture */ | ||
if (lifePct <= 0.) discard; | ||
float mid = .5; | ||
float cosine = cos(finalRotation); | ||
float sine = sin(finalRotation); | ||
vec2 rotated = vec2(cosine * (gl_PointCoord.x - mid) + sine * (gl_PointCoord.y - mid) + mid, | ||
cosine * (gl_PointCoord.y - mid) - sine * (gl_PointCoord.x - mid) + mid); | ||
vec4 color = texture(graphic, rotated); | ||
fragColor = color * (fade ? lifePct : 1.0); | ||
} else { | ||
/** Draw circle */ | ||
if (lifePct <= 0.) discard; | ||
vec2 uv = gl_PointCoord.xy * 2.0 - 1.0; | ||
float dist = 1.0 - length(uv); | ||
float edge = fwidth(dot(uv, uv)); | ||
float circle = smoothstep(-edge/2.0, edge/2.0, dist); | ||
vec4 color = mix(beginColor, endColor, 1.0 - lifePct) * startOpacity; | ||
fragColor = color * (fade ? lifePct : 1.0) * circle; | ||
} | ||
} |
128 changes: 128 additions & 0 deletions
128
src/engine/Graphics/Context/particle-renderer/particle-renderer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import { ExcaliburGraphicsContextWebGL } from '../ExcaliburGraphicsContextWebGL'; | ||
import { RendererPlugin } from '../renderer'; | ||
import { Shader } from '../shader'; | ||
import particleVertexSource from './particle-vertex.glsl'; | ||
import particleFragmentSource from './particle-fragment.glsl'; | ||
import { GpuParticleRenderer } from '../../../Particles/GpuParticleRenderer'; | ||
import { vec } from '../../../Math/vector'; | ||
import { Color } from '../../../Color'; | ||
import { HTMLImageSource } from '../ExcaliburGraphicsContext'; | ||
import { ImageSourceAttributeConstants } from '../../ImageSource'; | ||
import { parseImageWrapping } from '../../Wrapping'; | ||
import { parseImageFiltering } from '../../Filtering'; | ||
import { AffineMatrix } from '../../../Math/affine-matrix'; | ||
import { ParticleTransform } from '../../../Particles/Particles'; | ||
|
||
export class ParticleRenderer implements RendererPlugin { | ||
public readonly type = 'ex.particle' as const; | ||
public priority: number = 0; | ||
private _gl!: WebGL2RenderingContext; | ||
private _context!: ExcaliburGraphicsContextWebGL; | ||
private _shader!: Shader; | ||
|
||
initialize(gl: WebGL2RenderingContext, context: ExcaliburGraphicsContextWebGL): void { | ||
this._gl = gl; | ||
this._context = context; | ||
this._shader = new Shader({ | ||
gl, | ||
vertexSource: particleVertexSource, | ||
fragmentSource: particleFragmentSource, | ||
onPreLink: (program) => { | ||
gl.transformFeedbackVaryings( | ||
program, | ||
['finalPosition', 'finalVelocity', 'finalRotation', 'finalAngularVelocity', 'finalLifeMs'], | ||
gl.INTERLEAVED_ATTRIBS | ||
); | ||
} | ||
}); | ||
this._shader.compile(); | ||
this._shader.use(); | ||
this._shader.setUniformMatrix('u_matrix', this._context.ortho); | ||
} | ||
|
||
private _getTexture(image: HTMLImageSource) { | ||
const maybeFiltering = image.getAttribute(ImageSourceAttributeConstants.Filtering); | ||
const filtering = maybeFiltering ? parseImageFiltering(maybeFiltering) : undefined; | ||
const wrapX = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingX) as any); | ||
const wrapY = parseImageWrapping(image.getAttribute(ImageSourceAttributeConstants.WrappingY) as any); | ||
|
||
const force = image.getAttribute('forceUpload') === 'true' ? true : false; | ||
const texture = this._context.textureLoader.load( | ||
image, | ||
{ | ||
filtering, | ||
wrapping: { x: wrapX, y: wrapY } | ||
}, | ||
force | ||
)!; | ||
// remove force attribute after upload | ||
image.removeAttribute('forceUpload'); | ||
return texture; | ||
} | ||
|
||
draw(renderer: GpuParticleRenderer, elapsedMs: number): void { | ||
const gl = this._gl; | ||
|
||
this._shader.use(); | ||
this._shader.setUniformMatrix('u_matrix', this._context.ortho); | ||
const transform = renderer.particle.transform === ParticleTransform.Local ? this._context.getTransform() : AffineMatrix.identity(); | ||
this._shader.setUniformAffineMatrix('u_transform', transform); | ||
this._shader.setUniformBoolean('fade', renderer.particle.fade ? true : false); | ||
this._shader.setUniformBoolean('useTexture', renderer.particle.graphic ? true : false); | ||
this._shader.setUniformFloat('maxLifeMs', renderer.particle.life ?? 2000); | ||
this._shader.setUniformFloat('deltaMs', elapsedMs); | ||
this._shader.setUniformFloatVector('gravity', renderer.particle.acc ?? vec(0, 0)); | ||
this._shader.setUniformFloatColor('beginColor', renderer.particle.beginColor ?? Color.Transparent); | ||
this._shader.setUniformFloatColor('endColor', renderer.particle.endColor ?? Color.Transparent); | ||
|
||
let startSize = renderer.particle.startSize ?? 0; | ||
let endSize = renderer.particle.endSize ?? 0; | ||
const size = renderer.particle.size ?? 0; | ||
if (size > 0) { | ||
startSize = size; | ||
endSize = size; | ||
} | ||
|
||
this._shader.setUniformFloat('startSize', startSize ?? 10); | ||
this._shader.setUniformFloat('endSize', endSize ?? 10); | ||
this._shader.setUniformFloat('startOpacity', renderer.particle.opacity ?? 1); | ||
|
||
if (renderer.particle.focus) { | ||
this._shader.setUniformFloatVector('focus', renderer.particle.focus); | ||
this._shader.setUniformFloat('focusAccel', renderer.particle.focusAccel ?? 0); | ||
} | ||
|
||
// Particle Graphic (only Sprites right now) | ||
if (renderer.particle.graphic) { | ||
const graphic = renderer.particle.graphic; | ||
|
||
const texture = this._getTexture(graphic.image.image); | ||
|
||
gl.activeTexture(gl.TEXTURE0); | ||
gl.bindTexture(gl.TEXTURE_2D, texture); | ||
this._shader.setUniformInt('graphic', 0); | ||
} | ||
|
||
// Collision Mask | ||
// gl.activeTexture(gl.TEXTURE0 + 1); | ||
// gl.bindTexture(gl.TEXTURE_2D, obstacleTex); | ||
// gl.uniform1i(u_obstacle, 1); | ||
|
||
// Blending wont work because ex doesn't have a depth attachment | ||
// gl.enable(gl.DEPTH_TEST); | ||
// gl.enable(gl.BLEND); | ||
// gl.blendEquation(gl.FUNC_ADD); | ||
// gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); | ||
|
||
renderer.draw(gl); | ||
} | ||
hasPendingDraws(): boolean { | ||
return false; | ||
} | ||
flush(): void { | ||
// pass | ||
} | ||
dispose(): void { | ||
// pass | ||
} | ||
} |
Oops, something went wrong.