From a6491d62252bfc4b07d7a5cd51c9fd82fb2ec7d2 Mon Sep 17 00:00:00 2001 From: HifiExperiments Date: Wed, 17 Apr 2024 15:43:20 -0700 Subject: [PATCH] add default particle props --- .../proceduralParticleSwarmRender.frag | 7 ++ .../proceduralParticleSwarmRender.vert | 86 +++++++++++++++++++ .../proceduralParticleSwarmUpdate.frag | 73 ++++++++++++++++ .../entities/src/EntityItemProperties.cpp | 18 +++- scripts/system/create/edit.js | 24 +++++- 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 interface/resources/shaders/proceduralParticleSwarmRender.frag create mode 100644 interface/resources/shaders/proceduralParticleSwarmRender.vert create mode 100644 interface/resources/shaders/proceduralParticleSwarmUpdate.frag diff --git a/interface/resources/shaders/proceduralParticleSwarmRender.frag b/interface/resources/shaders/proceduralParticleSwarmRender.frag new file mode 100644 index 0000000000..746191814c --- /dev/null +++ b/interface/resources/shaders/proceduralParticleSwarmRender.frag @@ -0,0 +1,7 @@ +layout(location=2) in vec3 _normalWS; + +float getProceduralFragment(inout ProceduralFragment proceduralData) { + proceduralData.normal = normalize(_normalWS); + proceduralData.diffuse = 0.5 * (proceduralData.normal + 1.0); + return 0.0; +} diff --git a/interface/resources/shaders/proceduralParticleSwarmRender.vert b/interface/resources/shaders/proceduralParticleSwarmRender.vert new file mode 100644 index 0000000000..4f9806d1a3 --- /dev/null +++ b/interface/resources/shaders/proceduralParticleSwarmRender.vert @@ -0,0 +1,86 @@ +uniform float radius = 0.01; +uniform float lifespan = 1.0; // seconds + +layout(location=2) out vec3 _normalWS; + +float bezierInterpolate(float y1, float y2, float y3, float u) { + // https://en.wikipedia.org/wiki/Bezier_curve + return (1.0 - u) * (1.0 - u) * y1 + 2.0 * (1.0 - u) * u * y2 + u * u * y3; +} + +float interpolate3Points(float y1, float y2, float y3, float u) { + // Makes the interpolated values intersect the middle value. + + if ((u <= 0.5f && y1 == y2) || (u >= 0.5f && y2 == y3)) { + // Flat line. + return y2; + } + + float halfSlope; + if ((y2 >= y1 && y2 >= y3) || (y2 <= y1 && y2 <= y3)) { + // U or inverted-U shape. + // Make the slope at y2 = 0, which means that the control points half way between the value points have the value y2. + halfSlope = 0.0f; + + } else { + // L or inverted and/or mirrored L shape. + // Make the slope at y2 be the slope between y1 and y3, up to a maximum of double the minimum of the slopes between y1 + // and y2, and y2 and y3. Use this slope to calculate the control points half way between the value points. + // Note: The maximum ensures that the control points and therefore the interpolated values stay between y1 and y3. + halfSlope = (y3 - y1) / 2.0f; + float slope12 = y2 - y1; + float slope23 = y3 - y2; + + { + float check = float(abs(halfSlope) > abs(slope12)); + halfSlope = mix(halfSlope, slope12, check); + halfSlope = mix(halfSlope, slope23, (1.0 - check) * float(abs(halfSlope) > abs(slope23))); + } + } + + float stepU = step(0.5f, u); // 0.0 if u < 0.5, 1.0 otherwise. + float slopeSign = 2.0f * stepU - 1.0f; // -1.0 if u < 0.5, 1.0 otherwise + float start = (1.0f - stepU) * y1 + stepU * y2; // y1 if u < 0.5, y2 otherwise + float middle = y2 + slopeSign * halfSlope; + float finish = (1.0f - stepU) * y2 + stepU * y3; // y2 if u < 0.5, y3 otherwise + float v = 2.0f * u - step(0.5f, u); // 0.0-0.5 -> 0.0-1.0 and 0.5-1.0 -> 0.0-1.0 + return bezierInterpolate(start, middle, finish, v); +} + +vec3 getProceduralVertex(const int particleID) { + vec4 positionAndAge = getParticleProperty(0, particleID); + vec3 position = positionAndAge.xyz; + + const vec3 UP = vec3(0, 1, 0); + vec3 forward = normalize(getParticleProperty(1, particleID).xyz); + vec3 right = cross(forward, UP); + vec3 up = cross(right, forward); + + const int VERTEX = gl_VertexID % 3; + int TRIANGLE = int(gl_VertexID / 3); + + float age = positionAndAge.w; + float particleRadius = interpolate3Points(0.0, radius, 0.0, clamp(age / lifespan, 0.0, 1.0)); + + if (TRIANGLE < 3) { + const vec3 SIDE_POINTS[3] = vec3[3]( + up, + normalize(-up + right), + normalize(-up - right) + ); + position += particleRadius * (VERTEX == 2 ? forward : SIDE_POINTS[(TRIANGLE + VERTEX) % 3]); + _normalWS = normalize(cross(forward - SIDE_POINTS[TRIANGLE], forward - SIDE_POINTS[(TRIANGLE + 1) % 3])); + } else { + TRIANGLE -= 3; + vec3 backward = -2.0 * normalize(getParticleProperty(2, particleID).xyz); + const vec3 SIDE_POINTS[3] = vec3[3]( + up, + normalize(-up - right), + normalize(-up + right) + ); + position += particleRadius * (VERTEX == 2 ? backward : SIDE_POINTS[(TRIANGLE + VERTEX) % 3]); + _normalWS = normalize(cross(backward - SIDE_POINTS[TRIANGLE], backward - SIDE_POINTS[(TRIANGLE + 1) % 3])); + } + + return position; +} diff --git a/interface/resources/shaders/proceduralParticleSwarmUpdate.frag b/interface/resources/shaders/proceduralParticleSwarmUpdate.frag new file mode 100644 index 0000000000..cd052cb8db --- /dev/null +++ b/interface/resources/shaders/proceduralParticleSwarmUpdate.frag @@ -0,0 +1,73 @@ +uniform float lifespan = 1.0; // seconds +uniform float speed = 0.1; // m/s +uniform float speedSpread = 0.25; +uniform float mass = 1.0; + +const float G = 6.67e-11; + +// prop0: xyz: position, w: age +// prop1: xyz: velocity, w: prevUpdateTime +// prop2: xyz: prevVelocity + +vec3 initPosition(const int particleID) { + return 0.5 * (vec3(hifi_hash(particleID + iGlobalTime), + hifi_hash(particleID + iGlobalTime + 1.0), + hifi_hash(particleID + iGlobalTime + 2.0)) - 0.5); +} + +mat3 rotationMatrix(vec3 axis, float angle) { + float s = sin(angle); + float c = cos(angle); + float oc = 1.0 - c; + + return mat3(oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, + oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, + oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c); +} + +vec3 initVelocity(const int particleID, const vec3 position) { + const float particleSpeed = speed * ((1.0 - speedSpread) + speedSpread * hifi_hash(particleID + iGlobalTime + 3.0)); + vec3 r = normalize(iWorldPosition - position); + float angle = 2.0 * 3.14159 * hifi_hash(particleID + iGlobalTime + 4.0); + return particleSpeed * rotationMatrix(r, angle) * cross(r, vec3(0, 1, 0)); +} + +void updateParticleProps(const int particleID, inout ParticleUpdateProps particleProps) { + // First draw + if (particleProps.prop1.w < 0.00001) { + particleProps.prop0.xyz = iWorldOrientation * (initPosition(particleID) * iWorldScale) + iWorldPosition; + particleProps.prop0.w = -lifespan * hifi_hash(particleID + iGlobalTime + 3.0); + particleProps.prop1.xyz = initVelocity(particleID, particleProps.prop0.xyz); + particleProps.prop1.w = iGlobalTime; + particleProps.prop2.xyz = particleProps.prop1.xyz; + return; + } + + // Particle expired + if (particleProps.prop0.w >= lifespan) { + particleProps.prop0.xyz = iWorldOrientation * (initPosition(particleID) * iWorldScale) + iWorldPosition; + particleProps.prop0.w = 0.0; + particleProps.prop1.xyz = initVelocity(particleID, particleProps.prop0.xyz); + particleProps.prop1.w = iGlobalTime; + particleProps.prop2.xyz = particleProps.prop1.xyz; + return; + } + + float dt = 0.01666666666;//max(0.0, iGlobalTime - particleProps.prop1.w); + particleProps.prop2.xyz = particleProps.prop1.xyz; + if (particleProps.prop0.w >= 0.0) { + // gravitational acceleration + vec3 r = iWorldPosition - particleProps.prop0.xyz; + vec3 g = (G * mass / max(0.01, dot(r, r))) * r; + + // position + particleProps.prop0.xyz += particleProps.prop1.xyz * dt + (0.5 * dt * dt) * g; + // velocity + particleProps.prop1.xyz += g * dt; + } + + // age + particleProps.prop0.w += dt; + // prevUpdateTime + particleProps.prop1.w = iGlobalTime; +} diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 81fd692e58..60559e54c7 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -1352,10 +1352,26 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * can manipulate the properties of, and use JSON.stringify() to convert the object into a string to put in * the property. * - * @example TODO + * @example A cube of oscillating, unlit, billboarded triangles, with the oscillation in the update (computed once per particle instead of once per vertex). * particles = Entities.addEntity({ * type: "ProceduralParticleEffect", * position: Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, { x: 0, y: 0.5, z: -4 })), + * dimensions: 3, + * numParticles: 10000, + * numTrianglesPerParticle: 1, + * numUpdateProps: 1, + * particleUpdateData: JSON.stringify({ + * version: 1.0, + * fragmentShaderURL: "https://gist.githubusercontent.com/HifiExperiments/9049fb4a8dcd2c1401ff4321103dce16/raw/4f9474ed82c66c1f94c1055d2724af808cd7aace/proceduralParticleUpdate.fs", + * }), + * particleRenderData: JSON.stringify({ + * version: 1.0, + * vertexShaderURL: "https://gist.github.com/HifiExperiments/5dda24e28e7de1719e3a594d81306343/raw/92e0c5b82a9fa87685064cdbab92ed0c16f49f94/proceduralParticle2.vs", + * fragmentShaderURL: "https://gist.github.com/HifiExperiments/7def54504362c7bc79b5c85cd515b98b/raw/93b3828c2ec66b12b789a625dd141f533c595ede/proceduralParticle.fs", + * uniforms: { + * radius: 0.03 + * } + * }), * lifetime: 300 // Delete after 5 minutes. * }); */ diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 81f398ab79..1ddc2fa0d6 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -484,7 +484,29 @@ azimuthFinish: Math.PI }, ProceduralParticleEffect: { - // TODO: what should the default procedural particle be? + dimensions: 3, + numParticles: 10000, + numTrianglesPerParticle: 6, + numUpdateProps: 3, + particleUpdateData: JSON.stringify({ + version: 1.0, + fragmentShaderURL: "qrc:///shaders/proceduralParticleSwarmUpdate.frag", + uniforms: { + lifespan: 3.0, + speed: 2.0, + speedSpread: 0.25, + mass: 50000000000 + } + }), + particleRenderData: JSON.stringify({ + version: 3.0, + vertexShaderURL: "qrc:///shaders/proceduralParticleSwarmRender.vert", + fragmentShaderURL: "qrc:///shaders/proceduralParticleSwarmRender.frag", + uniforms: { + radius: 0.03, + lifespan: 3.0 + } + }) }, Light: { color: { red: 255, green: 255, blue: 255 },