#include "particle_system.h" #include "render/render_state.h" #include "main/references.h" #include "util/random.h" #include "scripts/binds.h" #include "ISound.h" #include "constants.h" #include "files.h" #include "memory/AllocOnlyPool.h" #include "render/vertexBuffer.h" #include #include #include extern double frameLen_s, frameTime_s; const uint32_t fileIdentifier = (uint32_t)(('S' << 24) | ('R' << 16) | ('2' << 8) | 'P'); const uint8_t currentVersion = 3; struct BinaryFile { FILE* file; BinaryFile(const char* filename, const char* mode) : file(fopen(filename, mode)) {} ~BinaryFile() { if(file) fclose(file); } bool open() const { return file != nullptr; } template BinaryFile& operator<<(const T& data) { fwrite(&data, sizeof(T), 1, file); return *this; } template BinaryFile& operator>>(T& data) { fread(&data, sizeof(T), 1, file); return *this; } void read(char* buffer, unsigned bytes) { fread(buffer, bytes, 1, file); } void write(const char* buffer, unsigned bytes) { fwrite(buffer, bytes, 1, file); } void write(const std::string& str) { uint8_t len = (uint8_t)str.size(); *this << len; write(str.c_str(), len); } void read(std::string& str) { uint8_t len; *this >> len; char buffer[255]; read(buffer, len); str.assign(buffer,len); } }; struct RandRange { float min, max; float get() const { return randomf(min,max); } void set(float low, float hi) { min = low; max = hi; } RandRange() : min(0), max(0) {} RandRange(const RandRange& other) : min(other.min), max(other.max) {} }; template T interp(const T& a, const T& b, float percent) { return a + (b - a) * percent; } Color interp(Color a, Color b, float percent) { return a.getInterpolated(b, percent); } template struct BezierCurve { std::vector values; T def; T interp(float percent) const { unsigned count = (unsigned)values.size(); if(count == 0) return def; if(count == 1) return values[0]; if(count == 2) return ::interp(values[0], values[1], percent); if(count > 100) count = 100; //Need additional space for interpolated values T buffer[512]; T* dest = &buffer[0]; --count; for(unsigned i = 0; i < count; ++i) dest[i] = ::interp(values[i], values[i+1], percent); //Each iteration, swap dest and source, reduce the next count by one, and interp between previous results //When count reaches 0, we have the final value T* source = dest + count; while(--count != 0) { std::swap(source, dest); for(unsigned i = 0; i < count; ++i) dest[i] = ::interp(source[i], source[i+1], percent); } return dest[0]; } void save(BinaryFile& file) const { uint8_t count = (uint8_t)values.size(); file << count; for(uint8_t i = 0; i < count; ++i) file << values[i]; } void load(BinaryFile& file) { uint8_t count; file >> count; values.resize(count); for(uint8_t i = 0; i < count; ++i) file >> values[i]; } }; namespace scene { struct ParticleFlowDesc { std::vector materials; std::vector matNames; BezierCurve color; BezierCurve size; float start, end; float rate; RandRange cone, spawnDist, scale, life, speed; std::string sfx_start; const resource::Sound* sound_start; bool flat; ParticleFlowDesc() : start(0.f), end(0.f), rate(1.f), sound_start(nullptr), flat(false) { size.def = 1.f; cone.set(0.f, pi); life.set(1.f, 1.f); scale.set(1.f, 1.f); } unsigned getColorCount() const { return color.values.size(); } Color getColor(int index) const { if(index < 0 || index >= (int)color.values.size()) return color.def; else return color.values[index]; } void setColor(int index, const Color& col) { if(index < 0 || index >= (int)color.values.size()) color.def = col; else color.values[index] = col; } void addColor(unsigned index, const Color& col) { if(index >= color.values.size()) color.values.push_back(col); else color.values.insert(color.values.begin() + index, col); } void removeColor(unsigned index) { if(index < color.values.size()) color.values.erase(color.values.begin() + index); } unsigned getSizeCount() const { return size.values.size(); } float getSize(int index) const { if(index < 0 || index >= (int)size.values.size()) return size.def; else return size.values[index]; } void setSize(int index, float Size) { if(index < 0 || index >= (int)size.values.size()) size.def = Size; else size.values[index] = Size; } void addSize(unsigned index, float Size) { if(index >= size.values.size()) size.values.push_back(Size); else size.values.insert(size.values.begin() + index, Size); } void removeSize(unsigned index) { if(index < size.values.size()) size.values.erase(size.values.begin() + index); } unsigned getMatCount() const { return matNames.size(); } std::string getMatName(unsigned index) const { if(index >= matNames.size()) return ""; else return matNames[index]; } void setMatName(unsigned index, const std::string& name) { if(index < matNames.size()) { matNames[index] = name; materials[index] = &devices.library.getMaterial(name); } } void addMat(const std::string& name) { matNames.push_back(name); materials.push_back(&devices.library.getMaterial(name)); } void removeMat(unsigned index) { if(index >= matNames.size()) return; matNames.erase(matNames.begin() + index); materials.erase(materials.begin() + index); } void save(BinaryFile& file) const { file << start << end << rate; file << cone << spawnDist; file << scale.min << scale.max; file << life.min << life.max; file << speed.min << speed.max; color.save(file); size.save(file); file << flat; uint8_t count = (uint8_t)matNames.size(); file << count; for(uint8_t i = 0; i < count; ++i) file.write(matNames[i]); file.write(sfx_start); } void load(BinaryFile& file, unsigned version) { file >> start >> end >> rate; if(version >= 1) { file >> cone >> spawnDist; } else { file >> cone.max; } file >> scale.min >> scale.max; file >> life.min >> life.max; file >> speed.min >> speed.max; color.load(file); size.load(file); if(version >= 3) file >> flat; uint8_t count = 0; file >> count; matNames.resize(count); materials.resize(count); for(uint8_t i = 0; i < count; ++i) { file.read(matNames[i]); //TODO: Defer matching names with materials materials[i] = &devices.library.getMaterial(matNames[i]); } if(version > 1) { file.read(sfx_start); sound_start = devices.library.getSound(sfx_start); } } }; struct Particle { const render::RenderState* mat; Particle* next; quaternionf rot; vec3d pos; vec3f vel; float scale, life, age; float rotation; float frame_scale; Color frame_color; bool update(float time, const ParticleFlowDesc* flow) { age += time; if(age >= life) return true; pos += vec3d(vel * time); float percent = age / life; frame_scale = scale * flow->size.interp(percent); frame_color = flow->color.interp(percent); return false; } Particle* updateChain(float time, const ParticleFlowDesc* flow) { Particle* cur = this, *prev = nullptr, *head = nullptr; while(cur) { if(cur->update(time, flow)) { auto* deleteParticle = cur; cur = cur->next; if(prev) prev->next = cur; delete deleteParticle; } else { if(!head) head = cur; prev = cur; cur = cur->next; } } return head; } Particle(const ParticleFlowDesc* flow, const vec3d& position, const vec3d& velocity, const quaterniond& rot, float Scale, float Life, float timeAdvance) : age(0), life(Life), next(0), pos(position) { scale = flow->scale.get() * Scale; rotation = (float)randomd(0,twopi); if(!flow->materials.empty()) mat = flow->materials[randomi(0,(int)flow->materials.size() - 1)]; else mat = &devices.library.getErrorMaterial(); vec3d from = vec3d::front(); vec3d perpRight = from.cross(vec3d::right()); vec3d perpUp = from.cross(perpRight); double perpAngle = randomd() * twopi; vec3d perp = (perpRight * cos(perpAngle) + perpUp * sin(perpAngle)).normalized(); //Slerp to the perpendicular vector based on the actual chosen spread angle double angle = flow->cone.get(); vec3d dir; if(angle < pi * 0.5) dir = from.slerp(perp, angle / (pi * 0.5)); else dir = perp.slerp(-from, (angle - pi*0.5) / (pi * 0.5)); dir = rot * dir; if(flow->flat) this->rot = quaternionf::fromImpliedTransform(vec3f::up(), vec3f(dir)); vel = vec3f((dir * (flow->speed.get() * Scale)) + velocity); pos += dir * (flow->spawnDist.get() * Scale); update(timeAdvance, flow); } static Particle* create(const ParticleFlowDesc* flow, const vec3d& position, const vec3d& velocity, const quaterniond& rot, float Scale, float timeAdvance) { float life = flow->life.get(); if(timeAdvance >= life) return nullptr; return new Particle(flow, position, velocity, rot, Scale, life, timeAdvance); } }; struct ParticleSystemDesc { std::vector flows; ParticleFlowDesc* getFlow(unsigned index) { if(index < (unsigned)flows.size()) return flows[index]; else return 0; } unsigned getFlowCount() const { return (unsigned)flows.size(); } void removeFlow(unsigned index) { //TODO: Totally unsafe if(index < (unsigned)flows.size()) { delete flows[index]; flows.erase(flows.begin() + index); } } ParticleFlowDesc* addFlow() { ParticleFlowDesc* desc = new ParticleFlowDesc(); flows.push_back(desc); return desc; } ParticleFlowDesc* copyFlow(ParticleFlowDesc* flow) { if(!flow) return nullptr; ParticleFlowDesc* desc = new ParticleFlowDesc(*flow); flows.push_back(desc); return desc; } void save(const char* filename) const { BinaryFile file(filename, "wb"); if(!file.open()) return; //Identifier and version file << fileIdentifier << currentVersion; file << (uint16_t)flows.size(); for(uint16_t i = 0, cnt = (uint16_t)flows.size(); i < cnt; ++i) flows[i]->save(file); } void load(const char* filename) { BinaryFile file(filename, "rb"); if(!file.open()) return; uint32_t identifier; uint8_t version; file >> identifier >> version; if(identifier != fileIdentifier || version > currentVersion) return; uint16_t flowCount = 0; file >> flowCount; flows.resize(flowCount); for(uint16_t i = 0; i < flowCount; ++i) { flows[i] = new ParticleFlowDesc(); flows[i]->load(file, version); } } }; ParticleSystemDesc* loadParticleSystem(const std::string& filename) { ParticleSystemDesc* system = new ParticleSystemDesc(); system->load(filename.c_str()); return system; } ParticleSystemDesc* createDummyParticleSystem() { return new ParticleSystemDesc(); } ParticleSystem* playParticleSystem(const ParticleSystemDesc* desc, Node* parent, const vec3d& pos, const quaterniond& rot, const vec3d& vel, float scale, float delay) { if(desc == 0) return 0; ParticleSystem* sys = new ParticleSystem(desc); sys->position = pos; sys->vel = vel; sys->rot = rot; sys->scale = scale; sys->delay = delay; if(parent) { sys->setFlag(NF_Independent, false); sys->queueReparent(parent); } else { sys->queueReparent(devices.scene); } return sys; } ParticleSystem::ParticleSystem(const ParticleSystemDesc* system) : age(0.f), delay(0.f), scale(1.f), lastUpdate(frameTime_s) { setFlag(NF_NoMatrix, true); setFlag(NF_Transparent, true); flows.resize(system->flows.size()); for(size_t i = 0, cnt = flows.size(); i < cnt; ++i) flows[i].flow = system->flows[i]; } ParticleSystem::~ParticleSystem() { for(size_t i = 0, cnt = flows.size(); i < cnt; ++i) { auto* particle = flows[i].list; while(particle) { auto* next = particle->next; delete particle; particle = next; } } } bool ParticleSystem::preRender(render::RenderDriver& driver) { float time = (float)(frameTime_s - lastUpdate); if(delay > 0) { delay -= time; if(delay > 0) return true; } if(time > 0) { lastUpdate = frameTime_s; rebuildTransformation(); age += time; bool alive = false; for(size_t i = 0, cnt = flows.size(); i != cnt; ++i) { auto& flowData = flows[i]; auto* flow = flowData.flow; if(age >= flow->start) { if(!flowData.started) { if(flow->sound_start) { //At the start of a flow, play its start sound (if the flow isn't already over due) if(auto* sound = flow->sound_start->play3D(abs_position,false,true,false)) { int msOffset = (int)(1000.0 * (age - flow->start)); //Avoid offsetting the sound unless it's at least a few frames off if(msOffset > 64) sound->setPlayPosition(msOffset); double base_dist = abs_scale * (flow->life.max * flow->speed.max + flow->scale.max); sound->setMinDistance(base_dist); sound->setMaxDistance(base_dist * 128.f); sound->setVolume(base_dist); sound->resume(); sound->drop(); } } flowData.started = true; } unsigned make = 0; float overtime = age - flow->end; if(overtime < 0.f) { alive = true; flowData.progress += flow->rate * time; float iPart; flowData.progress = std::modf(flowData.progress + (flow->rate * time), &iPart); make = (unsigned)iPart; } else if(flowData.progress > 0.f) { //If we had enough time in our dying moment to create a particle, do so //Handles cases of very short lived flows that only generate one particle or very few float iPart; std::modf(flowData.progress + flow->rate * (time - overtime), &iPart); if(iPart > 0) make = (unsigned)iPart; flowData.progress = 0.f; } if(flowData.list) flowData.list = flowData.list->updateChain(time, flow); if(make > 0) { float tStep = time / (float)make; float tOff = tStep; if(time > flow->life.max) { //Skip generating particles that definitely won't survive // Special case for long-duration particle systems that may spend long periods invisible int skip = (int)((time - flow->life.max) / tStep); tOff += (float)skip * tStep; make -= (unsigned)skip; } quaterniond totRot = abs_rotation * rot; while(make--) { Particle* particle = Particle::create(flow, abs_position, vel, totRot, scale, tOff); tOff += tStep; if(particle) { if(flowData.list) particle->next = flowData.list; flowData.list = particle; } } } if(flowData.list) alive = true; } else if(age < flow->end) { alive = true; } } if(!alive) { markForDeletion(); return false; } } sortDistance = devices.render->cam_pos.distanceTo(abs_position); return true; } void ParticleSystem::end() { for(size_t i = 0, cnt = flows.size(); i != cnt; ++i) age = std::max(age, flows[i].flow->end); } void ParticleSystem::render(render::RenderDriver& driver) { for(size_t i = 0, cnt = flows.size(); i != cnt; ++i) { auto& flowData = flows[i]; Particle* particle = flowData.list; if(!flowData.flow->flat) { while(particle) { devices.render->drawBillboard(particle->pos, particle->frame_scale * 2.f, *particle->mat, particle->rotation, &particle->frame_color); particle = particle->next; } } else { while(particle) { vec3f up = particle->rot * vec3f::front(particle->frame_scale); vec3f right = particle->rot * vec3f::right(particle->frame_scale); vec3f ur = up + right, ul = up - right; double st = sin(particle->rotation), ct = cos(particle->rotation); vec3f upLeft = (ul * ct) - (ur * st); vec3f upRight = (ur * ct) + (ul * st); vec3f center = vec3f(particle->pos - devices.render->cam_pos); auto* buffer = render::VertexBufferTCV::fetch(particle->mat); auto* verts = buffer->request(1, render::PT_Quads); auto* vert = &verts[0]; vert->pos = center + upLeft; vert->col = particle->frame_color; vert->uv = vec2f(0.f, 0.f); vert = &verts[1]; vert->pos = center + upRight; vert->col = particle->frame_color; vert->uv = vec2f(1.f, 0.f); vert = &verts[2]; vert->pos = center - upLeft; vert->col = particle->frame_color; vert->uv = vec2f(1.f, 1.f); vert = &verts[3]; vert->pos = center - upRight; vert->col = particle->frame_color; vert->uv = vec2f(0.f, 1.f); particle = particle->next; } } } } }; namespace scripts { void saveParticleSystem(scene::ParticleSystemDesc* system, const std::string& filename) { //TODO: Check that they aren't hacking system->save(filename.c_str()); } scene::ParticleSystemDesc* copyParticleSystem(scene::ParticleSystemDesc* system) { auto* ps = new scene::ParticleSystemDesc(); ps->flows.resize(system->flows.size()); for(size_t i = 0, cnt = system->flows.size(); i < cnt; ++i) { ps->flows[i] = new scene::ParticleFlowDesc(*system->flows[i]); } return ps; } scene::ParticleSystemDesc* makeParticleSystem() { return new scene::ParticleSystemDesc(); } std::string flowGetStartSound(const scene::ParticleFlowDesc* desc) { return desc->sfx_start; } void flowSetStartSound(scene::ParticleFlowDesc* desc, const std::string& sfx) { desc->sfx_start = sfx; desc->sound_start = devices.library.getSound(sfx); } void RegisterParticleSystemBinds() { ClassBind rr("Range", asOBJ_VALUE | asOBJ_POD | asOBJ_APP_CLASS | asOBJ_APP_CLASS_ALLFLOATS, sizeof(RandRange)); rr.addMember("float min", offsetof(RandRange,min)); rr.addMember("float max", offsetof(RandRange,max)); //TODO: Leaks, leaks everywhere ClassBind ps("ParticleSystem", asOBJ_REF | asOBJ_NOCOUNT); ClassBind flow("ParticleFlow", asOBJ_REF | asOBJ_NOCOUNT); ps.addFactory("ParticleSystem@ f()", asFUNCTION(makeParticleSystem)); ps.addMethod("ParticleFlow@ createFlow()", asMETHOD(scene::ParticleSystemDesc,addFlow)); ps.addMethod("ParticleFlow@ duplicateFlow(ParticleFlow@ flow)", asMETHOD(scene::ParticleSystemDesc,copyFlow)); ps.addMethod("void removeFlow(uint index)", asMETHOD(scene::ParticleSystemDesc,removeFlow)); ps.addMethod("ParticleFlow@ get_flows(uint index)", asMETHOD(scene::ParticleSystemDesc,getFlow)); ps.addMethod("uint get_flowCount() const", asMETHOD(scene::ParticleSystemDesc,getFlowCount)); ps.addExternMethod("void save(const string& in filename) const", asFUNCTION(saveParticleSystem)); ps.addExternMethod("ParticleSystem@ duplicate() const", asFUNCTION(copyParticleSystem)); flow.addExternMethod("string get_soundStart() const", asFUNCTION(flowGetStartSound)); flow.addExternMethod("void set_soundStart(const string& sfx)", asFUNCTION(flowSetStartSound)); flow.addMethod("string get_materials(uint index) const", asMETHOD(scene::ParticleFlowDesc,getMatName)); flow.addMethod("void set_materials(uint index, const string& id)", asMETHOD(scene::ParticleFlowDesc,setMatName)); flow.addMethod("uint get_materialCount() const", asMETHOD(scene::ParticleFlowDesc,getMatCount)); flow.addMethod("void addMaterial(const string& id)", asMETHOD(scene::ParticleFlowDesc,addMat)); flow.addMethod("void removeMaterial(uint index)", asMETHOD(scene::ParticleFlowDesc,removeMat)); flow.addMethod("uint get_colorCount() const", asMETHOD(scene::ParticleFlowDesc,getColorCount)); flow.addMethod("Color get_colors(int index) const", asMETHOD(scene::ParticleFlowDesc,getColor)); flow.addMethod("void set_colors(int index, const Color& col)", asMETHOD(scene::ParticleFlowDesc,setColor)); flow.addMethod("void removeColor(uint index)", asMETHOD(scene::ParticleFlowDesc,removeColor)); flow.addMethod("void addColor(uint index, const Color& col) const", asMETHOD(scene::ParticleFlowDesc,addColor)); flow.addMethod("uint get_sizeCount() const", asMETHOD(scene::ParticleFlowDesc,getSizeCount)); flow.addMethod("float get_sizes(int i) const", asMETHOD(scene::ParticleFlowDesc,getSize)); flow.addMethod("void set_sizes(int i, float size)", asMETHOD(scene::ParticleFlowDesc,setSize)); flow.addMethod("void removeSize(uint index)", asMETHOD(scene::ParticleFlowDesc,removeSize)); flow.addMethod("void addSize(uint index, float size) const", asMETHOD(scene::ParticleFlowDesc,addSize)); flow.addMember("float start", offsetof(scene::ParticleFlowDesc,start)) doc("Second offset from when the particle system starts to begin this flow."); flow.addMember("float end", offsetof(scene::ParticleFlowDesc,end)) doc("Second offset from when the particle system starts to end this flow."); flow.addMember("float rate", offsetof(scene::ParticleFlowDesc,rate)) doc("Particles to generate per second."); flow.addMember("Range cone", offsetof(scene::ParticleFlowDesc,cone)) doc("Radian spread of particule emission cone."); flow.addMember("Range spawnDist", offsetof(scene::ParticleFlowDesc,spawnDist)) doc("Radius at which to emit particles."); flow.addMember("Range scale", offsetof(scene::ParticleFlowDesc,scale)) doc("Range of possible particle scales."); flow.addMember("Range life", offsetof(scene::ParticleFlowDesc,life)) doc("Range of possible particle durations (in seconds)."); flow.addMember("Range speed", offsetof(scene::ParticleFlowDesc,speed)) doc("Range of possible particle speeds."); flow.addMember("bool flat", offsetof(scene::ParticleFlowDesc,flat)) doc("Whether particles should face the direction they are moving, rather than the camera."); } };