Hi Simon,
I will give what you said a go, cheers!
I tried to answer as detailed as possible below, so added a 4. Please excuse the bit of a code dump and general state of code - learning graphics, not programming an’ all
2. GPU performs calcs on huge 1D array (vbo) based on initial position and some other parameters from CPU
(particleUpdate.vs vertex shader)
#version 150 core
uniform float uElapsedSeconds;
uniform vec3 uMousePos;
uniform bool uMouseDown;
uniform bool uCTRLDown;
uniform int uToolMode;
uniform float uBrushRadius;
uniform float uBrushLevel; // how much to offset at center of radius (mouse point)
uniform float uBrushFeather;
uniform int uBrushFeatherProfile;
uniform float uLevelValue;
uniform float uMaxVertexDepthZ;
uniform float uMinVertexDepthZ;
uniform bool uDepthRenderMode;
uniform float uDepthRenderModeMinVertexDepthZ;
uniform float uDepthRenderModeMaxVertexDepthZ;
uniform float uFlattenTargetValue;
in vec3 iPosition;
in vec4 iColor;
in vec3 iHomePosition;
in vec4 iHomeColor;
out vec3 position;
out vec4 color;
out vec3 homePosition;
out vec4 homeColor;
float easeInOutSine(float t) {
return -0.5 * (cos(3.141592653589793 * t) - 1.0);
}
float easeInOutQuad(float t) {
float p = 2.0 * t * t;
return t < 0.5 ? p : -p + (4.0 * t) - 1.0;
}
float easeInOutCubic(float t) {
return t < 0.5
? 4.0 * t * t * t
: 0.5 * pow(2.0 * t - 2.0, 3.0) + 1.0;
}
float easeInOutQuart(float t) {
return t < 0.5
? +8.0 * pow(t, 4.0)
: -8.0 * pow(t - 1.0, 4.0) + 1.0;
}
float easeInOutQuint(float t) {
return t < 0.5
? +16.0 * pow(t, 5.0)
: -0.5 * pow(2.0 * t - 2.0, 5.0) + 1.0;
}
float easeInOutExpo(float t) {
return t == 0.0 || t == 1.0
? t
: t < 0.5
? +0.5 * pow(2.0, (20.0 * t) - 10.0)
: -0.5 * pow(2.0, 10.0 - (t * 20.0)) + 1.0;
}
float easeInOutCirc(float t) {
return t < 0.5
? 0.5 * (1.0 - sqrt(1.0 - 4.0 * t * t))
: 0.5 * (sqrt((3.0 - 2.0 * t) * (2.0 * t - 1.0)) + 1.0);
}
float easeNone(float t) {
return t;
}
float ease(float t)
{
if (uBrushFeatherProfile == 1)
return easeInOutSine(t);
else if (uBrushFeatherProfile == 2)
return easeInOutQuad(t);
else if (uBrushFeatherProfile == 3)
return easeInOutCubic(t);
else if (uBrushFeatherProfile == 4)
return easeInOutQuart(t);
else if (uBrushFeatherProfile == 5)
return easeInOutQuint(t);
else if (uBrushFeatherProfile == 6)
return easeInOutExpo(t);
else if (uBrushFeatherProfile == 7)
return easeInOutCirc(t);
// no easing
return t;
}
void main()
{
// get distance from particle to mouse cursor
float distance = length(uMousePos - iPosition.xyz);
// get distance from particle to mouse cursor
vec3 camaraAtZ0ToVertex = iPosition;
camaraAtZ0ToVertex.z = 0;
float distMouseAtZ0ToVertex = length(uMousePos - camaraAtZ0ToVertex.xyz);
vec3 pos = iPosition;
vec4 col = iColor;
if (distMouseAtZ0ToVertex < uBrushRadius )
{
if(uMouseDown)
{
// SCULPT tool
if (uToolMode == 0)
{
// Feather
// ease produces number between 0 and 1 indicating how far into the brush this vertex is.
float totalFeatheredRadius = uBrushRadius-uBrushFeather;
float vertexProportion = (uBrushRadius-distMouseAtZ0ToVertex) / totalFeatheredRadius;
float deltaZ = uBrushLevel * ease(vertexProportion);
if (uCTRLDown)
{
// lower
pos.z += deltaZ;
}
else
{
// raise
pos.z -= deltaZ;
}
}
// LEVEL tool
else if(uToolMode == 1)
{
pos.z = -uLevelValue; // Z is going from - to + as you go into the screen1
}
// RESET tool
else if(uToolMode == 2)
{
pos = iHomePosition;
}
// FLATTEN tool
else if(uToolMode == 3)
{
// Feather
// ease produces number between 0 and 1 indicating how far into the brush this vertex is.
float totalFeatheredRadius = uBrushRadius-uBrushFeather;
float vertexProportion = (uBrushRadius-distMouseAtZ0ToVertex) / totalFeatheredRadius;
float deltaZ = uBrushLevel * ease(vertexProportion);
// which direction are we going relative to the flattenTargetValue value?
if (pos.z > uFlattenTargetValue)
{
pos.z -= deltaZ;
}
if (pos.z < uFlattenTargetValue)
{
pos.z += deltaZ;
}
}
}
// highlight in depth render mode
if (uDepthRenderMode)
{
float Input = pos.z;
float InputHigh = -uDepthRenderModeMaxVertexDepthZ;
float InputLow = -uDepthRenderModeMinVertexDepthZ;
float OutputHigh = 1;
float OutputLow = 0;
float depthScale = ((Input - InputLow) / (InputHigh - InputLow)) * (OutputHigh - OutputLow) + OutputLow;
if (depthScale > 0.8) // clamp so we are always able to highlight brush
depthScale = 0.8;
col.x = depthScale + 0.2;
col.y = depthScale;
col.z = depthScale;
col.w = 1.0;
}
// highlight normal colour
else
{
float brushOuterBorderWidth = uBrushRadius/10.0;
float brushInnerBorderWidth = uBrushRadius/20.0;
float midpoint = uBrushRadius/2.0;
float innerBorderMin = midpoint-(brushInnerBorderWidth/2.0);
float innerBorderMax = midpoint+(brushInnerBorderWidth/2.0);
// Outer Yellow Border 1/10 the brush size
if (distMouseAtZ0ToVertex > uBrushRadius-brushOuterBorderWidth)
{
col.x = 0.7;
col.y = 0.7;
col.z = 0.0;
col.a = 0.5;
}
// Inner border (half way where easing takes effect)
else if (uBrushFeatherProfile != 0 && // only show when easing enabled
(distMouseAtZ0ToVertex > innerBorderMin) &&
(distMouseAtZ0ToVertex < innerBorderMax))
{
col.x = 0.0;
col.y = 0.0;
col.z = 0.7;
col.a = 0.5;
}
else
{
col = iHomeColor; // reset brush interior colour to original image colour
}
}
}
else
{
// only oscillate vertices if not under mouse brush
// TODO: scale osicaltions to distance
float oFactor = 1.0;
float phase = uElapsedSeconds;
float offset = (pos.x + (pos.y * 0.05 )) * 6.5;
pos.z += (sin(phase+offset ) * 0.2);
// if outside brush radius, color is normal colour
if (uDepthRenderMode)
{
// ...unless in depth mode
float Input = pos.z;
float InputHigh = -uDepthRenderModeMaxVertexDepthZ;
float InputLow = -uDepthRenderModeMinVertexDepthZ;
float OutputHigh = 1;
float OutputLow = 0;
float depthScale = ((Input - InputLow) / (InputHigh - InputLow)) * (OutputHigh - OutputLow) + OutputLow;
col.x = depthScale;
col.y = depthScale;
col.z = depthScale;
col.w = 1.0;
}
else
{
col = iHomeColor;
}
}
position = pos;
color = col;
homePosition = iHomePosition;
homeColor = iHomeColor;
}
3. CPU reads back either all or part of it in order to work out how to manipulate variable the GPU.
(This happens when the user eg selects the flatten tool)
pickedPoint = x,y intersection of ray from mouse position on camera plane, with the Z=0 plane
m3DImage = representing a content viewer (eg image)
mSelectedVertexStats = representing the selected
int br = getCurrentBrushRadius();
int numBrushVertices = br*br;
vec2 pp = vec2(pickedPoint.x, pickedPoint.y) - vec2(m3DImage->getCurrentPosition().x, m3DImage->getCurrentPosition().y);
if (pp.x < -br)
{
mSelectedVertexStats.min = -1;
mSelectedVertexStats.max = -1;
mSelectedVertexStats.avg = -1;
return;
}
if (pp.y < -br)
{
mSelectedVertexStats.min = -1;
mSelectedVertexStats.max = -1;
mSelectedVertexStats.avg = -1;
return;
}
if (pp.x > m3DImage->getWidth()+br)
{
mSelectedVertexStats.min = -1;
mSelectedVertexStats.max = -1;
mSelectedVertexStats.avg = -1;
return;
}
if (pp.y > m3DImage->getHeight()+br)
{
mSelectedVertexStats.min = -1;
mSelectedVertexStats.max = -1;
mSelectedVertexStats.avg = -1;
return;
}
float x = pp.x;
float y = pp.y;
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x > m3DImage->getWidth()-br) x = m3DImage->getWidth()-br;
if (y > m3DImage->getHeight()-br) y = m3DImage->getHeight()-br;
int offset = (y*m3DImage->getWidth());
int numVerticesToRead = m3DImage->getWidth()*br;
Particle *particleData = (Particle*)mParticleBuffer[0]->mapBufferRange(offset * sizeof(Particle), numVerticesToRead * sizeof(Particle), GL_MAP_READ_BIT);
if (particleData != nullptr)
{
// get the latest min/max from the data read
float minValueZ = DBL_MAX;
float maxValueZ = -DBL_MAX;
double avg = 0;
int numHighlightedVertices = 0;
for (int vertexIndex = 0; vertexIndex < numVerticesToRead; vertexIndex++)
{
float height = particleData->currentPos.z;
// discount vertices that are not within brush radius
// get distance from vertex to mouse cursor
float distance = length(vec2(pickedPoint.x , pickedPoint.y) - vec2(particleData->currentPos.x, particleData->currentPos.y));
if (distance < mBrushRadiusStrokeStart)
{
avg += (double)height;
if (height < minValueZ)
{
minValueZ = height;
}
if (height > maxValueZ)
{
maxValueZ = height;
}
numHighlightedVertices++;
}
particleData++;
}
mSelectedVertexStats.min = minValueZ;
mSelectedVertexStats.max = maxValueZ;
mSelectedVertexStats.avg = avg / numHighlightedVertices;
}
mParticleBuffer[0]->unmap();
}
4. I have the vertex and fragment shader setup in code as follows.
mRenderProg = gl::GlslProg::create(gl::GlslProg::Format()
.vertex(CI_GLSL(150,
uniform mat4 ciModelViewProjection;
uniform mat4 ciModelView;
uniform float iMaxDistance;
uniform float iMinDistance;
uniform float iMinPointScale;
uniform float iMaxPointScale;
in vec4 ciPosition;
in vec4 ciColor;
out vec4 pixelColor;
void main(void) {
vec3 vertex = vec3(ciModelView * ciPosition);
float dist = length(vertex);
float pointScale = (1.0 - (dist / iMaxDistance));
// calculate point size
float Input = dist;
float InputHigh = iMaxDistance;
float InputLow = iMinDistance;
float OutputHigh = iMinPointScale;
float OutputLow = iMaxPointScale;
gl_PointSize = ((Input - InputLow) / (InputHigh - InputLow)) * (OutputHigh - OutputLow) + OutputLow;
gl_Position = ciModelViewProjection * ciPosition;
vec4 c = ciColor;
pixelColor = c;
}
))
.fragment(CI_GLSL(150,
uniform bool drawPointsRound;
uniform bool drawPointsFade;
in vec4 pixelColor;
out vec4 oColor;
void main(void)
{
// Make points circlular if specified
float pDist = -1;
if (drawPointsRound)
{
vec2 coord = gl_PointCoord - vec2(0.5);
pDist = length(coord);
if (pDist > 0.5)
discard;
}
oColor.rgb = pixelColor.rgb;
// If the A is less than 1 then the vertex
// shader has indicated that this vertex is seleceted
if (pixelColor.a < 1)
{
// pass throught the alpha valu calculated by the vertex shader.
// This is used for highlighting the brush
oColor.a = pixelColor.a;
}
else
{
// non highlighted vertices - fade out GL point to border, if specified
if (drawPointsFade)
{
// calculate distance to border of GL point if not already calculated above.
if (pDist == -1)
{
vec2 coord = gl_PointCoord - vec2(0.5);
pDist = length(coord);
}
oColor.a = 1 - pDist;
}
else
{
oColor.a = pixelColor.a; // should be 1 as reveiced from the v shader
}
}
}
)));
mUpdateProg = gl::GlslProg::create(gl::GlslProg::Format().vertex(loadAssetAbsolute("C:\\shaders\\particleUpdate.vs"))
.feedbackFormat(GL_INTERLEAVED_ATTRIBS)
.feedbackVaryings({ "position", "color", "homePosition", "homeColor" })
.attribLocation("iPosition", 0)
.attribLocation("iColor", 1)
.attribLocation("iHomePosition", 2)
.attribLocation("iHomeColor", 3));
mRenderProg called in the draw function:
gl::ScopedGlslProg render(mRenderProg);
mRenderProg->uniform("drawPointsRound", mDrawPointsRound);
mRenderProg->uniform("drawPointsFade", mDrawPointsFade);
mRenderProg->uniform("iMaxDistance", mMaxDistance);
mRenderProg->uniform("iMinDistance", mMinDistance);
mRenderProg->uniform("iMinPointScale", mMinPointScale);
mRenderProg->uniform("iMaxPointScale", mMaxPointScale);
mUpdateProg called in the update function:
gl::ScopedGlslProg prog(mUpdateProg);
gl::ScopedState rasterizer(GL_RASTERIZER_DISCARD, true); // turn off fragment stage
mUpdateProg->uniform("uElapsedSeconds", (float)getElapsedSeconds());
mUpdateProg->uniform("uMousePos", getPickedPoint());
mUpdateProg->uniform("uMouseDown", mMouseLeftDown);
mUpdateProg->uniform("uCTRLDown", mCTRLDown);
mUpdateProg->uniform("uToolMode", (int)mToolMode);
mUpdateProg->uniform("uBrushRadius", getCurrentBrushRadius()); // animated stroke
mUpdateProg->uniform("uBrushLevel", (float)mBrushLevel);
mUpdateProg->uniform("uBrushFeather", (float)mBrushFeather);
mUpdateProg->uniform("uBrushFeatherProfile", (int)mBrushFeatherProfile);
mUpdateProg->uniform("uLevelValue", mLevelValue);
mUpdateProg->uniform("uDepthRenderMode", mDepthRenderMode);
mUpdateProg->uniform("uDepthRenderModeMinVertexDepthZ", mDepthRenderModeMinVertexDepthZ);
mUpdateProg->uniform("uDepthRenderModeMaxVertexDepthZ", mDepthRenderModeMaxVertexDepthZ);
mUpdateProg->uniform("uFlattenTargetValue", mSelectedVertexStats.avg);
Cheers!