Add particlesystem stuff

This commit is contained in:
RandomityGuy 2021-06-08 23:27:35 +05:30
parent cc706f8cbb
commit f88433ea26
11 changed files with 553 additions and 2 deletions

View file

@ -3,5 +3,6 @@
-lib hlsdl
-lib polygonal-ds
-hl marblegame.hl
-D windowSize=1280x720
--main Main
-debug

View file

@ -1,5 +1,6 @@
package src;
import shaders.Billboard;
import collision.BoxCollisionEntity;
import shaders.DtsTexture;
import h3d.shader.AlphaMult;

View file

@ -1,5 +1,6 @@
package src;
import shaders.Billboard;
import shaders.DtsTexture;
import h3d.mat.Pass;
import h3d.shader.AlphaMult;

View file

@ -1,5 +1,12 @@
package;
import h3d.mat.Data.Blend;
import src.ParticleSystem.ParticleEmitterOptions;
import src.ParticleSystem.ParticleEmitter;
import src.ParticleSystem.Particle;
import src.ParticleSystem.ParticleManager;
import src.ParticleSystem.ParticleData;
import src.ParticleSystem.ParticleData;
import shapes.Helicopter;
import shapes.ShockAbsorber;
import shapes.SuperBounce;
@ -116,7 +123,45 @@ class Main extends hxd.App {
ag.y = 6;
world.addDtsObject(ag);
// var le:ParticleEmitterOptions = {
// ejectionPeriod: 0.01,
// ambientVelocity: new Vector(0, 0, 0),
// ejectionVelocity: 0.5,
// velocityVariance: 0.25,
// emitterLifetime: 1e8,
// inheritedVelFactor: 0.2,
// particleOptions: {
// texture: 'particles/smoke.png',
// blending: Add,
// spinSpeed: 40,
// spinRandomMin: -90,
// spinRandomMax: 90,
// lifetime: 1,
// lifetimeVariance: 0.15,
// dragCoefficient: 0.8,
// acceleration: 0,
// colors: [new Vector(0.56, 0.36, 0.26, 1), new Vector(0.56, 0.36, 0.26, 0)],
// sizes: [0.5, 1],
// times: [0, 1]
// }
// };
// var p1 = new ParticleData();
// p1.identifier = "testparticle";
// p1.texture = ResourceLoader.getTexture("data/particles/smoke.png");
// // var emitter = new ParticleEmitter(le, p1, world.particleManager); // var p = new Particle();
// world.particleManager.createEmitter(le, p1, new Vector());
// p.position = new Vector();
// p.color = new Vector(255, 255, 255);
// p.rotation = Math.PI;
// p.scale = 5;
// world.particleManager.addParticle(p1, p);
// for (i in 0...10) {
// for (j in 0...10) {
// var trapdoor = new Tornado();
// trapdoor.x = i * 2;

View file

@ -1,5 +1,7 @@
package src;
import src.ParticleSystem.ParticleData;
import src.ParticleSystem.ParticleEmitterOptions;
import src.DtsObject;
import sdl.Cursor;
import hxd.Cursor;
@ -36,6 +38,29 @@ class Move {
public function new() {}
}
final bounceParticleOptions:ParticleEmitterOptions = {
ejectionPeriod: 1,
ambientVelocity: new Vector(0, 0, 0.0),
ejectionVelocity: 2.6,
velocityVariance: 0.25 * 0.5,
emitterLifetime: 4,
inheritedVelFactor: 0,
particleOptions: {
texture: 'particles/star.png',
blending: Alpha,
spinSpeed: 90,
spinRandomMin: -90,
spinRandomMax: 90,
lifetime: 500,
lifetimeVariance: 100,
dragCoefficient: 0.5,
acceleration: -2,
colors: [new Vector(0.9, 0, 0, 1), new Vector(0.9, 0.9, 0, 1), new Vector(0.9, 0.9, 0, 0)],
sizes: [0.25, 0.25, 0.25],
times: [0, 0.75, 1]
}
};
class Marble extends GameObject {
public var camera:CameraController;
public var cameraObject:Object;
@ -86,6 +111,8 @@ class Marble extends GameObject {
var shockAbsorberEnableTime:Float = -1e8;
var helicopterEnableTime:Float = -1e8;
var bounceEmitterData:ParticleData;
public function new() {
super();
var geom = Sphere.defaultUnitSphere();
@ -100,6 +127,10 @@ class Marble extends GameObject {
this.camera = new CameraController(cast this);
this.collider = new SphereCollisionEntity(cast this);
this.bounceEmitterData = new ParticleData();
this.bounceEmitterData.identifier = "MarbleBounceParticle";
this.bounceEmitterData.texture = ResourceLoader.getTexture("data/particles/star.png");
}
public function init(level:MarbleWorld) {
@ -307,6 +338,7 @@ class Marble extends GameObject {
var velocityAdd = surfaceVel.multiply(-(1 + restitution));
var vAtC = sVel.add(this.omega.cross(contacts[i].normal.multiply(-this._radius)));
var normalVel = -contacts[i].normal.dot(sVel);
bounceEmitter(sVel.length() * restitution, contacts[i].normal);
vAtC = vAtC.sub(contacts[i].normal.multiply(contacts[i].normal.dot(sVel)));
var vAtCMag = vAtC.length();
if (vAtCMag != 0) {
@ -473,6 +505,10 @@ class Marble extends GameObject {
return [A, a];
}
function bounceEmitter(speed:Float, normal:Vector) {
this.level.particleManager.createEmitter(bounceParticleOptions, this.bounceEmitterData, this.getAbsPos().getPosition());
}
function ReportBounce(pos:Vector, normal:Vector, speed:Float) {
if (this._bounceYet && speed < this._bounceSpeed) {
return;

View file

@ -1,5 +1,6 @@
package src;
import src.ParticleSystem.ParticleManager;
import src.Util;
import h3d.Quat;
import shapes.PowerUp;
@ -21,6 +22,7 @@ import src.Marble;
class MarbleWorld {
public var collisionWorld:CollisionWorld;
public var instanceManager:InstanceManager;
public var particleManager:ParticleManager;
public var interiors:Array<InteriorObject> = [];
public var pathedInteriors:Array<PathedInterior> = [];
@ -49,6 +51,7 @@ class MarbleWorld {
this.collisionWorld = new CollisionWorld();
this.scene = scene;
this.instanceManager = new InstanceManager(scene);
this.particleManager = new ParticleManager(cast this);
this.sky = new Sky();
sky.dmlPath = "data/skies/sky_day.dml";
sky.init(cast this);
@ -112,6 +115,7 @@ class MarbleWorld {
marble.update(currentTime, dt, collisionWorld, this.pathedInteriors);
}
this.instanceManager.update(dt);
this.particleManager.update(1000 * currentTime, dt);
currentTime += dt;
if (this.marble != null) {
callCollisionHandlers(marble);

363
src/ParticleSystem.hx Normal file
View file

@ -0,0 +1,363 @@
package src;
import h3d.prim.UV;
import h3d.parts.Data.BlendMode;
import src.MarbleWorld;
import src.Util;
import h3d.mat.Data.Wrap;
import shaders.Billboard;
import hxd.IndexBuffer;
import h3d.col.Point;
import h3d.prim.Polygon;
import h3d.prim.MeshPrimitive;
import h3d.mat.Texture;
import h3d.scene.Scene;
import h3d.mat.Material;
import h3d.Vector;
import h3d.scene.MeshBatch;
import h3d.scene.Mesh;
@:publicFields
class ParticleData {
var texture:Texture;
var identifier:String;
public function new() {}
}
@:publicFields
class Particle {
var data:ParticleData;
var manager:ParticleManager;
var o:ParticleOptions;
var position:Vector;
var vel:Vector;
var rotation:Float;
var color:Vector;
var scale:Float;
var lifeTime:Float;
var initialSpin:Float;
var spawnTime:Float;
var currentAge:Float = 0;
var acc:Vector = new Vector();
public function new(options:ParticleOptions, manager:ParticleManager, data:ParticleData, spawnTime:Float, pos:Vector, vel:Vector) {
this.o = options;
this.manager = manager;
this.data = data;
this.spawnTime = spawnTime;
this.position = pos;
this.vel = vel;
this.acc = this.vel.multiply(options.acceleration);
this.lifeTime = this.o.lifetime + this.o.lifetimeVariance * (Math.random() * 2 - 1);
this.initialSpin = Util.lerp(this.o.spinRandomMin, this.o.spinRandomMax, Math.random());
}
public function update(time:Float, dt:Float) {
var t = dt;
var a = this.acc;
a = a.sub(this.vel.multiply(this.o.dragCoefficient));
this.vel = this.vel.add(a.multiply(dt));
this.position = this.position.add(this.vel.multiply(dt));
this.currentAge += dt;
var elapsed = time - this.spawnTime;
var completion = Util.clamp(elapsed / this.lifeTime, 0, 1);
if (currentAge > this.lifeTime) // Again, rewind needs this
{
this.manager.removeParticle(this.data, this);
return;
}
if (completion == 1) {
// The particle can die
this.manager.removeParticle(this.data, this);
return;
}
// var velElapsed = elapsed / 1000;
// velElapsed *= 0.001;
// velElapsed = Math.pow(velElapsed, (1 - this.o.dragCoefficient)); // Somehow slow down velocity over time based on the drag coefficient
// // Compute the position
// var pos = this.position.add(this.vel.multiply(velElapsed + this.o.acceleration * (velElapsed * velElapsed) / 2));
// this.position = pos;
this.rotation = (this.initialSpin + this.o.spinSpeed * elapsed / 1000) * Math.PI / 180;
// Check where we are in the times array
var indexLow = 0;
var indexHigh = 1;
for (i in 2...this.o.times.length) {
if (this.o.times[indexHigh] >= completion)
break;
indexLow = indexHigh;
indexHigh = i;
}
if (this.o.times.length == 1)
indexHigh = indexLow;
var t = (completion - this.o.times[indexLow]) / (this.o.times[indexHigh] - this.o.times[indexLow]);
// Adjust color
var color = Util.lerpThreeVectors(this.o.colors[indexLow], this.o.colors[indexHigh], t);
this.color = color;
// this.material.opacity = color.a * * 1.5; // Adjusted because additive mixing can be kind of extreme
// Adjust sizing
this.scale = Util.lerp(this.o.sizes[indexLow], this.o.sizes[indexHigh], t);
}
}
typedef ParticleBatch = {
var instances:Array<Particle>;
var meshBatch:MeshBatch;
}
/** The options for a single particle. */
typedef ParticleOptions = {
var texture:String;
/** Which blending mode to use. */
var blending:h3d.mat.BlendMode;
/** The spinning speed in degrees per second. */
var spinSpeed:Float;
var spinRandomMin:Float;
var spinRandomMax:Float;
var lifetime:Float;
var lifetimeVariance:Float;
var dragCoefficient:Float;
/** Acceleration along the velocity vector. */
var acceleration:Float;
var colors:Array<Vector>;
var sizes:Array<Float>;
/** Determines at what percentage of lifetime the corresponding colors and sizes are in effect. */
var times:Array<Float>;
};
/** The options for a particle emitter. */
typedef ParticleEmitterOptions = {
/** The time between particle ejections. */
var ejectionPeriod:Float; /** A fixed velocity to add to each particle. */
var ambientVelocity:Vector; /** The particle is ejected in a random direction with this velocity. */
var ejectionVelocity:Float;
var velocityVariance:Float;
var emitterLifetime:Float;
/** How much of the emitter's own velocity the particle should inherit. */
var inheritedVelFactor:Float; /** Computes a spawn offset for each particle. */
var ?spawnOffset:Void->Vector;
var particleOptions:ParticleOptions;
}
@:publicFields
class ParticleEmitter {
var o:ParticleEmitterOptions;
var data:ParticleData;
var manager:ParticleManager;
var spawnTime:Float;
var lastEmitTime:Float;
var currentWaitPeriod:Float;
var lastPos:Vector;
var lastPosTime:Float;
var currPos:Vector;
var currPosTime:Float;
var creationTime:Float;
var vel = new Vector();
var getPos:Void->Vector;
public function new(options:ParticleEmitterOptions, data:ParticleData, manager:ParticleManager, ?getPos:Void->Vector) {
this.o = options;
this.manager = manager;
this.getPos = getPos;
this.data = data;
}
public function spawn(time:Float) {
this.spawnTime = time;
this.emit(time);
}
public function tick(time:Float) {
// Cap the amount of particles emitted in such a case to prevent lag
if (time - this.lastEmitTime >= 1000)
this.lastEmitTime = time - 1000;
// Spawn as many particles as needed
while (this.lastEmitTime + this.currentWaitPeriod <= time) {
this.emit(this.lastEmitTime + this.currentWaitPeriod);
var completion = Util.clamp((this.lastEmitTime - this.spawnTime) / this.o.emitterLifetime, 0, 1);
if (completion == 1) {
this.manager.removeEmitter(this);
return;
}
}
}
/** Emit a single particle. */
public function emit(time:Float) {
this.lastEmitTime = time;
this.currentWaitPeriod = this.o.ejectionPeriod;
var pos = this.getPosAtTime(time).clone();
if (this.o.spawnOffset != null)
pos.add(this.o.spawnOffset()); // Call the spawnOffset function if it's there
// This isn't necessarily uniform but it's fine for the purpose.
var randomPointOnSphere = new Vector(Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1).normalized();
// Compute the total velocity
var vel = this.vel.multiply(this.o.inheritedVelFactor)
.add(randomPointOnSphere.multiply(this.o.ejectionVelocity + this.o.velocityVariance * (Math.random() * 2 - 1)))
.add(this.o.ambientVelocity);
var particle = new Particle(this.o.particleOptions, this.manager, this.data, time, pos, vel);
this.manager.addParticle(data, particle);
}
/** Computes the interpolated emitter position at a point in time. */
public function getPosAtTime(time:Float) {
if (this.lastPos == null)
return this.currPos;
var completion = Util.clamp((time - this.lastPosTime) / (this.currPosTime - this.lastPosTime), 0, 1);
return Util.lerpThreeVectors(this.lastPos, this.currPos, completion);
}
public function setPos(pos:Vector, time:Float) {
this.lastPos = this.currPos;
this.lastPosTime = this.currPosTime;
this.currPos = pos.clone();
this.currPosTime = time;
this.vel = this.currPos.sub(this.lastPos).multiply(1000 / (this.currPosTime - this.lastPosTime));
}
}
class ParticleManager {
var particlebatches:Map<String, ParticleBatch> = new Map();
var level:MarbleWorld;
var scene:Scene;
var currentTime:Float;
var emitters:Array<ParticleEmitter> = [];
public function new(level:MarbleWorld) {
this.level = level;
this.scene = level.scene;
}
public function update(currentTime:Float, dt:Float) {
this.tick();
this.currentTime = currentTime;
for (obj => batch in particlebatches) {
for (instance in batch.instances)
instance.update(currentTime, dt);
}
for (obj => batch in particlebatches) {
batch.meshBatch.begin(batch.instances.length);
for (instance in batch.instances) {
if (instance.currentAge != 0) {
batch.meshBatch.setPosition(instance.position.x, instance.position.y, instance.position.z);
var particleShader = batch.meshBatch.material.mainPass.getShader(Billboard);
particleShader.scale = instance.scale;
particleShader.rotation = instance.rotation;
batch.meshBatch.material.blendMode = instance.o.blending;
batch.meshBatch.material.color.load(instance.color);
batch.meshBatch.shadersChanged = true;
batch.meshBatch.setScale(instance.scale);
batch.meshBatch.emitInstance();
}
}
}
}
public function addParticle(particleData:ParticleData, particle:Particle) {
if (particlebatches.exists(particleData.identifier)) {
particlebatches.get(particleData.identifier).instances.push(particle);
} else {
var pts = [
new Point(-0.5, -0.5, 0),
new Point(-0.5, 0.5, 0),
new Point(0.5, -0.5, 0),
new Point(0.5, 0.5)
];
var prim = new Polygon(pts);
prim.idx = new IndexBuffer();
prim.idx.push(0);
prim.idx.push(1);
prim.idx.push(2);
prim.idx.push(1);
prim.idx.push(3);
prim.idx.push(2);
prim.uvs = [new UV(0, 0), new UV(0, 1), new UV(1, 0), new UV(1, 1)];
prim.addNormals();
var mat = Material.create(particleData.texture);
mat.mainPass.addShader(new h3d.shader.pbr.PropsValues(1, 0, 0, 1));
mat.texture.wrap = Wrap.Repeat;
mat.blendMode = Alpha;
var billboardShader = new Billboard();
mat.mainPass.addShader(billboardShader);
var mb = new MeshBatch(prim, mat, this.scene);
var batch:ParticleBatch = {
instances: [particle],
meshBatch: mb
};
particlebatches.set(particleData.identifier, batch);
}
}
public function removeParticle(particleData:ParticleData, particle:Particle) {
if (particlebatches.exists(particleData.identifier)) {
particlebatches.get(particleData.identifier).instances.remove(particle);
}
}
public function getTime() {
return this.currentTime;
}
public function createEmitter(options:ParticleEmitterOptions, data:ParticleData, initialPos:Vector, ?getPos:Void->Vector) {
var emitter = new ParticleEmitter(options, data, cast this, getPos);
emitter.currPos = (getPos != null) ? getPos() : initialPos.clone();
if (emitter.currPos == null)
emitter.currPos = initialPos.clone();
emitter.currPosTime = this.getTime();
emitter.creationTime = this.getTime();
emitter.spawn(this.getTime());
this.emitters.push(emitter);
return emitter;
}
public function removeEmitter(emitter:ParticleEmitter) {
this.emitters.remove(emitter);
}
public function removeEverything() {
for (particle in this.particlebatches) {
particle.instances = [];
}
for (emitter in this.emitters)
this.removeEmitter(emitter);
}
public function tick() {
var time = this.getTime();
for (emitter in this.emitters) {
if (emitter.getPos != null)
emitter.setPos(emitter.getPos(), time);
emitter.tick(time);
// Remove the artifact that was created in a different future cause we rewinded and now we shifted timelines
if (emitter.creationTime > time) {
this.removeEmitter(emitter);
}
}
}
}

View file

@ -33,7 +33,7 @@ class Util {
}
public static function lerpThreeVectors(v1:Vector, v2:Vector, t:Float) {
return new Vector(lerp(v1.x, v2.x, t), lerp(v1.y, v2.y, t), lerp(v1.z, v2.z, t));
return new Vector(lerp(v1.x, v2.x, t), lerp(v1.y, v2.y, t), lerp(v1.z, v2.z, t), lerp(v1.w, v2.w, t));
}
public static function rotateImage(bitmap:BitmapData, angle:Float) {

33
src/shaders/Billboard.hx Normal file
View file

@ -0,0 +1,33 @@
package shaders;
class Billboard extends hxsl.Shader {
static var SRC = {
@input var input:{
var uv:Vec2;
};
@global var camera:{
var view:Mat4;
var proj:Mat4;
};
@global var global:{
@perObject var modelView:Mat4;
};
@param var scale:Float;
@param var rotation:Float;
var relativePosition:Vec3;
var projectedPosition:Vec4;
var calculatedUV:Vec2;
function vertex() {
var mid = 0.5;
var uv = input.uv;
calculatedUV.x = cos(rotation) * (uv.x - mid) + sin(rotation) * (uv.y - mid) + mid;
calculatedUV.y = cos(rotation) * (uv.y - mid) - sin(rotation) * (uv.x - mid) + mid;
}
function billboard(pos:Vec2, scale:Vec2):Vec4 {
return (vec4(0, 0, 0, 1) * (global.modelView * camera.view) + vec4(pos * scale, 0, 0));
}
function __init__() {
projectedPosition = billboard(relativePosition.xy, vec2(scale, scale)) * camera.proj;
}
}
}

View file

@ -1,14 +1,45 @@
package shapes;
import src.ResourceLoader;
import src.ParticleSystem.ParticleData;
import h3d.Vector;
import src.DtsObject;
final superJumpParticleOptions:src.ParticleSystem.ParticleEmitterOptions = {
ejectionPeriod: 10,
ambientVelocity: new Vector(0, 0, 0.05),
ejectionVelocity: 1 * 0.5,
velocityVariance: 0.25 * 0.5,
emitterLifetime: 1000,
inheritedVelFactor: 0.1,
particleOptions: {
texture: 'particles/twirl.png',
blending: Add,
spinSpeed: 90,
spinRandomMin: -90,
spinRandomMax: 90,
lifetime: 1000,
lifetimeVariance: 150,
dragCoefficient: 0.25,
acceleration: 0,
colors: [new Vector(0, 0.5, 1, 0), new Vector(0, 0.6, 1, 1), new Vector(0, 0.5, 1, 0)],
sizes: [0.25, 0.25, 0.5],
times: [0, 0.75, 1]
}
};
class SuperJump extends PowerUp {
var sjEmitterParticleData:ParticleData;
public function new() {
super();
this.dtsPath = "data/shapes/items/superjump.dts";
this.isCollideable = false;
this.isTSStatic = false;
this.identifier = "SuperJump";
sjEmitterParticleData = new ParticleData();
sjEmitterParticleData.identifier = "superJumpParticle";
sjEmitterParticleData.texture = ResourceLoader.getTexture("data/particles/twirl.png");
}
public function pickUp():Bool {
@ -18,6 +49,7 @@ class SuperJump extends PowerUp {
public function use(time:Float) {
var marble = this.level.marble;
marble.velocity = marble.velocity.add(this.level.currentUp.multiply(20));
this.level.particleManager.createEmitter(superJumpParticleOptions, this.sjEmitterParticleData, null, () -> marble.getAbsPos().getPosition());
// marble.body.addLinearVelocity(this.level.currentUp.scale(20)); // Simply add to vertical velocity
// if (!this.level.rewinding)
// AudioManager.play(this.sounds[1]);

View file

@ -1,10 +1,42 @@
package shapes;
import src.ResourceLoader;
import src.ParticleSystem.ParticleData;
import src.ParticleSystem.ParticleEmitterOptions;
import h3d.Quat;
import h3d.Vector;
import src.DtsObject;
final superSpeedParticleOptions:ParticleEmitterOptions = {
ejectionPeriod: 5,
ambientVelocity: new Vector(0, 0, 0.2),
ejectionVelocity: 1 * 0.5,
velocityVariance: 0.25 * 0.5,
emitterLifetime: 1100,
inheritedVelFactor: 0.25,
particleOptions: {
texture: 'particles/spark.png',
blending: Add,
spinSpeed: 0,
spinRandomMin: 0,
spinRandomMax: 0,
lifetime: 1500,
lifetimeVariance: 150,
dragCoefficient: 0.25,
acceleration: 0,
colors: [
new Vector(0.8, 0.8, 0, 0),
new Vector(0.8, 0.8, 0, 1),
new Vector(0.8, 0.8, 0, 0)
],
sizes: [0.25, 0.25, 1],
times: [0, 0.25, 1]
}
};
class SuperSpeed extends PowerUp {
var ssEmitterParticleData:ParticleData;
public function new() {
super();
this.dtsPath = "data/shapes/items/superspeed.dts";
@ -12,6 +44,9 @@ class SuperSpeed extends PowerUp {
this.isTSStatic = false;
this.identifier = "SuperSpeed";
this.useInstancing = true;
ssEmitterParticleData = new ParticleData();
ssEmitterParticleData.identifier = "superSpeedParticle";
ssEmitterParticleData.texture = ResourceLoader.getTexture("data/particles/spark.png");
}
public function pickUp():Bool {
@ -37,7 +72,7 @@ class SuperSpeed extends PowerUp {
// marble.body.addLinearVelocity(this.level.currentUp.scale(20)); // Simply add to vertical velocity
// if (!this.level.rewinding)
// AudioManager.play(this.sounds[1]);
// this.level.particles.createEmitter(superJumpParticleOptions, null, () => Util.vecOimoToThree(marble.body.getPosition()));
this.level.particleManager.createEmitter(superSpeedParticleOptions, this.ssEmitterParticleData, null, () -> marble.getAbsPos().getPosition());
// this.level.deselectPowerUp();
}
}