Created
May 2, 2017 07:16
-
-
Save VirtuosoChris/6e0d163e76a8d42fd7b5084d2e24cdfc to your computer and use it in GitHub Desktop.
SHOPS - Stateless Header Only Particle System - VS based, part of HOPS library.
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
// | |
// HOPS.h | |
// IMGUITEST | |
// | |
// Created by Chris Pugh on 3/28/17. | |
// Copyright © 2017 VirtuosoEngine. All rights reserved. | |
// | |
/// --- Header Only Particle System by Virtuoso Engine --- | |
/// | |
/// GPU accelerated particles calculated with a vertex shader | |
/// The CPU emits particles and cleans them up when they expire; no intermediate updates | |
/// The vertex shader computes particle positions, sizes, colors, etc analytically instead of incremental time updates | |
/// | |
/// GUI Editor is built in (Dear IMGUI) as long as you include imgui.h first (IMGUI_VERSION defined) | |
/// Call psGUI while an IMGUI window is open to add GUI parameters | |
/// | |
/// glsl version string should be defined by the user before including the header as #define HOPS_GLSL_VERSION_STRING | |
/// defaults to #version 410 core if not defined by the user | |
/// | |
///\todo screen aligned velocity aligned particles for air resistance path. | |
///\todo fix "spear throwing" appearance to the top of the gravity arc for arc welder type particles | |
///\todo textured point sprites | |
///\todo screen alignment modes | |
#ifndef VIRTUOSO_HOPS_h | |
#define VIRTUOSO_HOPS_h | |
#include <array> | |
#include <queue> | |
#ifndef HOPS_GLSL_VERSION_STRING | |
#define HOPS_GLSL_VERSION_STRING "#version 410 core" | |
#endif | |
namespace Virtuoso | |
{ | |
namespace HOPS | |
{ | |
///evaluate the 1D gaussian curve at a particular point | |
inline double gaussian1D(double distanceX, double stdev) | |
{ | |
double a = 1.0 / sqrt(2.0 * M_PI * stdev * stdev); | |
double exponent = (-distanceX * distanceX )/ (2.0 * stdev * stdev); | |
return a * exp(exponent); | |
} | |
///evaluate the 2 dimensional gaussian curve at a particular point | |
inline double gaussian2D(double distanceX,double distanceY, double stdev) | |
{ | |
return gaussian1D( distanceX , stdev) * gaussian1D(distanceY, stdev); | |
} | |
// make a square sprite, grayscale, based on a 2D gaussian function. | |
// useful as a generic particle, for sparks, etc. | |
void gaussianParticleImageData(std::uint8_t* outBuffer, float stdev, int spriteWidth) | |
{ | |
const int splatWidth = spriteWidth; | |
const int splatHeight = splatWidth; | |
const float scale = 6. * stdev; ///\todo it really should just be 6 * the stdev! This isnt' correct... | |
for (int h = 0; h < splatHeight; h++) | |
{ | |
for (int w =0; w < splatWidth; w++) | |
{ | |
const float uCoord = (.5 + w) / (float) splatWidth; | |
const float vCoord = (.5 + h) / (float) splatHeight; | |
const float centralizedU = uCoord * 2.0 - 1.0; | |
const float centralizedV = vCoord * 2.0 - 1.0; | |
const std::size_t outLocation = splatWidth * h + w; | |
const float scaledVal = 255. * gaussian2D(scale * centralizedU, scale * centralizedV, stdev); | |
outBuffer[outLocation] = std::min<float>(255., scaledVal);; | |
} | |
} | |
} | |
const unsigned int UNLIMITED_RECYCLE = std::numeric_limits<unsigned int>::max(); | |
class ParticleSystemOptions; | |
std::string makePSVertexShader(const ParticleSystemOptions& options); | |
std::string makePSFragShader(const ParticleSystemOptions& options); | |
enum ParticleParameterMode {CONSTANT_VALUE, UNIFORM_VALUE, PER_PARTICLE_VALUE, MAX_PARAMETER_MODES}; | |
/// enum ParticleInitializationMode {CONSTANT_INIT, FUNCTION_INIT, MAX_PARAMETER_INIT_MODES}; | |
enum ParticleParameterUpdateMode {UPDATE_DISABLED, UPDATE_LINEAR, MAX_UPDATE_MODES}; | |
enum ParticleTextureMode {TEXTURE_DISABLED=0, TEXTURE_2D=1, TEXTURE_2D_ATLAS=2, TEXTURE_2D_ARRAY=3, MAX_TEXTURE_MODE}; | |
template <typename RealType, int Length> | |
struct ParticleParameterInitializationValues | |
{ | |
typedef std::array<RealType, Length> ValueType; | |
static const std::size_t Elements = Length; | |
ValueType value = {0}; | |
///std::function<ValueType (const ParticleParameterInitializationValues&)> func; | |
}; | |
template <typename RealType> | |
struct ParticleParameterInitializationValues<RealType, 1> | |
{ | |
typedef RealType ValueType; | |
static const std::size_t Elements = 1; | |
ValueType value = 0; | |
///std::function<ValueType (const ParticleParameterInitializationValues&)> func; | |
}; | |
template <typename RealType, int Length=1> | |
struct ParticleParameter | |
{ | |
static const std::size_t Elements = Length; | |
ParticleParameterMode paramMode = CONSTANT_VALUE; | |
/// ParticleInitializationMode paramInitMode = CONSTANT_INIT; | |
ParticleParameterInitializationValues<RealType, Length> paramInitValues; | |
std::string shaderName; | |
}; | |
template <typename RealType, int Length=1> | |
struct UpdatableParticleParameter : public ParticleParameter <RealType, Length> | |
{ | |
typedef std::array<RealType, Length> ValueType; | |
ParticleParameterUpdateMode updateMode = UPDATE_DISABLED; | |
ParticleParameterMode deltaParameterMode = CONSTANT_VALUE; | |
ValueType delta = {0}; | |
}; | |
template <typename RealType> | |
struct UpdatableParticleParameter<RealType, 1> : public ParticleParameter<RealType, 1> | |
{ | |
typedef RealType ValueType; | |
ParticleParameterUpdateMode updateMode = UPDATE_DISABLED; | |
ParticleParameterMode deltaParameterMode = CONSTANT_VALUE; | |
ValueType delta = 0; | |
}; | |
struct ParticleSystemOptions | |
{ | |
ParticleParameter<float, 3> position; | |
ParticleParameter<float, 3> velocity; | |
ParticleParameter<float, 3> acceleration; | |
UpdatableParticleParameter<float, 3> color; | |
UpdatableParticleParameter<float> size; | |
ParticleParameter<float> spawnTime; | |
ParticleParameter<float> lifespan; | |
ParticleParameter<float> resistance; | |
UpdatableParticleParameter<float> rotation; | |
UpdatableParticleParameter<int> textureFrame; // initialize to the start frame, will "modulus" back around after hitting the max texture frame | |
ParticleParameter<int> maxTextureFrame; | |
ParticleTextureMode textureMode = TEXTURE_DISABLED; | |
bool useResistanceValue=false; | |
bool quadPrimitives = false; | |
bool enableAnimation = false; | |
bool enableRotation = false; | |
bool hideExpiredParticles = false; ///\todo implement me | |
bool velocityAlignedParticles = false; | |
std::string vsSource; | |
std::string psSource; | |
ParticleSystemOptions() | |
{ | |
position.shaderName = "position"; | |
velocity.shaderName = "velocity"; | |
acceleration.shaderName = "acceleration"; | |
color.shaderName = "color"; | |
size.shaderName = "size"; | |
spawnTime.shaderName = "spawnTime"; | |
lifespan.shaderName = "lifespan"; | |
resistance.shaderName = "resistance"; | |
resistance.paramInitValues.value = .1; | |
rotation.shaderName = "rotation"; | |
textureFrame.shaderName = "textureFrame"; | |
maxTextureFrame.shaderName = "maxTextureFrame"; | |
} | |
}; | |
struct PSEmitter | |
{ | |
std::function<std::array<float, 3> (void)> position; | |
std::function<std::array<float, 3> (void)> velocity; | |
std::function<std::array<float, 3> (void)> acceleration; | |
std::function<std::array<float, 3> (void)> color; | |
std::function<float (void)> size; | |
std::function<float (void)> spawnTime; | |
std::function<float (void)> lifespan; | |
std::function<float (void)> resistance; | |
std::function<float (void)> rotation; | |
std::function<int (void)> textureFrame; | |
std::function<int (void)> maxTextureFrame; | |
}; | |
struct PSRenderer | |
{ | |
gl::Program psProg; | |
gl::VertexArray psVAO; | |
gl::Buffer psBuffer; | |
gl::Buffer quadBuffer; | |
std::size_t currentParticles=0; | |
const std::size_t maxParticles; | |
std::size_t vertexSizeBytes=0; | |
typedef std::priority_queue<double, std::vector<double>, std::greater<double> > LifespanQueue; | |
LifespanQueue lifespanQueue; | |
std::vector<double> shadowParticles; | |
std::vector<GLubyte> newParticlesBuffer; | |
std::size_t particlesRenderedFrame=0; | |
std::size_t particlesReclaimedFrame=0; | |
std::size_t particlesFailedEmission=0; | |
int newParticlesBatch = 0; | |
std::size_t attribCount; // opengl attributes | |
std::size_t particleVBOSizeBytes() | |
{ | |
return maxParticles * vertexSizeBytes; | |
} | |
PSRenderer(const ParticleSystemOptions& opts, std::size_t maxParticlesIn = 100000) | |
: maxParticles(maxParticlesIn) | |
{ | |
initialize(opts); | |
newParticlesBuffer.resize(maxParticlesIn * vertexSizeBytes); | |
shadowParticles.resize(maxParticlesIn); | |
} | |
int countExpiredParticles(double currentTime, int maxKillParticles) | |
{ | |
int rval=0; | |
while (((rval < maxKillParticles) || !maxKillParticles) && lifespanQueue.size() && lifespanQueue.top() <= currentTime) | |
{ | |
lifespanQueue.pop(); | |
rval++; | |
} | |
return rval; | |
} | |
void createParticle(PSEmitter& emitter, double currentTime, ParticleSystemOptions& opts, GLubyte* p) | |
{ | |
if (opts.position.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.position(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.position.paramInitValues.value); | |
} | |
if (opts.velocity.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.velocity(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.velocity.paramInitValues.value); | |
} | |
if (opts.acceleration.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.acceleration(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.acceleration.paramInitValues.value); | |
} | |
if (opts.color.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
///\todo should initialize by function! | |
memcpy(p, &opts.color.delta, sizeof(opts.color.delta)); | |
p += sizeof(opts.color.delta); | |
} | |
if (opts.color.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.color(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.color.paramInitValues.value); | |
} | |
if (opts.size.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
///\todo | |
memcpy(p, &opts.size.delta, sizeof(opts.size.delta)); | |
p += sizeof(opts.size.delta); | |
} | |
if (opts.size.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.size(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.size.paramInitValues.value); | |
} | |
if (opts.spawnTime.paramMode == PER_PARTICLE_VALUE) | |
{ | |
///\todo | |
float VAL = currentTime;///opts.spawnTime.paramInitValues.func(opts.spawnTime.paramInitValues); | |
std::size_t sizeOfVal = sizeof(VAL); | |
memcpy(p, &VAL, sizeOfVal); | |
p += sizeof(opts.spawnTime.paramInitValues.value); | |
} | |
if (opts.rotation.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
///\todo | |
memcpy(p, &opts.rotation.delta, sizeof(opts.rotation.delta)); | |
p += sizeof(opts.rotation.delta); | |
} | |
if (opts.rotation.paramMode == PER_PARTICLE_VALUE) | |
{ | |
auto VAL = emitter.rotation(); | |
memcpy(p, &VAL, sizeof(VAL)); | |
p += sizeof(opts.rotation.paramInitValues.value); | |
} | |
} | |
double getLifespan(ParticleSystemOptions& opts) | |
{ | |
///\todo | |
return opts.lifespan.paramInitValues.value; | |
} | |
///\todo multiple emitters, use this as an interface | |
void emitParticles(PSEmitter& emitter, double currentTime, ParticleSystemOptions& opts, unsigned int count, unsigned int MAX_RECYCLE = 0) | |
{ | |
int availableParticles = (maxParticles - currentParticles); | |
if ((availableParticles < count) && MAX_RECYCLE) | |
{ | |
handleExpiredParticles(currentTime, std::min<unsigned int>(MAX_RECYCLE, count)); | |
availableParticles = (maxParticles - currentParticles); | |
} | |
int adjustedCount = std::min<int>(availableParticles,count); | |
particlesFailedEmission += count - adjustedCount; | |
if (adjustedCount) | |
{ | |
for (int i =0; i < adjustedCount; i++) | |
{ | |
int newParticleIndex = i + currentParticles; | |
const float particleLifespan = getLifespan(opts); | |
double expirationTime = currentTime + particleLifespan; | |
shadowParticles[newParticleIndex] = expirationTime; | |
lifespanQueue.push(expirationTime); | |
std::size_t byteOffset = (newParticlesBatch + i) * vertexSizeBytes; | |
GLubyte* arrayBegin = &(newParticlesBuffer[byteOffset]); | |
createParticle(emitter, currentTime, opts, arrayBegin); | |
} | |
} | |
newParticlesBatch += adjustedCount; | |
} | |
void pushNewParticles() | |
{ | |
if (vertexSizeBytes) | |
{ | |
std::size_t offset = currentParticles * vertexSizeBytes; | |
std::size_t size = newParticlesBatch * vertexSizeBytes; | |
psBuffer.SubData(offset, size, newParticlesBuffer.data()); | |
currentParticles += newParticlesBatch; | |
newParticlesBatch = 0; | |
} | |
assert(currentParticles <= maxParticles); | |
} | |
void handleExpiredParticles(double currentTime, unsigned int maxKillCount=0) | |
{ | |
if (currentParticles) | |
{ | |
int expiredCt = countExpiredParticles(currentTime, maxKillCount); | |
if (expiredCt && vertexSizeBytes) | |
{ | |
GLubyte* mapped = (GLubyte*)psBuffer.Map(GL_WRITE_ONLY); | |
int handled = 0; | |
for (int i =0; /*i < currentParticles &&**/ handled < expiredCt; i++) | |
{ | |
if (shadowParticles[i] <= currentTime) | |
{ | |
currentParticles--; | |
GLubyte* backLoc = mapped + (currentParticles * vertexSizeBytes); | |
GLubyte* frontLoc = mapped + (i * vertexSizeBytes); | |
// swap back constant time array removal | |
memcpy(frontLoc, backLoc, vertexSizeBytes); | |
std::swap(shadowParticles[i], shadowParticles[currentParticles]); | |
handled++; | |
} | |
else | |
{ | |
/// std::clog << "skipping " << i << " with value " << shadowParticles[i] << std::endl; | |
} | |
} | |
psBuffer.Unmap(); | |
} | |
else // if no vertex size we do not actually need to update any of the buffers | |
{ | |
currentParticles -= expiredCt; | |
} | |
particlesReclaimedFrame += expiredCt; | |
} | |
else | |
{ | |
particlesReclaimedFrame += 0; | |
} | |
} | |
void setUniform(gl::Program& prog, const std::string& name, float val) | |
{ | |
prog.Uniform(name, val); | |
} | |
void setUniform(gl::Program& prog, const std::string& name, const std::array<float,2>& val) | |
{ | |
prog.Uniform(name, val[0], val[1]); | |
} | |
void setUniform(gl::Program& prog, const std::string& name, const std::array<float,3>& val) | |
{ | |
prog.Uniform(name, val[0], val[1], val[2]); | |
} | |
void setUniform(gl::Program& prog, const std::string& name, const std::array<float,4>& val) | |
{ | |
prog.Uniform(name, val[0], val[1], val[2], val[3]); | |
} | |
template <typename RealType, int Length> | |
void setUniform(gl::Program& prog, const ParticleParameter<RealType, Length>& param) | |
{ | |
if (param.paramMode == UNIFORM_VALUE) | |
{ | |
setUniform(prog,param.shaderName, param.paramInitValues.value); | |
} | |
} | |
template <typename RealType, int Length> | |
void setUniform(gl::Program& prog, const UpdatableParticleParameter<RealType, Length>& parm) | |
{ | |
if (parm.deltaParameterMode == UNIFORM_VALUE) | |
{ | |
setUniform(prog, "delta_" + parm.shaderName, parm.delta); | |
} | |
const ParticleParameter<RealType, Length>& p2 = parm; | |
setUniform(prog, p2); | |
} | |
void setUniforms(gl::Program& prog, const ParticleSystemOptions& opts) | |
{ | |
setUniform(prog, opts.position); | |
setUniform(prog, opts.velocity); | |
setUniform(prog, opts.acceleration); | |
setUniform(prog, opts.color); | |
setUniform(prog, opts.size); | |
setUniform(prog, opts.rotation); | |
setUniform(prog, opts.spawnTime); | |
} | |
void render(const ParticleSystemOptions& opts, | |
float globalTime, | |
float exposure, | |
float oneOverGamma, | |
float fovRadsY, | |
float screenHeight, | |
const Eigen::Matrix4f& mvp, | |
const Eigen::Vector3f& cameraPos) | |
{ | |
if (currentParticles) | |
{ | |
psProg.Use(); | |
setUniforms(psProg, opts); | |
psProg.Uniform("cameraPos", cameraPos[0], cameraPos[1], cameraPos[2]); | |
psProg.Uniform("time", globalTime); | |
psProg.Uniform("exposure", exposure); | |
psProg.Uniform("invGamma", oneOverGamma); | |
psProg.UniformMatrix<float, 4, 4>("mvp", mvp.data(), false, 1); | |
float toPixels = screenHeight / (2.0 * tan(.5 * fovRadsY)); | |
psProg.Uniform("toPixels", toPixels); | |
psVAO.Bind(); | |
if (opts.quadPrimitives) | |
{ | |
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, currentParticles); | |
} | |
else | |
{ | |
glDrawArrays(GL_POINTS, 0, currentParticles); | |
} | |
} | |
particlesRenderedFrame = currentParticles; | |
} | |
void glInit() | |
{ | |
glEnable(GL_PROGRAM_POINT_SIZE); | |
} | |
void initialize(const ParticleSystemOptions& opts) | |
{ | |
glInit(); | |
///std::clog << "PS FRAG SHADER SOURCE : " << psFrag2 << std::endl; | |
psProg = | |
Virtuoso::GL::Program({ | |
Virtuoso::GL::Shader(GL_VERTEX_SHADER,opts.vsSource), | |
Virtuoso::GL::Shader(GL_FRAGMENT_SHADER, opts.psSource) | |
}); | |
/*** Compute the strides of the relevant vertex attributes ***/ | |
psVAO.Bind(); | |
std::size_t attribIndex=0; | |
std::size_t positionStride=0; | |
std::size_t velocityStride=0; | |
std::size_t accelerationStride=0; | |
std::size_t deltaColorStride=0; | |
std::size_t colorStride=0; | |
std::size_t deltaSizeStride=0; | |
std::size_t sizeStride=0; | |
std::size_t spawnTimeStride=0; | |
std::size_t currentStride=0; | |
std::size_t deltaRotationStride=0; | |
std::size_t rotationStride=0; | |
if (opts.position.paramMode == PER_PARTICLE_VALUE) | |
{ | |
currentStride += sizeof(opts.position.paramInitValues.value); | |
} | |
if (opts.velocity.paramMode == PER_PARTICLE_VALUE) | |
{ | |
velocityStride=currentStride; | |
currentStride += sizeof(opts.velocity.paramInitValues.value); | |
} | |
if (opts.acceleration.paramMode == PER_PARTICLE_VALUE) | |
{ | |
accelerationStride = currentStride; | |
currentStride += sizeof(opts.acceleration.paramInitValues.value); | |
} | |
if (opts.color.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
deltaColorStride = currentStride; | |
currentStride += sizeof(opts.color.delta); | |
} | |
if (opts.color.paramMode == PER_PARTICLE_VALUE) | |
{ | |
colorStride = currentStride; | |
currentStride += sizeof(opts.color.paramInitValues.value); | |
} | |
if (opts.size.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
deltaSizeStride = currentStride; | |
currentStride += sizeof(opts.size.delta); | |
} | |
if (opts.size.paramMode == PER_PARTICLE_VALUE) | |
{ | |
sizeStride = currentStride; | |
currentStride += sizeof(opts.size.paramInitValues.value); | |
} | |
if (opts.spawnTime.paramMode == PER_PARTICLE_VALUE) | |
{ | |
spawnTimeStride = currentStride; | |
currentStride += sizeof(opts.spawnTime.paramInitValues.value); | |
} | |
if (opts.rotation.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
deltaRotationStride = currentStride; | |
currentStride += sizeof(opts.rotation.delta); | |
} | |
if (opts.rotation.paramMode == PER_PARTICLE_VALUE) | |
{ | |
rotationStride = currentStride; | |
currentStride += sizeof(opts.rotation.paramInitValues.value); | |
} | |
vertexSizeBytes = currentStride; | |
/***** Initialize VBO *****/ | |
if (opts.position.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.position.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, positionStride); | |
} | |
if (opts.velocity.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.velocity.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, velocityStride); | |
} | |
if (opts.acceleration.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.acceleration.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, accelerationStride); | |
} | |
if (opts.color.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.color.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, deltaColorStride); | |
} | |
if (opts.color.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.color.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, colorStride); | |
} | |
if (opts.size.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.size.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, deltaSizeStride); | |
} | |
if (opts.size.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.size.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, sizeStride); | |
} | |
if (opts.spawnTime.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.spawnTime.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, spawnTimeStride); | |
} | |
if (opts.rotation.deltaParameterMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.rotation.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, deltaRotationStride); | |
} | |
if (opts.rotation.paramMode == PER_PARTICLE_VALUE) | |
{ | |
psVAO.Attrib(attribIndex++, opts.rotation.Elements, GL_FLOAT, GL_FALSE, vertexSizeBytes, psBuffer, rotationStride); | |
} | |
std::size_t vboSize = maxParticles * vertexSizeBytes; | |
psBuffer.Data(vboSize, nullptr, GL_STATIC_DRAW); | |
GLfloat quadData[16] = {-1.0f, 1.0f, //pos | |
0.0, 1.0, //uv | |
-1.0f, -1.0f, //pos | |
0.0f, 0.0f, //uv | |
1.0f, 1.0f, // pos | |
1.0f, 1.0f, //uv | |
1.0f, -1.0f, //pos | |
1.0f, 0.0f // uv | |
}; | |
quadBuffer.Data(16 * sizeof(GLfloat), quadData, GL_STATIC_DRAW); | |
attribCount = attribIndex; | |
psVAO.Attrib(attribIndex, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GL_FLOAT), quadBuffer, 0); | |
psVAO.Attrib(attribIndex+1, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GL_FLOAT), quadBuffer, 2 * sizeof(GL_FLOAT)); | |
updateInstancing(opts); | |
} | |
void updateInstancing(const ParticleSystemOptions& opts) | |
{ | |
psVAO.Bind(); | |
if (opts.quadPrimitives) | |
{ | |
for (int i=0; i< attribCount; i++) | |
{ | |
glVertexAttribDivisor(i, 1); | |
} | |
} | |
else | |
{ | |
for (int i=0; i< attribCount; i++) | |
{ | |
glVertexAttribDivisor(i, 0); | |
} | |
} | |
} | |
}; | |
std::string printVal(float v) | |
{ | |
std::stringstream sstr; | |
sstr<<std::fixed; | |
sstr.precision(9); | |
sstr << v; | |
return sstr.str(); | |
} | |
std::string printVal(const std::array<float, 2>& val) | |
{ | |
std::stringstream sstr; | |
sstr<<std::fixed; | |
sstr.precision(9); | |
sstr << "vec2(" << val[0] <<','<<val[1]<<")"; | |
return sstr.str(); | |
} | |
std::string printVal(const std::array<float, 3>& val) | |
{ | |
std::stringstream sstr; | |
sstr<<std::fixed; | |
sstr.precision(9); | |
sstr << "vec3(" << val[0] <<','<<val[1]<<','<<val[2]<<")"; | |
return sstr.str(); | |
} | |
std::string printVal(const std::array<float, 4>& val) | |
{ | |
std::stringstream sstr; | |
sstr<<std::fixed; | |
sstr.precision(9); | |
sstr << "vec4(" << val[0] <<','<<val[1]<<','<<val[2]<<','<<val[3]<<")"; | |
return sstr.str(); | |
} | |
template <typename RealType, int Length> | |
std::string particleParamString(const UpdatableParticleParameter<RealType, Length>& param, int& attribCounter) | |
{ | |
std::string deltaStr; | |
const std::string& name = "delta_" + param.shaderName; | |
std::string vecType[4] = {"float", "vec2", "vec3", "vec4"}; | |
const std::string& type = vecType[Length-1]; | |
std::string define = param.shaderName; | |
for (auto & c: define) c = toupper(c); | |
define = "#define DELTA_" + define; | |
switch (param.updateMode) | |
{ | |
case UPDATE_DISABLED: | |
break; | |
case UPDATE_LINEAR: | |
define += "_LINEAR\n"; | |
switch (param.deltaParameterMode) | |
{ | |
case CONSTANT_VALUE: | |
{ | |
typename ParticleParameterInitializationValues<RealType, Length>::ValueType val = param.delta; | |
deltaStr = define + std::string("const ") + type + ' ' + name + " = " + printVal(val) + ';' + "\n"; | |
break; | |
} | |
case UNIFORM_VALUE: | |
{ | |
deltaStr = define + std::string("uniform ") + type + ' ' + name + ';' + "\n"; | |
break; | |
} | |
case PER_PARTICLE_VALUE: | |
{ | |
const char* indexString[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; | |
deltaStr = define + std::string ("layout (location=") + indexString[attribCounter++] + ") in " + type + ' ' + name + ';' + "\n"; | |
break; | |
} | |
default: | |
{ | |
throw std::runtime_error("Unsupported param mode"); | |
return ""; | |
} | |
} | |
break; | |
default: | |
throw std::runtime_error("Unsupported delta mode for particle system parameter"); | |
} | |
const ParticleParameter<RealType, Length>& val = param; | |
return deltaStr + particleParamString(val, attribCounter); | |
} | |
template <typename RealType, int Length> | |
std::string particleParamString(const ParticleParameter<RealType, Length>& param, int& attribCounter) | |
{ | |
assert(Length < 5); | |
assert(Length); | |
assert(typeid(RealType) == typeid(float)); | |
const std::string& name = param.shaderName; | |
std::string vecType[4] = {"float", "vec2", "vec3", "vec4"}; | |
const std::string& type = vecType[Length-1]; | |
switch (param.paramMode) | |
{ | |
case CONSTANT_VALUE: | |
{ | |
typename ParticleParameterInitializationValues<RealType, Length>::ValueType val = param.paramInitValues.value; | |
return std::string("const ") + type + ' ' + name + " = " + printVal(val) + ';'; | |
break; | |
} | |
case UNIFORM_VALUE: | |
{ | |
return std::string("uniform ") + type + ' ' + name + ';'; | |
break; | |
} | |
case PER_PARTICLE_VALUE: | |
{ | |
const char* indexString[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; | |
return std::string ("layout (location=") + indexString[attribCounter++] + ") in " + type + ' ' + name + ';'; | |
break; | |
} | |
default: | |
{ | |
std::clog << "Error with " << param.shaderName << " and value " << (int)param.paramMode<<std::endl; | |
throw std::runtime_error("Unsupported param mode"); | |
return ""; | |
} | |
} | |
} | |
std::string makePSFragShader(const ParticleSystemOptions& opts) | |
{ | |
const std::string psFragHeader = | |
"uniform float invGamma;\n" | |
"uniform float exposure;\n\n" | |
"in vec3 col;\n" | |
"out vec4 colorOut;\n"; | |
std::string tcString; | |
switch(opts.textureMode) | |
{ | |
case TEXTURE_DISABLED: | |
break; | |
case TEXTURE_2D_ATLAS: | |
case TEXTURE_2D: | |
tcString = | |
"in vec2 texCoords;\n\n" | |
"uniform sampler2D tex;\n"; | |
break; | |
case TEXTURE_2D_ARRAY: | |
tcString = | |
"in vec3 texCoords;\n\n" | |
"uniform sampler2DArray tex;\n"; | |
break; | |
default: | |
throw std::runtime_error("Unknown texture mode in HOPS Frag Shader generator"); | |
} | |
std::string psFrag2(HOPS_GLSL_VERSION_STRING); | |
psFrag2 += "\n" + psFragHeader + tcString; | |
if (opts.textureMode != TEXTURE_DISABLED) | |
{ | |
///\todo fix me - color texture | |
psFrag2 += | |
"\nfloat texLookup()\n" | |
"{\n" | |
"\treturn texture(tex, texCoords).r;\n" | |
"}\n"; | |
} | |
psFrag2 += "\nvoid main(void)\n{\n"; | |
if (opts.textureMode != TEXTURE_DISABLED) | |
{ | |
psFrag2 += "\tvec3 linearColor = col * exposure * texLookup();\n"; | |
} | |
else | |
{ | |
psFrag2 += "\tvec3 linearColor = col * exposure;\n"; | |
} | |
psFrag2 += | |
"\n\tvec3 exposed = linearColor / (vec3(1.0) + linearColor);\n" | |
"\n\tcolorOut = vec4(pow(exposed, vec3(invGamma)), 1.0);\n}"; | |
std::clog << "\n\nFRAG SHADER " << psFrag2 << std::endl; | |
return psFrag2; | |
} | |
const std::string& velocityAlignmentOffsetUtil() | |
{ | |
static const std::string& rval = R"STRING( | |
/// used for screen aligned, velocity aligned rectangle particles | |
vec2 velocityAlignmentOffset(float ltime, float lsize, vec3 posWorld) | |
{ | |
vec3 velT = normalize(velocity + acceleration * ltime); | |
vec4 endPos = mvp * vec4(posWorld, 1.0); | |
vec4 startPos = mvp * vec4(posWorld - velT * 2.0 * lsize, 1.0); | |
endPos /= endPos.w; | |
startPos /= startPos.w; | |
vec2 screenSpaceVelocity = (endPos.xy - startPos.xy); | |
vec2 screenSpaceVelocityN = normalize(screenSpaceVelocity); | |
///vec4 mvpprod = (mvp * vec4(velT, 1.0)); | |
///vec2 screenSpaceVelocity = mvpprod.xy;// / mvpprod.w; | |
vec2 screenSpaceVelocityPerp = vec2(-screenSpaceVelocityN.y, screenSpaceVelocityN.x); | |
float velScale = 10.0; | |
float maxLength = min(.05, length(screenSpaceVelocity)); | |
screenSpaceVelocity = normalize(screenSpaceVelocity) * maxLength; | |
//2.0 * lsize * | |
// we are scaling in post transform space but because of scalar multiply rule this should be ok | |
// the two is hardcoded for now. should be input as vector size. | |
// also the double .5 scale | |
///vec2 billboardXAxisOffset = - 5. * lsize * screenSpaceVelocity * (1.0 - quadTC.x); | |
///vec2 billboardYAxisOffset = .5 * lsize * screenSpaceVelocityPerp * quadPosition.y; | |
vec2 billboardXAxisOffset = -velScale * screenSpaceVelocity * (1.0 - quadTC.x); | |
vec2 billboardYAxisOffset = .5 * lsize * screenSpaceVelocityPerp * quadPosition.y; | |
return billboardXAxisOffset + billboardYAxisOffset; | |
} | |
)STRING"; | |
return rval; | |
} | |
std::string particleSizeString(const ParticleSystemOptions& options) | |
{ | |
switch (options.size.updateMode) | |
{ | |
case UPDATE_LINEAR: | |
return "\tfloat lsize = size + delta_size * ltime;"; | |
case UPDATE_DISABLED: | |
return "\tconst float lsize = size;"; | |
default: | |
throw std::runtime_error("unknown parameter update mode :: particleSizeString"); | |
} | |
} | |
std::string particleColorString(const ParticleSystemOptions& options) | |
{ | |
switch (options.color.updateMode) | |
{ | |
case UPDATE_LINEAR: | |
return "\tcol = color + delta_color * ltime;\n"; | |
case UPDATE_DISABLED: | |
return "\tcol = color;\n"; | |
default: | |
throw std::runtime_error("unknown parameter update mode :: particleColorString"); | |
} | |
} | |
bool rotationSupported(const ParticleSystemOptions& options) | |
{ | |
bool rotateAlwaysZero = | |
(options.rotation.paramInitValues.value == 0.0) | |
&& (options.rotation.paramMode == CONSTANT_VALUE); | |
bool noUpdates = options.rotation.updateMode == UPDATE_DISABLED; | |
bool updateIsZero = (options.rotation.delta == 0.0f) && | |
(options.rotation.deltaParameterMode == CONSTANT_VALUE); | |
bool geometryOK = options.quadPrimitives && !options.velocityAlignedParticles; | |
return !(rotateAlwaysZero && (noUpdates || updateIsZero)); | |
} | |
std::string particleRotString(const ParticleSystemOptions& options) | |
{ | |
if (!rotationSupported(options)) | |
{ | |
return ""; | |
} | |
switch (options.rotation.updateMode) | |
{ | |
case UPDATE_LINEAR: | |
return "\tfloat rot = rotation + delta_rotation * ltime;\n" | |
"\tvec2 trigVec = vec2(cos(rot), sin(rot));\n" | |
; | |
case UPDATE_DISABLED: | |
return "\tconst float rot = rotation;\n" | |
"\tvec2 trigVec = vec2(cos(rot), sin(rot));\n" | |
; | |
default: | |
throw std::runtime_error("unknown parameter update mode :: particleRotString"); | |
} | |
} | |
std::string pointSpriteFooter(const ParticleSystemOptions& options) | |
{ | |
return "\tfloat z = gl_Position.w; // -z coordinate in camera coords, but looking down -z axis.\n" | |
"\tgl_PointSize = (lsize * toPixels) / z;\n" | |
"\tgl_Position = gl_PointSize > .50 ? gl_Position : vec4(100.0);\n"; | |
} | |
std::string cameraFacingQuadFooter(const ParticleSystemOptions& options) | |
{ | |
return R"STRING(///\todo quad primitives | |
///float scale = .5 * (lsize) / pos.z; ///\todo still need the tan fov stuff i think | |
float scale = .5 * lsize; | |
vec3 pZ = normalize(cameraPos-pos); | |
vec3 pUpTmp = vec3(0., 1., 0.); | |
vec3 pRight = cross(pUpTmp, pZ); | |
vec3 pUp = cross(pZ, pRight); | |
/*** | |
vec3 pZ = normalize(-pos); | |
vec3 pRightTmp = vec3(1.0, 0.0, 0.0); | |
vec3 pUp = cross(pZ, pRightTmp); | |
vec3 pRight = cross(pUp, pZ); | |
***/ | |
vec2 quadPosition2; | |
quadPosition2.x = dot(trigVec, quadPosition.xy); | |
quadPosition2.y = dot(vec2(-trigVec.y, trigVec.x), quadPosition.xy); | |
pos += (scale * quadPosition2.x) * pRight; | |
pos += (scale * quadPosition2.y) * pUp; | |
gl_Position = mvp * vec4(pos,1.0); | |
/// pos.x = dot(vec2(cos(rot), sin(rot)), pos.xy); | |
/// pos.y = dot(vec2(-sin(rot), cos(rot)), pos.xy); | |
)STRING"; | |
} | |
std::string velocityAlignedQuadFooter(const ParticleSystemOptions& options) | |
{ | |
return | |
"\n\tvec4 screenSpacePosition = mvp * vec4(pos, 1.0);\n" | |
"\tscreenSpacePosition /= screenSpacePosition.w;\n" | |
"\n\tvec4 outPos = screenSpacePosition;\n" | |
"\toutPos.xy += velocityAlignmentOffset(ltime, lsize, pos);\n" | |
"\n\tgl_Position = outPos;\n" | |
; | |
} | |
std::string particleGeometryString(const ParticleSystemOptions& options) | |
{ | |
if (options.quadPrimitives) | |
{ | |
if (options.velocityAlignedParticles) | |
{ | |
return velocityAlignedQuadFooter(options); | |
} | |
else | |
{ | |
return cameraFacingQuadFooter(options); | |
} | |
} | |
else | |
{ | |
return pointSpriteFooter(options); | |
} | |
} | |
std::string particlePosString(const ParticleSystemOptions& options) | |
{ | |
std::string rval; | |
if (!options.useResistanceValue) | |
{ | |
rval += "\tvec3 pos = position + velocity * ltime + .5 * acceleration * tsq;\n"; | |
} | |
else | |
{ | |
rval += "\n\tconst float k1 = 1.0 / resistance;\n"; | |
rval += "\tconst float k2 = resistance;\n"; | |
rval += "\tvec3 pos = position + k1 * (acceleration * ltime + -exp(-k2 * ltime) \n* (velocity - acceleration * k1) + -acceleration *k1 + velocity);\n\n"; | |
} | |
return rval; | |
} | |
inline bool needsCameraPosition(const ParticleSystemOptions& opts) | |
{ | |
return opts.quadPrimitives && !opts.velocityAlignedParticles; | |
} | |
std::string makePSVertexShaderHeader(const ParticleSystemOptions& options) | |
{ | |
std::string rval = | |
"uniform mat4 mvp;\n" | |
"uniform float time;\n"; | |
if (needsCameraPosition(options)) | |
{ | |
rval += "uniform vec3 cameraPos;\n"; | |
} | |
if (!options.quadPrimitives) | |
{ | |
rval += "uniform float toPixels;\n"; | |
} | |
rval += "\nout vec3 col;\n"; | |
return rval; | |
} | |
std::string makePSVertexShader(const ParticleSystemOptions& options) | |
{ | |
int attribCounter=0; | |
const std::string& header = makePSVertexShaderHeader(options); | |
std::string rval(HOPS_GLSL_VERSION_STRING); | |
rval += "\n" + header; | |
switch (options.textureMode) | |
{ | |
case TEXTURE_DISABLED: | |
break; | |
case TEXTURE_2D: | |
case TEXTURE_2D_ATLAS: | |
rval += "out vec2 texCoords;\n"; | |
break; | |
case TEXTURE_2D_ARRAY: | |
rval += "out vec3 texCoords;\n"; | |
break; | |
default: | |
throw std::runtime_error("Unsupported texture mode making out param texCoords"); | |
} | |
rval += "\n"; | |
rval += particleParamString(options.position, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.velocity, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.acceleration, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.color, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.size, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.spawnTime, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.resistance, attribCounter); | |
rval += '\n'; | |
rval += particleParamString(options.rotation, attribCounter); | |
rval += '\n'; | |
std::string indexString[] = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"}; | |
if (options.quadPrimitives) | |
{ | |
rval += "layout (location = " + indexString[attribCounter] + ") in vec2 quadPosition;\n"; | |
rval += "layout (location = " + indexString[attribCounter+1] + ") in vec2 quadTC;\n"; | |
} | |
if (options.velocityAlignedParticles) | |
{ | |
rval +=velocityAlignmentOffsetUtil(); | |
} | |
rval += "\n\nvoid main()\n{\n\tfloat ltime = time - spawnTime;\n\tfloat tsq = ltime*ltime;\n"; | |
if (options.textureMode != TEXTURE_DISABLED) | |
{ | |
rval += "\n\ttexCoords = quadTC;\n"; | |
} | |
rval += '\n'+particlePosString(options); | |
rval += '\n'+particleColorString(options); | |
rval += '\n'+particleRotString(options); | |
rval += '\n'+particleSizeString(options); | |
rval += '\n'+particleGeometryString(options); | |
rval += "}"; | |
std::clog << " Vertex shader : \n " << rval << std::endl; | |
return rval; | |
} | |
#ifdef IMGUI_VERSION | |
void guiSetter(const std::string& name, float& value) | |
{ | |
float oldval = value; | |
ImGui::InputFloat(name.c_str(), &value); | |
} | |
void guiSetter(const std::string& name, std::array<float, 2>& value) | |
{ | |
ImGui::InputFloat2(name.c_str(), &value[0]); | |
} | |
void guiSetter(const std::string& name, std::array<float, 3>& value) | |
{ | |
ImGui::InputFloat3(name.c_str(), &value[0]); | |
} | |
void guiSetter(const std::string& name, std::array<float, 4>& value) | |
{ | |
ImGui::InputFloat4(name.c_str(), &value[0]); | |
} | |
template<typename RealType, int Length=1> | |
void psParamConfigBody(ParticleParameter<RealType, Length>& param) | |
{ | |
const char* ParameterModeStrings[] = {"Constant Value", "Uniform Value", "Value Per Particle"}; | |
ImGui::ListBox("Value Type", (int*)¶m.paramMode, ParameterModeStrings, (int)MAX_PARAMETER_MODES, (int)MAX_PARAMETER_MODES); | |
guiSetter(param.shaderName + " value", param.paramInitValues.value); | |
} | |
template<typename RealType, int Length=1> | |
void psParamConfig(UpdatableParticleParameter<RealType, Length>& param) | |
{ | |
if (ImGui::CollapsingHeader(param.shaderName.c_str())) | |
{ | |
psParamConfigBody(param); | |
const char* ParameterUpdateModeStrings[] = {"Static Value", "Linear Update"}; | |
const char* ParameterModeStrings[] = {"Constant Value", "Uniform Value", "Value Per Particle"}; | |
//deltaParameterMode | |
ImGui::ListBox("Parameter Update Mode ", (int*)¶m.updateMode, ParameterUpdateModeStrings, (int)MAX_UPDATE_MODES, (int)MAX_UPDATE_MODES); | |
ImGui::ListBox("Delta Value Type", (int*)¶m.deltaParameterMode, ParameterModeStrings, (int)MAX_PARAMETER_MODES, (int)MAX_PARAMETER_MODES); | |
guiSetter("Delta-" +param.shaderName, param.delta); | |
} | |
} | |
template<typename RealType, int Length=1> | |
void psParamConfig(ParticleParameter<RealType, Length>& param) | |
{ | |
if (ImGui::CollapsingHeader(param.shaderName.c_str())) | |
{ | |
psParamConfigBody(param); | |
} | |
} | |
void psGUI(ParticleSystemOptions& opts, PSRenderer& render) | |
{ | |
if(ImGui::CollapsingHeader("Particle System Config")) | |
{ | |
ImGui::Checkbox("Apply Resistance", &opts.useResistanceValue); | |
ImGui::Checkbox("Velocity Aligned Particle Test", &opts.velocityAlignedParticles); | |
bool quad=opts.quadPrimitives; | |
ImGui::Checkbox("Quad Primitives", &opts.quadPrimitives); | |
if (quad != opts.quadPrimitives) | |
{ | |
opts.vsSource = makePSVertexShader(opts); | |
opts.psSource = makePSFragShader(opts); | |
render.psProg = Virtuoso::GL::Program( | |
{ | |
Virtuoso::GL::Shader(GL_VERTEX_SHADER,opts.vsSource), | |
Virtuoso::GL::Shader(GL_FRAGMENT_SHADER, opts.psSource) | |
} | |
); | |
render.updateInstancing(opts); | |
} | |
if (ImGui::CollapsingHeader("Texture Options")) | |
{ | |
const char* TextureOptionsStrings[] = {"No Texture", "2D Texture", "2D Texture W/ Atlas", "2D Texture Array"}; | |
///const char* ParameterInitializationStrings[] = {"Constant Init", "Init from emitter function"}; | |
ImGui::ListBox("Texture Mode", (int*)&opts.textureMode, TextureOptionsStrings, (int)MAX_TEXTURE_MODE, (int)MAX_TEXTURE_MODE); | |
} | |
psParamConfig(opts.position); | |
psParamConfig(opts.velocity); | |
psParamConfig(opts.acceleration); | |
psParamConfig(opts.color); | |
psParamConfig(opts.size); | |
psParamConfig(opts.spawnTime); | |
psParamConfig(opts.lifespan); | |
psParamConfig(opts.rotation); | |
if (opts.useResistanceValue) | |
{ | |
psParamConfig(opts.resistance); | |
} | |
if (ImGui::Button("Rebuild")) | |
{ | |
opts.vsSource = makePSVertexShader(opts); | |
opts.psSource = makePSFragShader(opts); | |
render.psProg = Virtuoso::GL::Program( | |
{ | |
Virtuoso::GL::Shader(GL_VERTEX_SHADER,opts.vsSource), | |
Virtuoso::GL::Shader(GL_FRAGMENT_SHADER, opts.psSource) | |
} | |
); | |
/// render.initialize(opts); | |
render.updateInstancing(opts); | |
} | |
ImGui::TextUnformatted(opts.vsSource.c_str()); | |
std::stringstream sstr; | |
ImGui::Text("Particles: %d\n Expired Particles %d\n Failed Particles %d", render.currentParticles, render.particlesReclaimedFrame, render.particlesFailedEmission); | |
} | |
} | |
#endif // IMGUI_VERSION | |
} //namespace HOPS | |
} // namespace VIRTUOSO | |
#endif /* VIRTUOSO_HOPS_h */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment