Skip to content

Instantly share code, notes, and snippets.

@VirtuosoChris
Created May 2, 2017 07:16
Show Gist options
  • Save VirtuosoChris/6e0d163e76a8d42fd7b5084d2e24cdfc to your computer and use it in GitHub Desktop.
Save VirtuosoChris/6e0d163e76a8d42fd7b5084d2e24cdfc to your computer and use it in GitHub Desktop.
SHOPS - Stateless Header Only Particle System - VS based, part of HOPS library.
//
// 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*)&param.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*)&param.updateMode, ParameterUpdateModeStrings, (int)MAX_UPDATE_MODES, (int)MAX_UPDATE_MODES);
ImGui::ListBox("Delta Value Type", (int*)&param.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