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-renderer/src/RenderableEntityItem.cpp b/libraries/entities-renderer/src/RenderableEntityItem.cpp index 09bbfb9284..2b57c8b78a 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableEntityItem.cpp @@ -21,6 +21,7 @@ #include "RenderableImageEntityItem.h" #include "RenderableWebEntityItem.h" #include "RenderableParticleEffectEntityItem.h" +#include "RenderableProceduralParticleEffectEntityItem.h" #include "RenderableLineEntityItem.h" #include "RenderablePolyLineEntityItem.h" #include "RenderablePolyVoxEntityItem.h" @@ -379,6 +380,10 @@ EntityRenderer::Pointer EntityRenderer::addToScene(EntityTreeRenderer& renderer, result = make_renderer(entity); break; + case Type::ProceduralParticleEffect: + result = make_renderer(entity); + break; + case Type::Line: result = make_renderer(entity); break; diff --git a/libraries/entities-renderer/src/RenderableEntityItem.h b/libraries/entities-renderer/src/RenderableEntityItem.h index 99dbffbc72..495eeea220 100644 --- a/libraries/entities-renderer/src/RenderableEntityItem.h +++ b/libraries/entities-renderer/src/RenderableEntityItem.h @@ -78,6 +78,7 @@ public: ItemID computeMirrorView(ViewFrustum& viewFrustum) const override; static ItemID computeMirrorViewOperator(ViewFrustum& viewFrustum, const glm::vec3& inPropertiesPosition, const glm::quat& inPropertiesRotation, MirrorMode mirrorMode, const QUuid& portalExitID); + virtual void renderSimulate(RenderArgs* args) override {} protected: virtual bool needsRenderUpdateFromEntity() const final { return needsRenderUpdateFromEntity(_entity); } diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp index e2a57840d9..8fa787d413 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.cpp @@ -150,6 +150,8 @@ ItemKey ParticleEffectEntityRenderer::getKey() { builder.withSubMetaCulled(); } + builder.withSimulate(); + return builder.build(); } @@ -362,7 +364,11 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa return particle; } -void ParticleEffectEntityRenderer::stepSimulation() { +void ParticleEffectEntityRenderer::renderSimulate(RenderArgs* args) { + if (!_visible || !(_networkTexture && _networkTexture->isLoaded())) { + return; + } + if (_lastSimulated == 0) { _lastSimulated = usecTimestampNow(); return; @@ -436,9 +442,6 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) { return; } - // FIXME migrate simulation to a compute stage - stepSimulation(); - gpu::Batch& batch = *args->_batch; batch.setResourceTexture(0, _networkTexture->getGPUTexture()); diff --git a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h index 547d654486..b3414594c3 100644 --- a/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h +++ b/libraries/entities-renderer/src/RenderableParticleEffectEntityItem.h @@ -24,6 +24,8 @@ class ParticleEffectEntityRenderer : public TypedEntityRenderer +#include #include #include diff --git a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h index 6e5068c24f..75309eca4a 100644 --- a/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h +++ b/libraries/entities-renderer/src/RenderablePolyLineEntityItem.h @@ -13,7 +13,6 @@ #define hifi_RenderablePolyLineEntityItem_h #include "RenderableEntityItem.h" -#include #include namespace render { namespace entities { diff --git a/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp new file mode 100644 index 0000000000..b458a6da3e --- /dev/null +++ b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.cpp @@ -0,0 +1,220 @@ +// +// RenderableProceduralParticleEffectEntityItem.cpp +// interface/src +// +// Created by HifiExperiements on 11/19/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "RenderableProceduralParticleEffectEntityItem.h" + +#include +#include + +using namespace render; +using namespace render::entities; + +ProceduralParticleEffectEntityRenderer::ProceduralParticleEffectEntityRenderer(const EntityItemPointer& entity) : + Parent(entity) { + _updateProcedural._vertexSource = shader::Source::get(shader::gpu::vertex::DrawUnitQuadTexcoord); + _updateProcedural._opaqueFragmentSource = shader::Source::get(shader::entities_renderer::fragment::proceduralParticleUpdate); + _updateProcedural.setDoesFade(false); + + _renderProcedural._vertexSource = shader::Source::get(shader::entities_renderer::vertex::proceduralParticle); + _renderProcedural._opaqueFragmentSource = shader::Source::get(shader::entities_renderer::fragment::proceduralParticle); + _renderProcedural._transparentFragmentSource = shader::Source::get(shader::entities_renderer::fragment::proceduralParticle_translucent); + _renderProcedural._transparentState->setBlendFunction(true, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE, + gpu::State::FACTOR_ALPHA, gpu::State::BLEND_OP_ADD, gpu::State::ONE); + _renderProcedural.setDoesFade(false); +} + +void ProceduralParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { + void* key = (void*)this; + AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this, entity] { + withWriteLock([&] { + _renderTransform = getModelTransform(); + _renderTransform.postScale(entity->getScaledDimensions()); + }); + }); +} + +void ProceduralParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { + bool needsUpdateDefines = false; + bool needsRecreateParticles = false; + + uint32_t numParticles = entity->getNumParticles(); + if (_numParticles != numParticles) { + _numParticles = numParticles; + _particlePropTextureDim = pow(2, ceil(log2(sqrt(_numParticles)))); + needsUpdateDefines = true; + needsRecreateParticles = true; + } + + uint8_t numTrisPerParticle = entity->getNumTrianglesPerParticle(); + if (_numTrianglesPerParticle != numTrisPerParticle) { + _numTrianglesPerParticle = numTrisPerParticle; + needsUpdateDefines = true; + } + + uint8_t numUpdateProps = entity->getNumUpdateProps(); + if (_numUpdateProps != numUpdateProps) { + _numUpdateProps = numUpdateProps; + needsUpdateDefines = true; + needsRecreateParticles = true; + } + + if (needsRecreateParticles) { + recreateParticles(); + } + + bool particleTransparent = entity->getParticleTransparent(); + if (_transparent != particleTransparent) { + _transparent = particleTransparent; + } + + if (needsUpdateDefines) { + std::unordered_map replacements; + + static const std::string PROCEDURAL_PARTICLE_NUM_PARTICLES = "//PROCEDURAL_PARTICLE_NUM_PARTICLES"; + auto numParticlesDefine = "#undef NUM_PARTICLES\n#define NUM_PARTICLES " + std::to_string(_numParticles); + replacements[PROCEDURAL_PARTICLE_NUM_PARTICLES] = numParticlesDefine; + + static const std::string PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS = "//PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS"; + auto numUpdatePropsDefine = "#undef NUM_UPDATE_PROPS\n#define NUM_UPDATE_PROPS " + std::to_string(_numUpdateProps); + replacements[PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS] = numUpdatePropsDefine; + + static const std::string PROCEDURAL_PARTICLE_NUM_TRIS_PER_PARTICLE = "//PROCEDURAL_PARTICLE_NUM_TRIS_PER_PARTICLE"; + auto numTrisPerParticleDefine = "#undef NUM_TRIS_PER_PARTICLE\n#define NUM_TRIS_PER_PARTICLE " + std::to_string(_numTrianglesPerParticle); + replacements[PROCEDURAL_PARTICLE_NUM_TRIS_PER_PARTICLE] = numTrisPerParticleDefine; + + _updateProcedural.setFragmentReplacements(replacements); + _renderProcedural.setFragmentReplacements(replacements); + _renderProcedural.setVertexReplacements(replacements); + } + + QString particleUpdateData = entity->getParticleUpdateData(); + if (_particleUpdateData != particleUpdateData) { + _particleUpdateData = particleUpdateData; + _updateProcedural.setProceduralData(ProceduralData::parse(particleUpdateData)); + } + + QString particleRenderData = entity->getParticleRenderData(); + if (_particleRenderData != particleRenderData) { + _particleRenderData = particleRenderData; + _renderProcedural.setProceduralData(ProceduralData::parse(particleRenderData)); + } +} + +bool ProceduralParticleEffectEntityRenderer::isTransparent() const { + return _transparent || Parent::isTransparent(); +} + +ItemKey ProceduralParticleEffectEntityRenderer::getKey() { + ItemKey::Builder builder = + ItemKey::Builder().withTypeShape().withTypeMeta().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); + + if (isTransparent()) { + builder.withTransparent(); + } else if (_canCastShadow) { + builder.withShadowCaster(); + } + + if (_cullWithParent) { + builder.withSubMetaCulled(); + } + + if (!_visible) { + builder.withInvisible(); + } + + if (_numUpdateProps > 0) { + builder.withSimulate(); + } + + return builder.build(); +} + +ShapeKey ProceduralParticleEffectEntityRenderer::getShapeKey() { + auto builder = ShapeKey::Builder().withOwnPipeline(); + + if (isTransparent()) { + builder.withTranslucent(); + } + + if (_primitiveMode == PrimitiveMode::LINES) { + builder.withWireframe(); + } + + return builder.build(); +} + +void ProceduralParticleEffectEntityRenderer::recreateParticles() { + for (auto& buffer : _particleBuffers) { + if (!buffer) { + buffer = FramebufferPointer(gpu::Framebuffer::create(("RenderableProceduralParticleEffectEntity " + _entityID.toString()).toStdString())); + } + + buffer->removeRenderBuffers(); + for (size_t i = 0; i < _numUpdateProps; i++) { + TexturePointer texture = TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::VEC4, gpu::FLOAT, gpu::RGBA), + (gpu::uint16)_particlePropTextureDim, (gpu::uint16)_particlePropTextureDim, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + texture->setSource(("RenderableProceduralParticleEffectEntity " + _entityID.toString() + " " + (char)i).toStdString()); + buffer->setRenderBuffer((gpu::uint32)i, texture); + } + } +} + +void ProceduralParticleEffectEntityRenderer::renderSimulate(RenderArgs* args) { + PerformanceTimer perfTimer("RenderableProceduralParticleEffectEntityItem::simulate"); + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + + if (!_visible || _numUpdateProps == 0 || !_updateProcedural.isReady()) { + return; + } + + _evenPass = !_evenPass; + + Transform transform; + withReadLock([&] { + transform = _renderTransform; + }); + + glm::ivec4 viewport = glm::ivec4(0, 0, _particleBuffers[!_evenPass]->getWidth(), _particleBuffers[!_evenPass]->getHeight()); + batch.setViewportTransform(viewport); + batch.setFramebuffer(_particleBuffers[_evenPass]); + + for (size_t i = 0; i < _numUpdateProps; i++) { + batch.setResourceTexture((gpu::uint32)(procedural::slot::texture::ParticleProp0 + i), _particleBuffers[!_evenPass]->getRenderBuffer((gpu::uint32)i)); + } + + _updateProcedural.prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey()); + batch.draw(gpu::TRIANGLE_STRIP, 4); +} + +void ProceduralParticleEffectEntityRenderer::doRender(RenderArgs* args) { + PerformanceTimer perfTimer("RenderableProceduralParticleEffectEntityItem::render"); + Q_ASSERT(args->_batch); + gpu::Batch& batch = *args->_batch; + + if (!_visible || _numParticles == 0 || (_numUpdateProps > 0 && !_updateProcedural.isReady()) || !_renderProcedural.isReady()) { + return; + } + + Transform transform; + withReadLock([&] { + transform = _renderTransform; + }); + + for (size_t i = 0; i < _numUpdateProps; i++) { + batch.setResourceTexture((gpu::uint32)(procedural::slot::texture::ParticleProp0 + i), _particleBuffers[_evenPass]->getRenderBuffer((gpu::uint32)i)); + } + + _renderProcedural.prepare(batch, transform.getTranslation(), transform.getScale(), transform.getRotation(), _created, ProceduralProgramKey(_transparent)); + + static const size_t VERTEX_PER_TRIANGLE = 3; + batch.drawInstanced((gpu::uint32)_numParticles, gpu::TRIANGLES, (gpu::uint32)(VERTEX_PER_TRIANGLE * _numTrianglesPerParticle)); +} \ No newline at end of file diff --git a/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.h b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.h new file mode 100644 index 0000000000..deb3f70f33 --- /dev/null +++ b/libraries/entities-renderer/src/RenderableProceduralParticleEffectEntityItem.h @@ -0,0 +1,63 @@ +// +// RenderableProceduralParticleEffectEntityItem.h +// interface/src/entities +// +// Created by HifiExperiements on 11/19/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_RenderableProceduralParticleEffectEntityItem_h +#define hifi_RenderableProceduralParticleEffectEntityItem_h + +#include "RenderableEntityItem.h" +#include + +#include + +namespace render { namespace entities { + +class ProceduralParticleEffectEntityRenderer : public TypedEntityRenderer { + using Parent = TypedEntityRenderer; + friend class EntityRenderer; + +public: + ProceduralParticleEffectEntityRenderer(const EntityItemPointer& entity); + + virtual void renderSimulate(RenderArgs* args) override; + +protected: + virtual void doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) override; + virtual void doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) override; + + bool isTransparent() const override; + virtual ItemKey getKey() override; + virtual ShapeKey getShapeKey() override; + virtual void doRender(RenderArgs* args) override; + +private: + using TexturePointer = gpu::TexturePointer; + using FramebufferPointer = gpu::FramebufferPointer; + + void recreateParticles(); + + QString _particleUpdateData; + Procedural _updateProcedural; + QString _particleRenderData; + Procedural _renderProcedural; + + size_t _numParticles { 0 }; + size_t _particlePropTextureDim { 128 }; // 2^ceil(log2(sqrt(10,000))) + size_t _numTrianglesPerParticle { particle::DEFAULT_NUM_TRIS_PER }; + size_t _numUpdateProps { particle::DEFAULT_NUM_UPDATE_PROPS }; + bool _transparent { false }; + + std::array _particleBuffers; + bool _evenPass { true }; +}; + +} } // namespace + +#endif // hifi_RenderableProceduralParticleEffectEntityItem_h diff --git a/libraries/entities-renderer/src/entities-renderer/proceduralParticle.slp b/libraries/entities-renderer/src/entities-renderer/proceduralParticle.slp new file mode 100644 index 0000000000..e5119c55e4 --- /dev/null +++ b/libraries/entities-renderer/src/entities-renderer/proceduralParticle.slp @@ -0,0 +1 @@ +DEFINES translucent:f forward:f \ No newline at end of file diff --git a/libraries/entities-renderer/src/proceduralParticle.slf b/libraries/entities-renderer/src/proceduralParticle.slf new file mode 100644 index 0000000000..e2ad5bf7ff --- /dev/null +++ b/libraries/entities-renderer/src/proceduralParticle.slf @@ -0,0 +1,172 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// <$_SCRIBE_FILENAME$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by HifiExperiements on 11/21/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@if not HIFI_USE_TRANSLUCENT@> + <@include DeferredBufferWrite.slh@> +<@else@> + <@include DefaultMaterials.slh@> + + <@include GlobalLight.slh@> + <$declareEvalGlobalLightingAlphaBlended()$> + + layout(location=0) out vec4 _fragColor0; +<@endif@> + +<@include gpu/Transform.slh@> +<$declareStandardCameraTransform()$> + +<@include render-utils/ShaderConstants.h@> + +<@include procedural/ProceduralCommon.slh@> +<@include procedural/ProceduralParticleCommon.slh@> +<$declareProceduralParticleRender()$> + +layout(location=0) flat in int particleID; +layout(location=1) in vec4 _positionES; + +#line 1001 +//PROCEDURAL_BLOCK_BEGIN + +vec3 getProceduralColor() { + return vec3(1.0); +} + +float getProceduralColors(inout vec3 diffuse, inout vec3 specular, inout float shininess) { + return 1.0; +} + +float getProceduralFragment(inout ProceduralFragment proceduralData) { + return 1.0; +} + +float getProceduralFragmentWithPosition(inout ProceduralFragmentWithPosition proceduralData) { + return 1.0; +} + +//PROCEDURAL_BLOCK_END + +#line 2030 +void main(void) { + vec3 normal = vec3(0.0, 1.0, 0.0); + vec3 diffuse = vec3(0.0); + vec3 fresnel = DEFAULT_FRESNEL; + float roughness = DEFAULT_ROUGHNESS; + float metallic = DEFAULT_METALLIC; + vec3 emissive = DEFAULT_EMISSIVE; + float occlusion = DEFAULT_OCCLUSION; + float scattering = DEFAULT_SCATTERING; + float alpha = 1.0; + + float emissiveAmount = 0.0; + +<@if HIFI_USE_TRANSLUCENT@> + TransformCamera cam = getTransformCamera(); + vec3 posEye = _positionES.xyz; +<@endif@> + +#if defined(PROCEDURAL_V1) + diffuse = getProceduralColor().rgb; + emissiveAmount = 1.0; + emissive = vec3(1.0); +#elif defined(PROCEDURAL_V2) + vec3 specular = DEFAULT_SPECULAR; + float shininess = DEFAULT_SHININESS; + emissiveAmount = getProceduralColors(diffuse, specular, shininess); + roughness = max(0.0, 1.0 - shininess / 128.0); + metallic = length(specular); + emissive = vec3(clamp(emissiveAmount, 0.0, 1.0)); +#elif defined(PROCEDURAL_V3) || defined(PROCEDURAL_V4) +#if defined(PROCEDURAL_V3) + ProceduralFragment proceduralData = ProceduralFragment( +#else + TransformCamera cam = getTransformCamera(); + vec4 position = cam._viewInverse * _positionES; + ProceduralFragmentWithPosition proceduralData = ProceduralFragmentWithPosition( + position.xyz, +#endif + normal, + diffuse, + fresnel, + emissive, + alpha, + roughness, + metallic, + occlusion, + scattering + ); + +#if defined(PROCEDURAL_V3) + emissiveAmount = getProceduralFragment(proceduralData); +#else + emissiveAmount = getProceduralFragmentWithPosition(proceduralData); +#endif + normal = proceduralData.normal; + diffuse = proceduralData.diffuse; + fresnel = proceduralData.specular; + roughness = proceduralData.roughness; + metallic = proceduralData.metallic; + emissive = proceduralData.emissive; + occlusion = proceduralData.occlusion; + scattering = proceduralData.scattering; + alpha = proceduralData.alpha; + +#if defined(PROCEDURAL_V4) + position = vec4(proceduralData.position, 1.0); + vec4 posEye4 = cam._view * position; +<@if HIFI_USE_TRANSLUCENT@> + posEye = posEye4.xyz; +<@endif@> + vec4 posClip = cam._projection * posEye4; + gl_FragDepth = 0.5 * (posClip.z / posClip.w + 1.0); +#endif + +#endif + +<@if not HIFI_USE_TRANSLUCENT@> + if (emissiveAmount > 0.0) { + packDeferredFragmentLightmap( + normal, + 1.0, + diffuse, + roughness, + metallic, + emissive); + } else { + packDeferredFragment( + normal, + 1.0, + diffuse, + roughness, + metallic, + emissive, + occlusion, + scattering); + } +<@else@> + if (emissiveAmount > 0.0) { + _fragColor0 = vec4(diffuse, alpha); + } else { + _fragColor0 = vec4(evalGlobalLightingAlphaBlended( + cam._viewInverse, + 1.0, + occlusion, + posEye, + normal, + diffuse, + fresnel, + metallic, + emissive, + roughness, alpha), + alpha); + } +<@endif@> +} diff --git a/libraries/entities-renderer/src/proceduralParticle.slv b/libraries/entities-renderer/src/proceduralParticle.slv new file mode 100644 index 0000000000..4d87399fa2 --- /dev/null +++ b/libraries/entities-renderer/src/proceduralParticle.slv @@ -0,0 +1,38 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// <$_SCRIBE_FILENAME$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by HifiExperiements on 11/21/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Transform.slh@> +<$declareStandardTransform()$> + +<@include procedural/ProceduralCommon.slh@> +<@include procedural/ProceduralParticleCommon.slh@> +<$declareProceduralParticleRender()$> + +layout(location=0) flat out int particleID; +layout(location=1) out vec4 _positionES; + +#line 1001 +//PROCEDURAL_BLOCK_BEGIN +vec3 getProceduralVertex(const int particleID) { + return vec3(0.0); +} +//PROCEDURAL_BLOCK_END + +#line 2030 +void main(void) { + particleID = gpu_InstanceID(); + vec4 worldPos = vec4(getProceduralVertex(particleID), 1.0); + + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformWorldToEyeAndClipPos(cam, worldPos, _positionES, gl_Position)$> +} diff --git a/libraries/entities-renderer/src/proceduralParticleUpdate.slf b/libraries/entities-renderer/src/proceduralParticleUpdate.slf new file mode 100644 index 0000000000..2188bdc462 --- /dev/null +++ b/libraries/entities-renderer/src/proceduralParticleUpdate.slf @@ -0,0 +1,107 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// <$_SCRIBE_FILENAME$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by HifiExperiements on 11/21/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include procedural/ProceduralCommon.slh@> +<@include procedural/ProceduralParticleCommon.slh@> + +layout(location=0) in vec2 varTexCoord0; + +#if NUM_UPDATE_PROPS > 0 +layout(location=0) out vec4 _prop0; +#endif +#if NUM_UPDATE_PROPS > 1 +layout(location=1) out vec4 _prop1; +#endif +#if NUM_UPDATE_PROPS > 2 +layout(location=2) out vec4 _prop2; +#endif +#if NUM_UPDATE_PROPS > 3 +layout(location=3) out vec4 _prop3; +#endif +#if NUM_UPDATE_PROPS > 4 +layout(location=4) out vec4 _prop4; +#endif + +#if NUM_UPDATE_PROPS > 0 +struct ParticleUpdateProps { + vec4 prop0; +#if NUM_UPDATE_PROPS > 1 + vec4 prop1; +#endif +#if NUM_UPDATE_PROPS > 2 + vec4 prop2; +#endif +#if NUM_UPDATE_PROPS > 3 + vec4 prop3; +#endif +#if NUM_UPDATE_PROPS > 4 + vec4 prop4; +#endif +}; + +ParticleUpdateProps getParticleProps() { + ParticleUpdateProps particleProps; + particleProps.prop0 = texture(_prop0Texture, varTexCoord0); +#if NUM_UPDATE_PROPS > 1 + particleProps.prop1 = texture(_prop1Texture, varTexCoord0); +#endif +#if NUM_UPDATE_PROPS > 2 + particleProps.prop2 = texture(_prop2Texture, varTexCoord0); +#endif +#if NUM_UPDATE_PROPS > 3 + particleProps.prop3 = texture(_prop3Texture, varTexCoord0); +#endif +#if NUM_UPDATE_PROPS > 4 + particleProps.prop4 = texture(_prop4Texture, varTexCoord0); +#endif + return particleProps; +} +#endif + +#line 1001 +#if NUM_UPDATE_PROPS > 0 +//PROCEDURAL_BLOCK_BEGIN + +void updateParticleProps(const int particleID, inout ParticleUpdateProps particleProps) {} + +//PROCEDURAL_BLOCK_END +#endif + +#line 2030 +void main(void) { +#if NUM_UPDATE_PROPS > 0 + const ivec2 textureDims = textureSize(_prop0Texture, 0); + const ivec2 indexXY = ivec2(gl_FragCoord.xy); + const int particleID = indexXY.x + textureDims.x * indexXY.y; + + if (particleID >= NUM_PARTICLES) { + return; + } + + ParticleUpdateProps particleProps = getParticleProps(); + updateParticleProps(particleID, particleProps); + + _prop0 = particleProps.prop0; +#endif +#if NUM_UPDATE_PROPS > 1 + _prop1 = particleProps.prop1; +#endif +#if NUM_UPDATE_PROPS > 2 + _prop2 = particleProps.prop2; +#endif +#if NUM_UPDATE_PROPS > 3 + _prop3 = particleProps.prop3; +#endif +#if NUM_UPDATE_PROPS > 4 + _prop4 = particleProps.prop4; +#endif +} diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 91be6e5b94..6efb15b9d1 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -571,6 +571,14 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_SPIN_FINISH, spinFinish); CHECK_PROPERTY_CHANGE(PROP_PARTICLE_ROTATE_WITH_ENTITY, rotateWithEntity); + // Procedural Particles + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, numParticles); + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, numTrianglesPerParticle); + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, numUpdateProps); + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, particleTransparent); + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, particleUpdateData); + CHECK_PROPERTY_CHANGE(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, particleRenderData); + // Model CHECK_PROPERTY_CHANGE(PROP_MODEL_URL, modelURL); CHECK_PROPERTY_CHANGE(PROP_MODEL_SCALE, modelScale); @@ -892,6 +900,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * @see {@link Entities.EntityProperties-ParticleEffect|EntityProperties-ParticleEffect} * @see {@link Entities.EntityProperties-PolyLine|EntityProperties-PolyLine} * @see {@link Entities.EntityProperties-PolyVox|EntityProperties-PolyVox} + * @see {@link Entities.EntityProperties-ProceduralParticleEffect|EntityProperties-ProceduralParticleEffect} * @see {@link Entities.EntityProperties-Shape|EntityProperties-Shape} * @see {@link Entities.EntityProperties-Sound|EntityProperties-Sound} * @see {@link Entities.EntityProperties-Sphere|EntityProperties-Sphere} @@ -1330,6 +1339,54 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { * Entities.setVoxelSphere(polyVox, position, 0.8, 255); */ +/*@jsdoc + * The "ProceduralParticleEffect" {@link Entities.EntityType|EntityType} displays a particle system that can be + * used to simulate things such as fire, smoke, snow, magic spells, etc. The particles are fully controlled by the provided + * update and rendering shaders on the GPU. + * It has properties in addition to the common {@link Entities.EntityProperties|EntityProperties}. + * + * @typedef {object} Entities.EntityProperties-ProceduralParticleEffect + * @property {number} numParticles=10000 - The number of particles to render. + * @property {number} numTrianglesPerParticle=1 - The number of triangles to render per particle. By default, these triangles + * still need to be positioned in the particleRenderData vertex shader. + * @property {number} numUpdateProps=0 - The number of persistent Vec4 values stored per particle and updated once per frame. + * These can be modified in the particleUpdateData fragment shader and read in the + * particleRenderData vertex/fragment shaders. + * @property {boolean} particleTransparent=false - Whether the particles should render as transparent (with additive blending) + * or opaque. + * @property {ProceduralData} particleUpdateData="" - Used to store {@link ProceduralData} data as a JSON string to control + * per-particle updates if numUpdateProps > 0. You can use JSON.parse() to parse the string + * into a JavaScript object which you can manipulate the properties of, and use JSON.stringify() to convert + * the object into a string to put in the property. + * @property {ProceduralData} particleRenderData="" - Used to store {@link ProceduralData} data as a JSON string to control + * per-particle rendering. You can use JSON.parse() to parse the string into a JavaScript object which you + * can manipulate the properties of, and use JSON.stringify() to convert the object into a string to put in + * the property. + * + * @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. + * }); + */ + /*@jsdoc * The "Shape" {@link Entities.EntityType|EntityType} displays an entity of a specified shape. * It has properties in addition to the common {@link Entities.EntityProperties|EntityProperties}. @@ -1799,6 +1856,16 @@ ScriptValue EntityItemProperties::copyToScriptValue(ScriptEngine* engine, bool s COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARTICLE_ROTATE_WITH_ENTITY, rotateWithEntity); } + // Procedural Particles + if (_type == EntityTypes::ProceduralParticleEffect) { + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, numParticles); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, numTrianglesPerParticle); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, numUpdateProps); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, particleTransparent); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, particleUpdateData); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, particleRenderData); + } + // Models only if (_type == EntityTypes::Model) { COPY_PROPERTY_TO_QSCRIPTVALUE_GETTER(PROP_SHAPE_TYPE, shapeType, getShapeTypeAsString()); @@ -2225,6 +2292,14 @@ void EntityItemProperties::copyFromScriptValue(const ScriptValue& object, bool h COPY_PROPERTY_FROM_QSCRIPTVALUE(spinFinish, float, setSpinFinish); COPY_PROPERTY_FROM_QSCRIPTVALUE(rotateWithEntity, bool, setRotateWithEntity); + // Procedural Particles + COPY_PROPERTY_FROM_QSCRIPTVALUE(numParticles, uint32_t, setNumParticles); + COPY_PROPERTY_FROM_QSCRIPTVALUE(numTrianglesPerParticle, uint8_t, setNumTrianglesPerParticle); + COPY_PROPERTY_FROM_QSCRIPTVALUE(numUpdateProps, uint8_t, setNumUpdateProps); + COPY_PROPERTY_FROM_QSCRIPTVALUE(particleTransparent, bool, setParticleTransparent); + COPY_PROPERTY_FROM_QSCRIPTVALUE(particleUpdateData, QString, setParticleUpdateData); + COPY_PROPERTY_FROM_QSCRIPTVALUE(particleRenderData, QString, setParticleRenderData); + // Model COPY_PROPERTY_FROM_QSCRIPTVALUE(modelURL, QString, setModelURL); COPY_PROPERTY_FROM_QSCRIPTVALUE(modelScale, vec3, setModelScale); @@ -2521,6 +2596,14 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(spinFinish); COPY_PROPERTY_IF_CHANGED(rotateWithEntity); + // Procedural Particles + COPY_PROPERTY_IF_CHANGED(numParticles); + COPY_PROPERTY_IF_CHANGED(numTrianglesPerParticle); + COPY_PROPERTY_IF_CHANGED(numUpdateProps); + COPY_PROPERTY_IF_CHANGED(particleTransparent); + COPY_PROPERTY_IF_CHANGED(particleUpdateData); + COPY_PROPERTY_IF_CHANGED(particleRenderData); + // Model COPY_PROPERTY_IF_CHANGED(modelURL); COPY_PROPERTY_IF_CHANGED(modelScale); @@ -2884,6 +2967,17 @@ bool EntityItemProperties::getPropertyInfo(const QString& propertyName, EntityPr particle::MINIMUM_PARTICLE_SPIN, particle::MAXIMUM_PARTICLE_SPIN); ADD_PROPERTY_TO_MAP(PROP_PARTICLE_ROTATE_WITH_ENTITY, RotateWithEntity, rotateWithEntity, float); + // Procedural Particles + ADD_PROPERTY_TO_MAP_WITH_RANGE(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, NumParticles, numParticles, uint32_t, + particle::MINIMUM_MAX_PARTICLES, particle::MAXIMUM_NUM_PROCEDURAL_PARTICLES); + ADD_PROPERTY_TO_MAP_WITH_RANGE(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, NumTrianglesPerParticle, numTrianglesPerParticle, uint8_t, + particle::MINIMUM_TRIS_PER, particle::MAXIMUM_TRIS_PER); + ADD_PROPERTY_TO_MAP_WITH_RANGE(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, NumUpdateProps, numUpdateProps, uint8_t, + particle::MINIMUM_NUM_UPDATE_PROPS, particle::MAXIMUM_NUM_UPDATE_PROPS); + ADD_PROPERTY_TO_MAP(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, ParticleTransparent, particleTransparent, bool); + ADD_PROPERTY_TO_MAP(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, ParticleUpdateData, particleUpdateData, QString); + ADD_PROPERTY_TO_MAP(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, ParticleRenderData, particleRenderData, QString); + // Model ADD_PROPERTY_TO_MAP(PROP_MODEL_URL, ModelURL, modelURL, QString); ADD_PROPERTY_TO_MAP(PROP_MODEL_SCALE, ModelScale, modelScale, vec3); @@ -3333,6 +3427,15 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, properties.getRotateWithEntity()) } + if (properties.getType() == EntityTypes::ProceduralParticleEffect) { + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, properties.getNumParticles()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, properties.getNumTrianglesPerParticle()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, properties.getNumUpdateProps()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, properties.getParticleTransparent()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, properties.getParticleUpdateData()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, properties.getParticleRenderData()); + } + if (properties.getType() == EntityTypes::Model) { APPEND_ENTITY_PROPERTY(PROP_SHAPE_TYPE, (uint32_t)(properties.getShapeType())); APPEND_ENTITY_PROPERTY(PROP_COMPOUND_SHAPE_URL, properties.getCompoundShapeURL()); @@ -3827,6 +3930,15 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARTICLE_ROTATE_WITH_ENTITY, bool, setRotateWithEntity); } + if (properties.getType() == EntityTypes::ProceduralParticleEffect) { + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, uint32_t, setNumParticles); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, uint8_t, setNumTrianglesPerParticle); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, uint8_t, setNumUpdateProps); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, bool, setParticleTransparent); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, QString, setParticleUpdateData); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, QString, setParticleRenderData); + } + if (properties.getType() == EntityTypes::Model) { READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE_TYPE, ShapeType, setShapeType); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COMPOUND_SHAPE_URL, QString, setCompoundShapeURL); @@ -4227,6 +4339,14 @@ void EntityItemProperties::markAllChanged() { _spinSpreadChanged = true; _rotateWithEntityChanged = true; + // Procedural Particles + _numParticlesChanged = true; + _numTrianglesPerParticleChanged = true; + _numUpdatePropsChanged = true; + _particleTransparentChanged = true; + _particleUpdateDataChanged = true; + _particleRenderDataChanged = true; + // Model _modelURLChanged = true; _modelScaleChanged = true; @@ -4767,6 +4887,26 @@ QList EntityItemProperties::listChangedProperties() { out += "rotateWithEntity"; } + // Procedural Particles + if (numParticlesChanged()) { + out += "numParticles"; + } + if (numTrianglesPerParticleChanged()) { + out += "numTrianglesPerParticle"; + } + if (numUpdatePropsChanged()) { + out += "numUpdateProps"; + } + if (particleTransparentChanged()) { + out += "particleTransparent"; + } + if (particleUpdateDataChanged()) { + out += "particleUpdateData"; + } + if (particleRenderDataChanged()) { + out += "particleRenderData"; + } + // Model if (modelURLChanged()) { out += "modelURL"; diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 961049b1c7..4c333a5621 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -48,6 +48,7 @@ #include "TextEntityItem.h" #include "WebEntityItem.h" #include "ParticleEffectEntityItem.h" +#include "ProceduralParticleEffectEntityItem.h" #include "LineEntityItem.h" #include "PolyVoxEntityItem.h" #include "GridEntityItem.h" @@ -117,6 +118,7 @@ class EntityItemProperties { friend class ImageEntityItem; friend class WebEntityItem; friend class ParticleEffectEntityItem; + friend class ProceduralParticleEffectEntityItem; friend class LineEntityItem; friend class PolyLineEntityItem; friend class PolyVoxEntityItem; @@ -291,6 +293,14 @@ public: DEFINE_PROPERTY(PROP_SPIN_FINISH, SpinFinish, spinFinish, float, particle::DEFAULT_SPIN_FINISH); DEFINE_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, RotateWithEntity, rotateWithEntity, bool, particle::DEFAULT_ROTATE_WITH_ENTITY); + // Procedural Particles + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, NumParticles, numParticles, uint32_t, particle::DEFAULT_NUM_PROCEDURAL_PARTICLES); + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, NumTrianglesPerParticle, numTrianglesPerParticle, uint8_t, particle::DEFAULT_NUM_TRIS_PER); + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, NumUpdateProps, numUpdateProps, uint8_t, particle::DEFAULT_NUM_UPDATE_PROPS); + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, ParticleTransparent, particleTransparent, bool, false); + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, ParticleUpdateData, particleUpdateData, QString, ""); + DEFINE_PROPERTY_REF(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, ParticleRenderData, particleRenderData, QString, ""); + // Model DEFINE_PROPERTY_REF(PROP_MODEL_URL, ModelURL, modelURL, QString, ""); DEFINE_PROPERTY_REF(PROP_MODEL_SCALE, ModelScale, modelScale, glm::vec3, glm::vec3(1.0f)); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b27c9ed5e8..1ca51323a6 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -5,6 +5,7 @@ // Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2020 Vircadia contributors. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -216,6 +217,14 @@ enum EntityPropertyList { PROP_SPIN_SPREAD = PROP_DERIVED_29, PROP_PARTICLE_ROTATE_WITH_ENTITY = PROP_DERIVED_30, + // Procedural Particles + PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES = PROP_DERIVED_0, + PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER = PROP_DERIVED_1, + PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS = PROP_DERIVED_2, + PROP_PROCEDURAL_PARTICLE_TRANSPARENT = PROP_DERIVED_3, + PROP_PROCEDURAL_PARTCILE_UPDATE_DATA = PROP_DERIVED_4, + PROP_PROCEDURAL_PARTCILE_RENDER_DATA = PROP_DERIVED_5, + // Model PROP_MODEL_URL = PROP_DERIVED_0, PROP_MODEL_SCALE = PROP_DERIVED_1, diff --git a/libraries/entities/src/EntityTreeElement.cpp b/libraries/entities/src/EntityTreeElement.cpp index b6f65cff65..6b38a17d82 100644 --- a/libraries/entities/src/EntityTreeElement.cpp +++ b/libraries/entities/src/EntityTreeElement.cpp @@ -258,7 +258,7 @@ EntityItemID EntityTreeElement::evalDetailedRayIntersection(const glm::vec3& ori } else { // if the entity type doesn't support a detailed intersection, then just return the non-AABox results // Never intersect with particle or sound entities - if (localDistance < distance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::Sound)) { + if (localDistance < distance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::ProceduralParticleEffect && entity->getType() != EntityTypes::Sound)) { distance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 0.0f)); @@ -410,7 +410,7 @@ EntityItemID EntityTreeElement::evalDetailedParabolaIntersection(const glm::vec3 } else { // if the entity type doesn't support a detailed intersection, then just return the non-AABox results // Never intersect with particle or sound entities - if (localDistance < parabolicDistance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::Sound)) { + if (localDistance < parabolicDistance && (entity->getType() != EntityTypes::ParticleEffect && entity->getType() != EntityTypes::ProceduralParticleEffect && entity->getType() != EntityTypes::Sound)) { parabolicDistance = localDistance; face = localFace; surfaceNormal = glm::vec3(rotation * glm::vec4(localSurfaceNormal, 0.0f)); diff --git a/libraries/entities/src/EntityTypes.cpp b/libraries/entities/src/EntityTypes.cpp index 49109fd5dd..2873920793 100644 --- a/libraries/entities/src/EntityTypes.cpp +++ b/libraries/entities/src/EntityTypes.cpp @@ -4,6 +4,7 @@ // // Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 High Fidelity, Inc. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -23,6 +24,7 @@ #include "ShapeEntityItem.h" #include "ModelEntityItem.h" #include "ParticleEffectEntityItem.h" +#include "ProceduralParticleEffectEntityItem.h" #include "TextEntityItem.h" #include "ImageEntityItem.h" #include "WebEntityItem.h" @@ -52,6 +54,7 @@ REGISTER_ENTITY_TYPE(Text) REGISTER_ENTITY_TYPE(Image) REGISTER_ENTITY_TYPE(Web) REGISTER_ENTITY_TYPE(ParticleEffect) +REGISTER_ENTITY_TYPE(ProceduralParticleEffect) REGISTER_ENTITY_TYPE(Line) REGISTER_ENTITY_TYPE(PolyLine) REGISTER_ENTITY_TYPE(PolyVox) diff --git a/libraries/entities/src/EntityTypes.h b/libraries/entities/src/EntityTypes.h index 30d59727d4..ab3233e639 100644 --- a/libraries/entities/src/EntityTypes.h +++ b/libraries/entities/src/EntityTypes.h @@ -4,6 +4,7 @@ // // Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 High Fidelity, Inc. +// Copyright 2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html @@ -70,6 +71,9 @@ public: * "ParticleEffect"A particle system that can be used to simulate things such as fire, * smoke, snow, magic spells, etc. * {@link Entities.EntityProperties-ParticleEffect|EntityProperties-ParticleEffect} + * "ProceduralParticleEffect"A GPU particle system with custom update and rendering that can + * be used to simulate things such as fire, smoke, snow, magic spells, etc. + * {@link Entities.EntityProperties-ProceduralParticleEffect|EntityProperties-ProceduralParticleEffect} * "Line"A sequence of one or more simple straight lines. * {@link Entities.EntityProperties-Line|EntityProperties-Line} * "PolyLine"A sequence of one or more textured straight lines. @@ -102,6 +106,7 @@ public: Image, Web, ParticleEffect, + ProceduralParticleEffect, Line, PolyLine, PolyVox, diff --git a/libraries/entities/src/ProceduralParticleEffectEntityItem.cpp b/libraries/entities/src/ProceduralParticleEffectEntityItem.cpp new file mode 100644 index 0000000000..a8a601d6f6 --- /dev/null +++ b/libraries/entities/src/ProceduralParticleEffectEntityItem.cpp @@ -0,0 +1,174 @@ +// +// ProceduralParticleEffectEntityItem.cpp +// libraries/entities/src +// +// Created by HifiExperiements on 11/19/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ProceduralParticleEffectEntityItem.h" + +#include "EntityTree.h" +#include "EntityTreeElement.h" +#include "EntitiesLogging.h" +#include "EntityScriptingInterface.h" + +EntityItemPointer ProceduralParticleEffectEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { + std::shared_ptr entity(new ProceduralParticleEffectEntityItem(entityID), [](ProceduralParticleEffectEntityItem* ptr) { ptr->deleteLater(); }); + entity->setProperties(properties); + return entity; +} + +// our non-pure virtual subclass for now... +ProceduralParticleEffectEntityItem::ProceduralParticleEffectEntityItem(const EntityItemID& entityItemID) : + EntityItem(entityItemID) +{ + _type = EntityTypes::ProceduralParticleEffect; +} + +EntityItemProperties ProceduralParticleEffectEntityItem::getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const { + EntityItemProperties properties = EntityItem::getProperties(desiredProperties, allowEmptyDesiredProperties); // get the properties from our base class + + COPY_ENTITY_PROPERTY_TO_PROPERTIES(numParticles, getNumParticles); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(numTrianglesPerParticle, getNumTrianglesPerParticle); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(numUpdateProps, getNumUpdateProps); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(particleTransparent, getParticleTransparent); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(particleUpdateData, getParticleUpdateData); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(particleRenderData, getParticleRenderData); + + return properties; +} + +bool ProceduralParticleEffectEntityItem::setSubClassProperties(const EntityItemProperties& properties) { + bool somethingChanged = false; + + SET_ENTITY_PROPERTY_FROM_PROPERTIES(numParticles, setNumParticles); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(numTrianglesPerParticle, setNumTrianglesPerParticle); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(numUpdateProps, setNumUpdateProps); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(particleTransparent, setParticleTransparent); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(particleUpdateData, setParticleUpdateData); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(particleRenderData, setParticleRenderData); + + return somethingChanged; +} + +int ProceduralParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) { + + int bytesRead = 0; + const unsigned char* dataAt = data; + + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, uint32_t, setNumParticles); + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, uint8_t, setNumTrianglesPerParticle); + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, uint8_t, setNumUpdateProps); + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, bool, setParticleTransparent); + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, QString, setParticleUpdateData); + READ_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, QString, setParticleRenderData); + + return bytesRead; +} + +EntityPropertyFlags ProceduralParticleEffectEntityItem::getEntityProperties(EncodeBitstreamParams& params) const { + EntityPropertyFlags requestedProperties = EntityItem::getEntityProperties(params); + + requestedProperties += PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES; + requestedProperties += PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER; + requestedProperties += PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS; + requestedProperties += PROP_PROCEDURAL_PARTICLE_TRANSPARENT; + requestedProperties += PROP_PROCEDURAL_PARTCILE_UPDATE_DATA; + requestedProperties += PROP_PROCEDURAL_PARTCILE_RENDER_DATA; + + return requestedProperties; +} + +void ProceduralParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const { + + bool successPropertyFits = true; + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_PARTICLES, getNumParticles()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_TRIS_PER, getNumTrianglesPerParticle()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS, getNumUpdateProps()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTICLE_TRANSPARENT, getParticleTransparent()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_UPDATE_DATA, getParticleUpdateData()); + APPEND_ENTITY_PROPERTY(PROP_PROCEDURAL_PARTCILE_RENDER_DATA, getParticleRenderData()); +} + +void ProceduralParticleEffectEntityItem::debugDump() const { + quint64 now = usecTimestampNow(); + qCDebug(entities) << "PROC PA EFFECT EntityItem id:" << getEntityItemID() << "---------------------------------------------"; + qCDebug(entities) << " position:" << debugTreeVector(getWorldPosition()); + qCDebug(entities) << " dimensions:" << debugTreeVector(getScaledDimensions()); + qCDebug(entities) << " getLastEdited:" << debugTime(getLastEdited(), now); +} + +uint32_t ProceduralParticleEffectEntityItem::getNumParticles() const { + return resultWithReadLock([&] { return _numParticles; }); +} + +void ProceduralParticleEffectEntityItem::setNumParticles(uint32_t numParticles) { + withWriteLock([&] { + _needsRenderUpdate |= _numParticles != numParticles; + _numParticles = numParticles; + }); +} + +uint8_t ProceduralParticleEffectEntityItem::getNumTrianglesPerParticle() const { + return resultWithReadLock([&] { return _numTrianglesPerParticle; }); +} + +void ProceduralParticleEffectEntityItem::setNumTrianglesPerParticle(uint8_t numTrianglesPerParticle) { + withWriteLock([&] { + _needsRenderUpdate |= _numTrianglesPerParticle != numTrianglesPerParticle; + _numTrianglesPerParticle = numTrianglesPerParticle; + }); +} + +uint8_t ProceduralParticleEffectEntityItem::getNumUpdateProps() const { + return resultWithReadLock([&] { return _numUpdateProps; }); +} + +void ProceduralParticleEffectEntityItem::setNumUpdateProps(uint8_t numUpdateProps) { + withWriteLock([&] { + _needsRenderUpdate |= _numUpdateProps != numUpdateProps; + _numUpdateProps = numUpdateProps; + }); +} + +void ProceduralParticleEffectEntityItem::setParticleTransparent(bool particleTransparent) { + withWriteLock([&] { + _needsRenderUpdate |= _particleTransparent != particleTransparent; + _particleTransparent = particleTransparent; + }); +} + +QString ProceduralParticleEffectEntityItem::getParticleUpdateData() const { + return resultWithReadLock([&] { return _particleUpdateData; }); +} + +void ProceduralParticleEffectEntityItem::setParticleUpdateData(const QString& particleUpdateData) { + withWriteLock([&] { + _needsRenderUpdate |= _particleUpdateData != particleUpdateData; + _particleUpdateData = particleUpdateData; + }); +} + +QString ProceduralParticleEffectEntityItem::getParticleRenderData() const { + return resultWithReadLock([&] { return _particleRenderData; }); +} + +void ProceduralParticleEffectEntityItem::setParticleRenderData(const QString& particleRenderData) { + withWriteLock([&] { + _needsRenderUpdate |= _particleRenderData != particleRenderData; + _particleRenderData = particleRenderData; + }); +} \ No newline at end of file diff --git a/libraries/entities/src/ProceduralParticleEffectEntityItem.h b/libraries/entities/src/ProceduralParticleEffectEntityItem.h new file mode 100644 index 0000000000..98ae8b53df --- /dev/null +++ b/libraries/entities/src/ProceduralParticleEffectEntityItem.h @@ -0,0 +1,88 @@ +// +// ProceduralParticleEffectEntityItem.h +// libraries/entities/src +// +// Created by HifiExperiements on 11/19/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_ProceduralParticleEffectEntityItem_h +#define hifi_ProceduralParticleEffectEntityItem_h + +#include "EntityItem.h" + +namespace particle { + static const uint32_t DEFAULT_NUM_PROCEDURAL_PARTICLES = 10000; + static const uint32_t MAXIMUM_NUM_PROCEDURAL_PARTICLES = 1024 * 1024; + static const uint8_t DEFAULT_NUM_TRIS_PER = 1; + static const uint8_t MINIMUM_TRIS_PER = 1; + static const uint8_t MAXIMUM_TRIS_PER = 15; + static const uint8_t DEFAULT_NUM_UPDATE_PROPS = 0; + static const uint8_t MINIMUM_NUM_UPDATE_PROPS = 0; + static const uint8_t MAXIMUM_NUM_UPDATE_PROPS = 5; +} + +class ProceduralParticleEffectEntityItem : public EntityItem { +public: + ALLOW_INSTANTIATION // This class can be instantiated + + static EntityItemPointer factory(const EntityItemID& entityID, const EntityItemProperties& properties); + + ProceduralParticleEffectEntityItem(const EntityItemID& entityItemID); + + // methods for getting/setting all properties of this entity + virtual EntityItemProperties getProperties(const EntityPropertyFlags& desiredProperties, bool allowEmptyDesiredProperties) const override; + virtual bool setSubClassProperties(const EntityItemProperties& properties) override; + + virtual EntityPropertyFlags getEntityProperties(EncodeBitstreamParams& params) const override; + + virtual void appendSubclassData(OctreePacketData* packetData, EncodeBitstreamParams& params, + EntityTreeElementExtraEncodeDataPointer entityTreeElementExtraEncodeData, + EntityPropertyFlags& requestedProperties, + EntityPropertyFlags& propertyFlags, + EntityPropertyFlags& propertiesDidntFit, + int& propertyCount, + OctreeElement::AppendState& appendState) const override; + + virtual int readEntitySubclassDataFromBuffer(const unsigned char* data, int bytesLeftToRead, + ReadBitstreamToTreeParams& args, + EntityPropertyFlags& propertyFlags, bool overwriteLocalData, + bool& somethingChanged) override; + + bool shouldBePhysical() const override { return false; } + + virtual void debugDump() const override; + + virtual bool supportsDetailedIntersection() const override { return false; } + + uint32_t getNumParticles() const; + void setNumParticles(uint32_t numParticles); + + uint8_t getNumTrianglesPerParticle() const; + void setNumTrianglesPerParticle(uint8_t numTrianglesPerParticle); + + uint8_t getNumUpdateProps() const; + void setNumUpdateProps(uint8_t numUpdateProps); + + bool getParticleTransparent() const { return _particleTransparent; } + void setParticleTransparent(bool particleTransparent); + + QString getParticleUpdateData() const; + void setParticleUpdateData(const QString& particleUpdateData); + + QString getParticleRenderData() const; + void setParticleRenderData(const QString& particleRenderData); + +protected: + uint32_t _numParticles; + uint8_t _numTrianglesPerParticle; + uint8_t _numUpdateProps; + bool _particleTransparent; + QString _particleUpdateData; + QString _particleRenderData; +}; + +#endif // hifi_ProceduralParticleEffectEntityItem_h diff --git a/libraries/gpu/src/gpu/Transform.slh b/libraries/gpu/src/gpu/Transform.slh index 3015de7e0e..767db13595 100644 --- a/libraries/gpu/src/gpu/Transform.slh +++ b/libraries/gpu/src/gpu/Transform.slh @@ -246,6 +246,16 @@ TransformObject getTransformObject() { } <@endfunc@> +<@func transformWorldToEyeAndClipPos(cameraTransform, worldPos, eyePos, clipPos)@> + { // transformWorldToEyeAndClipPos + vec4 eyeWAPos = <$worldPos$> - vec4(<$cameraTransform$>._viewInverse[3].xyz, 0.0); + <$eyePos$> = vec4((<$cameraTransform$>._view * vec4(eyeWAPos.xyz, 0.0)).xyz, 1.0); + <$clipPos$> = <$cameraTransform$>._projectionViewUntranslated * eyeWAPos; + + <$transformStereoClipsSpace($cameraTransform$, $clipPos$)$> + } +<@endfunc@> + <@func transformModelToWorldPos(objectTransform, modelPos, worldPos)@> { // transformModelToWorldPos <$worldPos$> = (<$objectTransform$>._model * <$modelPos$>); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 6c637abedc..0709cf9a1b 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -295,6 +295,7 @@ enum class EntityVersion : PacketVersion { WantsKeyboardFocus, AudioZones, AnimationSmoothFrames, + ProceduralParticles, SoundEntities, // Add new versions above here diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index 66dde1ca56..139da1bf20 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -327,12 +327,12 @@ void Procedural::prepare(gpu::Batch& batch, // Build the fragment and vertex shaders auto versionDefine = "#define PROCEDURAL_V" + std::to_string(_data.version); - fragmentSource.replacements.clear(); + fragmentSource.replacements = _fragmentReplacements; fragmentSource.replacements[PROCEDURAL_VERSION] = versionDefine; if (!_fragmentShaderSource.isEmpty()) { fragmentSource.replacements[PROCEDURAL_BLOCK] = _fragmentShaderSource.toStdString(); } - vertexSource.replacements.clear(); + vertexSource.replacements = _vertexReplacements; vertexSource.replacements[PROCEDURAL_VERSION] = versionDefine; if (!_vertexShaderSource.isEmpty()) { vertexSource.replacements[PROCEDURAL_BLOCK] = _vertexShaderSource.toStdString(); @@ -525,6 +525,19 @@ bool Procedural::hasVertexShader() const { return !_data.vertexShaderUrl.isEmpty(); } + +void Procedural::setVertexReplacements(const std::unordered_map& replacements) { + std::lock_guard lock(_mutex); + _vertexReplacements = replacements; + _shaderDirty = true; +} + +void Procedural::setFragmentReplacements(const std::unordered_map& replacements) { + std::lock_guard lock(_mutex); + _fragmentReplacements = replacements; + _shaderDirty = true; +} + void graphics::ProceduralMaterial::initializeProcedural() { _procedural._vertexSource = gpu::Shader::getVertexShaderSource(shader::render_utils::vertex::simple_procedural); _procedural._vertexSourceSkinned = gpu::Shader::getVertexShaderSource(shader::render_utils::vertex::simple_procedural_deformed); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 39c619c687..d343956b17 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -118,6 +118,9 @@ public: bool hasBoundOperator() const { return (bool)_boundOperator; } AABox getBound(RenderArgs* args) { return _boundOperator(args); } + void setVertexReplacements(const std::unordered_map& replacements); + void setFragmentReplacements(const std::unordered_map& replacements); + gpu::Shader::Source _vertexSource; gpu::Shader::Source _vertexSourceSkinned; gpu::Shader::Source _vertexSourceSkinnedDQ; @@ -179,6 +182,8 @@ protected: // Rendering objects UniformLambdas _uniforms; NetworkTexturePointer _channels[MAX_PROCEDURAL_TEXTURE_CHANNELS]; + std::unordered_map _vertexReplacements; + std::unordered_map _fragmentReplacements; std::unordered_map _proceduralPipelines; diff --git a/libraries/procedural/src/procedural/ProceduralParticleCommon.slh b/libraries/procedural/src/procedural/ProceduralParticleCommon.slh new file mode 100644 index 0000000000..363cdeae01 --- /dev/null +++ b/libraries/procedural/src/procedural/ProceduralParticleCommon.slh @@ -0,0 +1,82 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// <$_SCRIBE_FILENAME$> +// Generated on <$_SCRIBE_DATE$> +// +// Created by HifiExperiements on 11/21/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include procedural/ShaderConstants.h@> + +#define NUM_PARTICLES 1 +//PROCEDURAL_PARTICLE_NUM_PARTICLES + +#define NUM_TRIS_PER_PARTICLE 3 +//PROCEDURAL_PARTICLE_NUM_TRIS_PER_PARTICLE + +#define NUM_UPDATE_PROPS 0 +//PROCEDURAL_PARTICLE_NUM_UPDATE_PROPS + + +#if NUM_UPDATE_PROPS > 0 +LAYOUT(binding=PROCEDURAL_PARTICLE_TEXTURE_PROP0) uniform sampler2D _prop0Texture; +#endif +#if NUM_UPDATE_PROPS > 1 +LAYOUT(binding=PROCEDURAL_PARTICLE_TEXTURE_PROP1) uniform sampler2D _prop1Texture; +#endif +#if NUM_UPDATE_PROPS > 2 +LAYOUT(binding=PROCEDURAL_PARTICLE_TEXTURE_PROP2) uniform sampler2D _prop2Texture; +#endif +#if NUM_UPDATE_PROPS > 3 +LAYOUT(binding=PROCEDURAL_PARTICLE_TEXTURE_PROP3) uniform sampler2D _prop3Texture; +#endif +#if NUM_UPDATE_PROPS > 4 +LAYOUT(binding=PROCEDURAL_PARTICLE_TEXTURE_PROP4) uniform sampler2D _prop4Texture; +#endif + +<@func declareProceduralParticleRender()@> + +vec4 getParticleProperty(const int propIndex, const int particleID) { + if (propIndex < 0 || propIndex >= NUM_UPDATE_PROPS || particleID < 0 || particleID >= NUM_PARTICLES) { + return vec4(0.0); + } + +#if NUM_UPDATE_PROPS > 0 + const ivec2 textureDims = textureSize(_prop0Texture, 0); + const ivec2 uv = ivec2(particleID % textureDims.x, particleID / textureDims.x); + if (propIndex == 0) { + return texelFetch(_prop0Texture, uv, 0); + } +#endif +#if NUM_UPDATE_PROPS > 1 + else if (propIndex == 1) { + return texelFetch(_prop1Texture, uv, 0); + } +#endif +#if NUM_UPDATE_PROPS > 2 + else if (propIndex == 2) { + return texelFetch(_prop2Texture, uv, 0); + } +#endif +#if NUM_UPDATE_PROPS > 3 + else if (propIndex == 3) { + return texelFetch(_prop3Texture, uv, 0); + } +#endif +#if NUM_UPDATE_PROPS > 4 + else if (propIndex == 4) { + return texelFetch(_prop4Texture, uv, 0); + } +#endif + + return vec4(0.0); +} + +<@endfunc@> + +// hack comment for extra whitespace + diff --git a/libraries/procedural/src/procedural/ShaderConstants.h b/libraries/procedural/src/procedural/ShaderConstants.h index f1336b6479..a70538359f 100644 --- a/libraries/procedural/src/procedural/ShaderConstants.h +++ b/libraries/procedural/src/procedural/ShaderConstants.h @@ -1,6 +1,7 @@ // ("DrawMirrorTask" + std::to_string(mirrorIndex) + "Depth" + std::to_string(depth), setupOutput); } + +void RenderSimulateTask::run(const render::RenderContextPointer& renderContext, const Inputs& inputs) { + auto args = renderContext->args; + auto items = inputs.get0(); + auto framebuffer = inputs.get1(); + + gpu::doInBatch("RenderSimulateTask::run", args->_context, [&](gpu::Batch& batch) { + args->_batch = &batch; + + for (ItemBound& item : items) { + args->_scene->simulate(item.id, args); + } + + // Reset render state after each simulate + batch.setFramebuffer(framebuffer); + batch.setViewportTransform(args->_viewport); + batch.setStateScissorRect(args->_viewport); + + args->_batch = nullptr; + }); +} \ No newline at end of file diff --git a/libraries/render-utils/src/RenderCommonTask.h b/libraries/render-utils/src/RenderCommonTask.h index cada3cb6ea..255fcb6392 100644 --- a/libraries/render-utils/src/RenderCommonTask.h +++ b/libraries/render-utils/src/RenderCommonTask.h @@ -167,4 +167,14 @@ public: static const size_t MAX_MIRRORS_PER_LEVEL { 3 }; }; +class RenderSimulateTask { +public: + using Inputs = render::VaryingSet2; + using JobModel = render::Job::ModelI; + + RenderSimulateTask() {} + + void run(const render::RenderContextPointer& renderContext, const Inputs& inputs); +}; + #endif // hifi_RenderDeferredTask_h diff --git a/libraries/render-utils/src/RenderDeferredTask.cpp b/libraries/render-utils/src/RenderDeferredTask.cpp index fd10452dfa..aa80b5ec6e 100644 --- a/libraries/render-utils/src/RenderDeferredTask.cpp +++ b/libraries/render-utils/src/RenderDeferredTask.cpp @@ -120,6 +120,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto& opaques = items[RenderFetchCullSortTask::OPAQUE_SHAPE]; const auto& transparents = items[RenderFetchCullSortTask::TRANSPARENT_SHAPE]; const auto& mirrors = items[RenderFetchCullSortTask::MIRROR]; + const auto& simulateItems = items[RenderFetchCullSortTask::SIMULATE]; const auto& inFrontOpaque = items[RenderFetchCullSortTask::LAYER_FRONT_OPAQUE_SHAPE]; const auto& inFrontTransparent = items[RenderFetchCullSortTask::LAYER_FRONT_TRANSPARENT_SHAPE]; const auto& hudOpaque = items[RenderFetchCullSortTask::LAYER_HUD_OPAQUE_SHAPE]; @@ -157,7 +158,12 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren const auto prepareDeferredOutputs = task.addJob("PrepareDeferred", prepareDeferredInputs); const auto deferredFramebuffer = prepareDeferredOutputs.getN(0); const auto lightingFramebuffer = prepareDeferredOutputs.getN(1); - const auto mirrorTargetFramebuffer = prepareDeferredOutputs.getN(2); + const auto mainTargetFramebuffer = prepareDeferredOutputs.getN(2); + + if (depth == 0) { + const auto simulateInputs = RenderSimulateTask::Inputs(simulateItems, mainTargetFramebuffer).asVarying(); + task.addJob("RenderSimulation", simulateInputs); + } // draw a stencil mask in hidden regions of the framebuffer. task.addJob("PrepareStencil", scaledPrimaryFramebuffer); @@ -167,7 +173,7 @@ void RenderDeferredTask::build(JobModel& task, const render::Varying& input, ren task.addJob("DrawOpaqueDeferred", opaqueInputs, shapePlumber); if (depth < RenderMirrorTask::MAX_MIRROR_DEPTH) { - const auto mirrorInputs = RenderMirrorTask::Inputs(mirrors, mirrorTargetFramebuffer, jitter).asVarying(); + const auto mirrorInputs = RenderMirrorTask::Inputs(mirrors, mainTargetFramebuffer, jitter).asVarying(); for (size_t i = 0; i < RenderMirrorTask::MAX_MIRRORS_PER_LEVEL; i++) { task.addJob("RenderMirrorTask" + std::to_string(i) + "Depth" + std::to_string(depth), mirrorInputs, i, cullFunctor, depth); } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 722ba2248c..9b483c16c2 100644 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -92,6 +92,7 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend const auto& transparents = items[RenderFetchCullSortTask::TRANSPARENT_SHAPE]; const auto& metas = items[RenderFetchCullSortTask::META]; const auto& mirrors = items[RenderFetchCullSortTask::MIRROR]; + const auto& simulateItems = items[RenderFetchCullSortTask::SIMULATE]; const auto& inFrontOpaque = items[RenderFetchCullSortTask::LAYER_FRONT_OPAQUE_SHAPE]; const auto& inFrontTransparent = items[RenderFetchCullSortTask::LAYER_FRONT_TRANSPARENT_SHAPE]; const auto& hudOpaque = items[RenderFetchCullSortTask::LAYER_HUD_OPAQUE_SHAPE]; @@ -123,6 +124,11 @@ void RenderForwardTask::build(JobModel& task, const render::Varying& input, rend const auto prepareForwardInputs = PrepareForward::Inputs(scaledPrimaryFramebuffer, lightFrame).asVarying(); task.addJob("PrepareForward", prepareForwardInputs); + if (depth == 0) { + const auto simulateInputs = RenderSimulateTask::Inputs(simulateItems, scaledPrimaryFramebuffer).asVarying(); + task.addJob("RenderSimulation", simulateInputs); + } + // draw a stencil mask in hidden regions of the framebuffer. task.addJob("PrepareStencil", scaledPrimaryFramebuffer); diff --git a/libraries/render/src/render/FilterTask.cpp b/libraries/render/src/render/FilterTask.cpp index b269f44b41..90720f5666 100644 --- a/libraries/render/src/render/FilterTask.cpp +++ b/libraries/render/src/render/FilterTask.cpp @@ -148,3 +148,11 @@ void IDsToBounds::run(const RenderContextPointer& renderContext, const ItemIDs& } } } + +void MergeItems::run(const RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs) { + const auto& array1 = inputs.get0(); + const auto& array2 = inputs.get1(); + + outputs = array1; + outputs.insert(outputs.end(), array2.begin(), array2.end()); +} diff --git a/libraries/render/src/render/FilterTask.h b/libraries/render/src/render/FilterTask.h index c2244e5f57..1a8f8f2e15 100644 --- a/libraries/render/src/render/FilterTask.h +++ b/libraries/render/src/render/FilterTask.h @@ -158,6 +158,18 @@ namespace render { bool _disableAABBs{ false }; }; + // Concatenate two arrays of items + class MergeItems { + public: + using Inputs = VaryingSet2; + using Outputs = ItemBounds; + using JobModel = Job::ModelIO; + + MergeItems() {} + + void run(const RenderContextPointer& renderContext, const Inputs& inputs, Outputs& outputs); + }; + } #endif // hifi_render_FilterTask_h; \ No newline at end of file diff --git a/libraries/render/src/render/Item.cpp b/libraries/render/src/render/Item.cpp index 1633523267..f169de5c98 100644 --- a/libraries/render/src/render/Item.cpp +++ b/libraries/render/src/render/Item.cpp @@ -147,6 +147,13 @@ namespace render { payload->render(args); } + template <> void payloadRenderSimulate(const PayloadProxyInterface::Pointer& payload, RenderArgs* args) { + if (!args || !payload) { + return; + } + payload->renderSimulate(args); + } + template <> uint32_t metaFetchMetaSubItems(const PayloadProxyInterface::Pointer& payload, ItemIDs& subItems) { if (!payload) { return 0; diff --git a/libraries/render/src/render/Item.h b/libraries/render/src/render/Item.h index f91b887fcb..a5cda7cedf 100644 --- a/libraries/render/src/render/Item.h +++ b/libraries/render/src/render/Item.h @@ -111,6 +111,7 @@ public: LAST_LAYER_BIT = FIRST_LAYER_BIT + NUM_LAYER_BITS, MIRROR, + SIMULATE, __SMALLER, // Reserved bit for spatialized item to indicate that it is smaller than expected in the cell in which it belongs (probably because it overlaps over several smaller cells) @@ -165,6 +166,7 @@ public: Builder& withSubMetaCulled() { _flags.set(SUB_META_CULLED); return (*this); } Builder& withoutSubMetaCulled() { _flags.reset(SUB_META_CULLED); return (*this); } Builder& withMirror() { _flags.set(MIRROR); return (*this); } + Builder& withSimulate() { _flags.set(SIMULATE); return (*this); } Builder& withTag(Tag tag) { _flags.set(FIRST_TAG_BIT + tag); return (*this); } // Set ALL the tags in one call using the Tag bits @@ -211,6 +213,9 @@ public: bool isNotMirror() const { return !_flags[MIRROR]; } bool isMirror() const { return _flags[MIRROR]; } + bool isNotSimulate() const { return !_flags[SIMULATE]; } + bool isSimulate() const { return _flags[SIMULATE]; } + bool isTag(Tag tag) const { return _flags[FIRST_TAG_BIT + tag]; } uint8_t getTagBits() const { return ((_flags.to_ulong() & KEY_TAG_BITS_MASK) >> FIRST_TAG_BIT); } @@ -285,6 +290,9 @@ public: Builder& withoutMirror() { _value.reset(ItemKey::MIRROR); _mask.set(ItemKey::MIRROR); return (*this); } Builder& withMirror() { _value.set(ItemKey::MIRROR); _mask.set(ItemKey::MIRROR); return (*this); } + Builder& withoutSimulate() { _value.reset(ItemKey::SIMULATE); _mask.set(ItemKey::SIMULATE); return (*this); } + Builder& withSimulate() { _value.set(ItemKey::SIMULATE); _mask.set(ItemKey::SIMULATE); return (*this); } + Builder& withoutTag(ItemKey::Tag tagIndex) { _value.reset(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } Builder& withTag(ItemKey::Tag tagIndex) { _value.set(ItemKey::FIRST_TAG_BIT + tagIndex); _mask.set(ItemKey::FIRST_TAG_BIT + tagIndex); return (*this); } // Set ALL the tags in one call using the Tag bits and the Tag bits touched @@ -443,6 +451,7 @@ public: virtual const ItemKey getKey() const = 0; virtual const Bound getBound(RenderArgs* args) const = 0; virtual void render(RenderArgs* args) = 0; + virtual void renderSimulate(RenderArgs* args) = 0; virtual const ShapeKey getShapeKey() const = 0; @@ -496,6 +505,9 @@ public: // Render call for the item void render(RenderArgs* args) const { _payload->render(args); } + // Render-side simulate call for the item + void renderSimulate(RenderArgs* args) { _payload->renderSimulate(args); } + // Shape Type Interface const ShapeKey getShapeKey() const; @@ -546,6 +558,7 @@ inline QDebug operator<<(QDebug debug, const Item& item) { template const ItemKey payloadGetKey(const std::shared_ptr& payloadData) { return ItemKey(); } template const Item::Bound payloadGetBound(const std::shared_ptr& payloadData, RenderArgs* args) { return Item::Bound(); } template void payloadRender(const std::shared_ptr& payloadData, RenderArgs* args) { } +template void payloadRenderSimulate(const std::shared_ptr& payloadData, RenderArgs* args) { } // Shape type interface // This allows shapes to characterize their pipeline via a ShapeKey, to be picked with a subclass of Shape. @@ -581,6 +594,7 @@ public: virtual const Item::Bound getBound(RenderArgs* args) const override { return payloadGetBound(_data, args); } virtual void render(RenderArgs* args) override { payloadRender(_data, args); } + virtual void renderSimulate(RenderArgs* args) override { payloadRenderSimulate(_data, args); } // Shape Type interface virtual const ShapeKey getShapeKey() const override { return shapeGetShapeKey(_data); } @@ -645,6 +659,7 @@ public: virtual ShapeKey getShapeKey() = 0; virtual Item::Bound getBound(RenderArgs* args) = 0; virtual void render(RenderArgs* args) = 0; + virtual void renderSimulate(RenderArgs* args) = 0; virtual uint32_t metaFetchMetaSubItems(ItemIDs& subItems) const = 0; virtual bool passesZoneOcclusionTest(const std::unordered_set& containingZones) const = 0; virtual ItemID computeMirrorView(ViewFrustum& viewFrustum) const = 0; @@ -657,6 +672,7 @@ public: template <> const ItemKey payloadGetKey(const PayloadProxyInterface::Pointer& payload); template <> const Item::Bound payloadGetBound(const PayloadProxyInterface::Pointer& payload, RenderArgs* args); template <> void payloadRender(const PayloadProxyInterface::Pointer& payload, RenderArgs* args); +template <> void payloadRenderSimulate(const PayloadProxyInterface::Pointer& payload, RenderArgs* args); template <> uint32_t metaFetchMetaSubItems(const PayloadProxyInterface::Pointer& payload, ItemIDs& subItems); template <> const ShapeKey shapeGetShapeKey(const PayloadProxyInterface::Pointer& payload); template <> bool payloadPassesZoneOcclusionTest(const PayloadProxyInterface::Pointer& payload, const std::unordered_set& containingZones); diff --git a/libraries/render/src/render/RenderFetchCullSortTask.cpp b/libraries/render/src/render/RenderFetchCullSortTask.cpp index 3bdaee25c6..4619c30de4 100644 --- a/libraries/render/src/render/RenderFetchCullSortTask.cpp +++ b/libraries/render/src/render/RenderFetchCullSortTask.cpp @@ -35,17 +35,19 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin const auto nonspatialSelection = task.addJob("FetchLayeredSelection", nonspatialFilter); // Multi filter visible items into different buckets - const int NUM_SPATIAL_FILTERS = 5; - const int NUM_NON_SPATIAL_FILTERS = 3; + const int NUM_SPATIAL_FILTERS = 6; + const int NUM_NON_SPATIAL_FILTERS = 4; const int OPAQUE_SHAPE_BUCKET = 0; const int TRANSPARENT_SHAPE_BUCKET = 1; - const int LIGHT_BUCKET = 2; - const int META_BUCKET = 3; - const int MIRROR_BUCKET = 4; - const int BACKGROUND_BUCKET = 2; + const int SIMULATE_BUCKET = 2; + const int LIGHT_BUCKET = 3; + const int META_BUCKET = 4; + const int MIRROR_BUCKET = 5; + const int BACKGROUND_BUCKET = 3; MultiFilterItems::ItemFilterArray spatialFilters = { { ItemFilter::Builder::opaqueShape().withoutMirror(), ItemFilter::Builder::transparentShape(), + ItemFilter::Builder().withSimulate(), ItemFilter::Builder::light(), ItemFilter::Builder::meta().withoutMirror(), ItemFilter::Builder::mirror() @@ -53,6 +55,7 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin MultiFilterItems::ItemFilterArray nonspatialFilters = { { ItemFilter::Builder::opaqueShape(), ItemFilter::Builder::transparentShape(), + ItemFilter::Builder().withSimulate(), ItemFilter::Builder::background() } }; const auto filteredSpatialBuckets = @@ -77,9 +80,13 @@ void RenderFetchCullSortTask::build(JobModel& task, const Varying& input, Varyin const auto filteredLayeredOpaque = task.addJob("FilterLayeredOpaque", layeredOpaques, ItemKey::Layer::LAYER_1); const auto filteredLayeredTransparent = task.addJob("FilterLayeredTransparent", layeredTransparents, ItemKey::Layer::LAYER_1); + // collect our simulate objects from both buckets + const auto mergeInputs = MergeItems::Inputs(filteredSpatialBuckets[SIMULATE_BUCKET], filteredNonspatialBuckets[SIMULATE_BUCKET]).asVarying(); + const auto simulate = task.addJob("MergeSimulateItems", mergeInputs); + task.addJob("ClearContainingZones"); - output = Output(BucketList{ opaques, transparents, lights, metas, mirrors, + output = Output(BucketList{ opaques, transparents, lights, metas, mirrors, simulate, filteredLayeredOpaque.getN(0), filteredLayeredTransparent.getN(0), filteredLayeredOpaque.getN(1), filteredLayeredTransparent.getN(1), background }, spatialSelection); diff --git a/libraries/render/src/render/RenderFetchCullSortTask.h b/libraries/render/src/render/RenderFetchCullSortTask.h index 9823c2acdf..ffbd815167 100644 --- a/libraries/render/src/render/RenderFetchCullSortTask.h +++ b/libraries/render/src/render/RenderFetchCullSortTask.h @@ -24,6 +24,7 @@ public: LIGHT, META, MIRROR, + SIMULATE, LAYER_FRONT_OPAQUE_SHAPE, LAYER_FRONT_TRANSPARENT_SHAPE, LAYER_HUD_OPAQUE_SHAPE, diff --git a/libraries/render/src/render/Scene.h b/libraries/render/src/render/Scene.h index 1bc282646b..4dbcaa4e8f 100644 --- a/libraries/render/src/render/Scene.h +++ b/libraries/render/src/render/Scene.h @@ -194,6 +194,8 @@ public: void setItemTransition(ItemID id, Index transitionId); void removeItemTransition(ItemID id); + void simulate(ItemID id, RenderArgs* args) { _items[id].renderSimulate(args); } + protected: // Thread safe elements that can be accessed from anywhere diff --git a/scripts/system/create/assets/data/createAppTooltips.json b/scripts/system/create/assets/data/createAppTooltips.json index 795974da91..2c53fd227f 100644 --- a/scripts/system/create/assets/data/createAppTooltips.json +++ b/scripts/system/create/assets/data/createAppTooltips.json @@ -710,6 +710,24 @@ "zTextureURL": { "tooltip": "The URL of the texture to map to surfaces perpendicular to the entity's local z-axis. JPG or PNG format." }, + "numParticles": { + "tooltip": "The maximum number of particles to render at one time." + }, + "numTrianglesPerParticle": { + "tooltip": "The number of triangles to render per particle." + }, + "numUpdateProps": { + "tooltip": "The number of persistent Vec4 per-particle properties to use during simulation and rendering." + }, + "particleTransparent": { + "tooltip": "If the particles should render as transparent (with additive blending) or as opaque." + }, + "particleUpdateData": { + "tooltip": "A JSON description of the shaders, textures, and uniforms to use during particle updating." + }, + "particleRenderData": { + "tooltip": "A JSON description of the shaders, textures, and uniforms to use during particle rendering." + }, "soundURL": { "tooltip": "The URL of the sound, as a wav, mp3, or raw file." }, @@ -734,4 +752,4 @@ "localOnly": { "tooltip": "Whether the sound should play locally for everyone separately, or globally via the audio mixer." } -} +} \ No newline at end of file diff --git a/scripts/system/create/edit.js b/scripts/system/create/edit.js index 27502af711..72204ebd68 100644 --- a/scripts/system/create/edit.js +++ b/scripts/system/create/edit.js @@ -96,7 +96,7 @@ var MATERIAL_URL = Script.resolvePath("assets/images/icon-material.svg"); var SOUND_URL = Script.resolvePath("assets/images/icon-sound.svg"); - var entityIconOverlayManager = new EntityIconOverlayManager(["Light", "ParticleEffect", "Zone", "Material", "Sound"], function(entityID) { + var entityIconOverlayManager = new EntityIconOverlayManager(["Light", "ParticleEffect", "ProceduralParticleEffect", "Zone", "Material", "Sound"], function(entityID) { var properties = Entities.getEntityProperties(entityID, ["type", "isSpotlight", "parentID", "name"]); if (properties.type === "Light") { return { @@ -486,6 +486,31 @@ azimuthStart: -Math.PI, azimuthFinish: Math.PI }, + ProceduralParticleEffect: { + 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 }, intensity: 5.0, @@ -581,7 +606,8 @@ properties.grab = {}; if (Menu.isOptionChecked(MENU_CREATE_ENTITIES_GRABBABLE) && !(properties.type === "Zone" || properties.type === "Light" || properties.type === "Sound" - || properties.type === "ParticleEffect" || properties.type === "Web")) { + || properties.type === "ParticleEffect" || properties.type == "ProceduralParticleEffect" + || properties.type === "Web")) { properties.grab.grabbable = true; } else { properties.grab.grabbable = false; @@ -869,6 +895,14 @@ } } + function handleNewParticleDialogResult(result) { + if (result) { + createNewEntity({ + type: result.procedural ? "ProceduralParticleEffect" : "ParticleEffect" + }); + } + } + function handleNewSoundDialogResult(result) { if (result) { var soundURL = result.textInput; @@ -902,6 +936,13 @@ case "newMaterialDialogCancel": closeExistingDialogWindow(); break; + case "newParticleDialogAdd": + handleNewParticleDialogResult(message.params); + closeExistingDialogWindow(); + break; + case "newParticleDialogCancel": + closeExistingDialogWindow(); + break; case "newPolyVoxDialogAdd": handleNewPolyVoxDialogResult(message.params); closeExistingDialogWindow(); @@ -1075,11 +1116,7 @@ }); }); - addButton("newParticleButton", function () { - createNewEntity({ - type: "ParticleEffect", - }); - }); + addButton("newParticleButton", createNewEntityDialogButtonCallback("Particle")); addButton("newMaterialButton", createNewEntityDialogButtonCallback("Material")); @@ -2072,7 +2109,7 @@ var entityParentIDs = []; var propType = Entities.getEntityProperties(pastedEntityIDs[0], ["type"]).type; - var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "Sound", "ParticleEffect"]; + var NO_ADJUST_ENTITY_TYPES = ["Zone", "Light", "ParticleEffect", "ProceduralParticleEffect", "Sound"]; if (NO_ADJUST_ENTITY_TYPES.indexOf(propType) === -1) { var targetDirection; if (Camera.mode === "entity" || Camera.mode === "independent") { @@ -2625,7 +2662,7 @@ if (data.snapToGrid !== undefined) { entityListTool.setListMenuSnapToGrid(data.snapToGrid); } - } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData') { + } else if (data.type === 'saveUserData' || data.type === 'saveMaterialData' || data.type === 'saveParticleUpdateData' || data.type === 'saveParticleRenderData') { data.ids.forEach(function(entityID) { Entities.editEntity(entityID, data.properties); }); diff --git a/scripts/system/create/entityList/html/js/entityList.js b/scripts/system/create/entityList/html/js/entityList.js index 213e4ad09c..a7cd70d837 100644 --- a/scripts/system/create/entityList/html/js/entityList.js +++ b/scripts/system/create/entityList/html/js/entityList.js @@ -174,6 +174,7 @@ const FILTER_TYPES = [ "Web", "Material", "ParticleEffect", + "ProceduralParticleEffect", "PolyLine", "PolyVox", "Text", diff --git a/scripts/system/create/entityProperties/html/js/entityProperties.js b/scripts/system/create/entityProperties/html/js/entityProperties.js index b5aeb3d69d..d644bc2d0b 100644 --- a/scripts/system/create/entityProperties/html/js/entityProperties.js +++ b/scripts/system/create/entityProperties/html/js/entityProperties.js @@ -1386,6 +1386,54 @@ const GROUPS = [ } ] }, + { + id: "particles_procedural", + label: "PROCEDURAL PARTICLES", + properties: [ + { + label: "Particles", + type: "number-draggable", + propertyID: "numParticles", + min: 1, + max: 1000000 + }, + { + label: "Triangles Per Particle", + type: "number-draggable", + propertyID: "numTrianglesPerParticle", + min: 1, + max: 15 + }, + { + label: "Update Props", + type: "number-draggable", + propertyID: "numUpdateProps", + min: 0, + max: 5 + }, + { + label: "Transparent", + type: "bool", + propertyID: "particleTransparent", + }, + { + label: "Particle Update Data", + type: "textarea", + buttons: [{ id: "clear", label: "Clear Update Data", className: "secondary_red red", onClick: clearParticleUpdateData }, + { id: "edit", label: "Edit as JSON", className: "secondary", onClick: newJSONParticleUpdateEditor }, + { id: "save", label: "Save Update Data", className: "secondary", onClick: saveParticleUpdateData }], + propertyID: "particleUpdateData", + }, + { + label: "Particle Render Data", + type: "textarea", + buttons: [{ id: "clear", label: "Clear Render Data", className: "secondary_red red", onClick: clearParticleRenderData }, + { id: "edit", label: "Edit as JSON", className: "secondary", onClick: newJSONParticleRenderEditor }, + { id: "save", label: "Save Render Data", className: "secondary", onClick: saveParticleRenderData }], + propertyID: "particleRenderData", + } + ] + }, { id: "polyvox", label: "POLYVOX", @@ -1869,6 +1917,7 @@ const GROUPS_PER_TYPE = { Material: [ 'base', 'material', 'spatial', 'behavior', 'scripts', 'physics' ], ParticleEffect: [ 'base', 'particles', 'particles_emit', 'particles_size', 'particles_color', 'particles_behavior', 'particles_constraints', 'spatial', 'behavior', 'scripts', 'physics' ], + ProceduralParticleEffect: [ 'base', 'particles_procedural', 'spatial', 'behavior', 'scripts', 'physics' ], PolyLine: [ 'base', 'spatial', 'behavior', 'scripts', 'collision', 'physics' ], PolyVox: [ 'base', 'polyvox', 'spatial', 'behavior', 'scripts', 'collision', 'physics' ], Grid: [ 'base', 'grid', 'spatial', 'behavior', 'scripts', 'physics' ], @@ -3848,6 +3897,327 @@ function saveJSONMaterialData(noUpdate, entityIDsToUpdate) { }, EDITOR_TIMEOUT_DURATION); } + +/** + * PROCEDURAL PARTICLE DATA FUNCTIONS + */ + +function clearParticleUpdateData() { + let elParticleUpdateData = getPropertyInputElement("particleUpdateData"); + deleteJSONParticleUpdateEditor(); + elParticleUpdateData.value = ""; + showParticleUpdateDataTextArea(); + showNewJSONParticleUpdateEditorButton(); + hideSaveParticleUpdateDataButton(); + updateProperty('particleUpdateData', elParticleUpdateData.value, false); +} + +function newJSONParticleUpdateEditor() { + getPropertyInputElement("particleUpdateData").classList.remove('multi-diff'); + deleteJSONParticleUpdateEditor(); + createJSONParticleUpdateEditor(); + let data = {}; + setParticleUpdateEditorJSON(data); + hideParticleUpdateDataTextArea(); + hideNewJSONParticleUpdateEditorButton(); + showSaveParticleUpdateDataButton(); +} + +/** + * @param {Set.} [entityIDsToUpdate] Entity IDs to update particleUpdateData for. + */ +function saveParticleUpdateData(entityIDsToUpdate) { + saveJSONParticleUpdateData(true, entityIDsToUpdate); +} + +function setJSONError(property, isError) { + $("#property-"+ property + "-editor").toggleClass('error', isError); + let $propertyParticleUpdateDataEditorStatus = $("#property-"+ property + "-editorStatus"); + $propertyParticleUpdateDataEditorStatus.css('display', isError ? 'block' : 'none'); + $propertyParticleUpdateDataEditorStatus.text(isError ? 'Invalid JSON code - look for red X in your code' : ''); +} + +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update particleUpdateData for. + */ +function setParticleUpdateDataFromEditor(noUpdate, entityIDsToUpdate) { + let errorFound = false; + try { + particleUpdateEditor.get(); + } catch (e) { + errorFound = true; + } + + setJSONError('particleUpdateData', errorFound); + + if (errorFound) { + return; + } + + let text = particleUpdateEditor.getText(); + if (noUpdate) { + EventBridge.emitWebEvent( + JSON.stringify({ + ids: [...entityIDsToUpdate], + type: "saveParticleUpdateData", + properties: { + particleUpdateData: text + } + }) + ); + } else { + updateProperty('particleUpdateData', text, false); + } +} + +let particleUpdateEditor = null; + +function createJSONParticleUpdateEditor() { + let container = document.getElementById("property-particleUpdateData-editor"); + let options = { + search: false, + mode: 'tree', + modes: ['code', 'tree'], + name: 'particleUpdateData', + onError: function(e) { + alert('JSON editor:' + e); + }, + onChange: function() { + let currentJSONString = particleUpdateEditor.getText(); + + if (currentJSONString === '{"":""}') { + return; + } + $('#property-particleUpdateData-button-save').attr('disabled', false); + } + }; + particleUpdateEditor = new JSONEditor(container, options); +} + +function showSaveParticleUpdateDataButton() { + $('#property-particleUpdateData-button-save').show(); +} + +function hideSaveParticleUpdateDataButton() { + $('#property-particleUpdateData-button-save').hide(); +} + +function disableSaveParticleUpdateDataButton() { + $('#property-particleUpdateData-button-save').attr('disabled', true); +} + +function showNewJSONParticleUpdateEditorButton() { + $('#property-particleUpdateData-button-edit').show(); +} + +function hideNewJSONParticleUpdateEditorButton() { + $('#property-particleUpdateData-button-edit').hide(); +} + +function showParticleUpdateDataTextArea() { + $('#property-particleUpdateData').show(); +} + +function hideParticleUpdateDataTextArea() { + $('#property-particleUpdateData').hide(); +} + +function hideParticleUpdateDataSaved() { + $('#property-particleUpdateData-saved').hide(); +} + +function setParticleUpdateEditorJSON(json) { + particleUpdateEditor.set(json); + if (particleUpdateEditor.hasOwnProperty('expandAll')) { + particleUpdateEditor.expandAll(); + } +} + +function deleteJSONParticleUpdateEditor() { + if (particleUpdateEditor !== null) { + setJSONError('particleUpdateData', false); + particleUpdateEditor.destroy(); + particleUpdateEditor = null; + } +} + +let savedParticleUpdateJSONTimer = null; + +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] Entity IDs to update particleUpdateData for + */ +function saveJSONParticleUpdateData(noUpdate, entityIDsToUpdate) { + setParticleUpdateDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); + $('#property-particleUpdateData-saved').show(); + $('#property-particleUpdateData-button-save').attr('disabled', true); + if (savedJSONTimer !== null) { + clearTimeout(savedJSONTimer); + } + savedJSONTimer = setTimeout(function() { + hideParticleUpdateDataSaved(); + }, EDITOR_TIMEOUT_DURATION); +} + +function clearParticleRenderData() { + let elParticleRenderData = getPropertyInputElement("particleRenderData"); + deleteJSONParticleRenderEditor(); + elParticleRenderData.value = ""; + showParticleRenderDataTextArea(); + showNewJSONParticleRenderEditorButton(); + hideSaveParticleRenderDataButton(); + updateProperty('particleRenderData', elParticleRenderData.value, false); +} + +function newJSONParticleRenderEditor() { + getPropertyInputElement("particleRenderData").classList.remove('multi-diff'); + deleteJSONParticleRenderEditor(); + createJSONParticleRenderEditor(); + let data = {}; + setParticleRenderEditorJSON(data); + hideParticleRenderDataTextArea(); + hideNewJSONParticleRenderEditorButton(); + showSaveParticleRenderDataButton(); +} + +/** + * @param {Set.} [entityIDsToUpdate] Entity IDs to update particleRenderData for. + */ +function saveParticleRenderData(entityIDsToUpdate) { + saveJSONParticleRenderData(true, entityIDsToUpdate); +} + +function setJSONError(property, isError) { + $("#property-"+ property + "-editor").toggleClass('error', isError); + let $propertyParticleRenderDataEditorStatus = $("#property-"+ property + "-editorStatus"); + $propertyParticleRenderDataEditorStatus.css('display', isError ? 'block' : 'none'); + $propertyParticleRenderDataEditorStatus.text(isError ? 'Invalid JSON code - look for red X in your code' : ''); +} + +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] - Entity IDs to update particleRenderData for. + */ +function setParticleRenderDataFromEditor(noUpdate, entityIDsToUpdate) { + let errorFound = false; + try { + particleRenderEditor.get(); + } catch (e) { + errorFound = true; + } + + setJSONError('particleRenderData', errorFound); + + if (errorFound) { + return; + } + + let text = particleRenderEditor.getText(); + if (noUpdate) { + EventBridge.emitWebEvent( + JSON.stringify({ + ids: [...entityIDsToUpdate], + type: "saveParticleRenderData", + properties: { + particleRenderData: text + } + }) + ); + } else { + updateProperty('particleRenderData', text, false); + } +} + +let particleRenderEditor = null; + +function createJSONParticleRenderEditor() { + let container = document.getElementById("property-particleRenderData-editor"); + let options = { + search: false, + mode: 'tree', + modes: ['code', 'tree'], + name: 'particleRenderData', + onError: function(e) { + alert('JSON editor:' + e); + }, + onChange: function() { + let currentJSONString = particleRenderEditor.getText(); + + if (currentJSONString === '{"":""}') { + return; + } + $('#property-particleRenderData-button-save').attr('disabled', false); + } + }; + particleRenderEditor = new JSONEditor(container, options); +} + +function showSaveParticleRenderDataButton() { + $('#property-particleRenderData-button-save').show(); +} + +function hideSaveParticleRenderDataButton() { + $('#property-particleRenderData-button-save').hide(); +} + +function disableSaveParticleRenderDataButton() { + $('#property-particleRenderData-button-save').attr('disabled', true); +} + +function showNewJSONParticleRenderEditorButton() { + $('#property-particleRenderData-button-edit').show(); +} + +function hideNewJSONParticleRenderEditorButton() { + $('#property-particleRenderData-button-edit').hide(); +} + +function showParticleRenderDataTextArea() { + $('#property-particleRenderData').show(); +} + +function hideParticleRenderDataTextArea() { + $('#property-particleRenderData').hide(); +} + +function hideParticleRenderDataSaved() { + $('#property-particleRenderData-saved').hide(); +} + +function setParticleRenderEditorJSON(json) { + particleRenderEditor.set(json); + if (particleRenderEditor.hasOwnProperty('expandAll')) { + particleRenderEditor.expandAll(); + } +} + +function deleteJSONParticleRenderEditor() { + if (particleRenderEditor !== null) { + setJSONError('particleRenderData', false); + particleRenderEditor.destroy(); + particleRenderEditor = null; + } +} + +let savedParticleRenderJSONTimer = null; + +/** + * @param {boolean} noUpdate - don't update the UI, but do send a property update. + * @param {Set.} [entityIDsToUpdate] Entity IDs to update particleRenderData for + */ +function saveJSONParticleRenderData(noUpdate, entityIDsToUpdate) { + setParticleRenderDataFromEditor(noUpdate, entityIDsToUpdate ? entityIDsToUpdate : selectedEntityIDs); + $('#property-particleRenderData-saved').show(); + $('#property-particleRenderData-button-save').attr('disabled', true); + if (savedJSONTimer !== null) { + clearTimeout(savedJSONTimer); + } + savedJSONTimer = setTimeout(function() { + hideParticleRenderDataSaved(); + }, EDITOR_TIMEOUT_DURATION); +} + function bindAllNonJSONEditorElements() { let inputs = $('input'); let i; @@ -3858,7 +4228,9 @@ function bindAllNonJSONEditorElements() { // an outer scoped variable may lead to confusing semantics. field.on('focus', function(e) { if (e.target.id === "property-userData-button-edit" || e.target.id === "property-userData-button-clear" || - e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear") { + e.target.id === "property-materialData-button-edit" || e.target.id === "property-materialData-button-clear" || + e.target.id === "property-particleUpdateData-button-edit" || e.target.id === "property-particleUpdateData-button-clear" || + e.target.id === "property-particleRenderData-button-edit" || e.target.id === "property-particleRenderData-button-clear") { return; } if ($('#property-userData-editor').css('height') !== "0px") { @@ -3867,6 +4239,12 @@ function bindAllNonJSONEditorElements() { if ($('#property-materialData-editor').css('height') !== "0px") { saveMaterialData(); } + if ($('#property-particleUpdateData-editor').css('height') !== "0px") { + saveParticleUpdateData(); + } + if ($('#property-particleRenderData-editor').css('height') !== "0px") { + saveParticleRenderData(); + } }); } } @@ -4275,6 +4653,8 @@ function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { if (selections.length === 0) { deleteJSONEditor(); deleteJSONMaterialEditor(); + deleteJSONParticleUpdateEditor(); + deleteJSONParticleRenderEditor(); resetProperties(); showGroupsForType("None"); @@ -4293,6 +4673,16 @@ function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { showSaveMaterialDataButton(); showNewJSONMaterialEditorButton(); + getPropertyInputElement("particleUpdateData").value = ""; + showParticleUpdateDataTextArea(); + showSaveParticleUpdateDataButton(); + showNewJSONParticleUpdateEditorButton(); + + getPropertyInputElement("particleRenderData").value = ""; + showParticleRenderDataTextArea(); + showSaveParticleRenderDataButton(); + showNewJSONParticleRenderEditorButton(); + setCopyPastePositionAndRotationAvailability (selections.length, true); disableProperties(); @@ -4330,6 +4720,8 @@ function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { enableProperties(); disableSaveUserDataButton(); disableSaveMaterialDataButton(); + disableSaveParticleUpdateDataButton(); + disableSaveParticleRenderDataButton(); setCopyPastePositionAndRotationAvailability (selections.length, false); } @@ -4598,6 +4990,70 @@ function handleEntitySelectionUpdate(selections, isPropertiesToolUpdate) { requestMaterialTarget(); } + let particleUpdateDataMultiValue = getMultiplePropertyValue("particleUpdateData"); + let particleUpdateDataTextArea = getPropertyInputElement("particleUpdateData"); + let particleUpdateJSON = null; + if (!particleUpdateDataMultiValue.isMultiDiffValue) { + try { + particleUpdateJSON = JSON.parse(particleUpdateDataMultiValue.value); + } catch (e) { + + } + } + if (particleUpdateJSON !== null && !lockedMultiValue.isMultiDiffValue && !lockedMultiValue.value) { + if (particleUpdateEditor === null) { + createJSONParticleUpdateEditor(); + } + particleUpdateDataTextArea.classList.remove('multi-diff'); + setParticleUpdateEditorJSON(particleUpdateJSON); + showSaveParticleUpdateDataButton(); + hideParticleUpdateDataTextArea(); + hideNewJSONParticleUpdateEditorButton(); + hideParticleUpdateDataSaved(); + } else { + // normal text + deleteJSONParticleUpdateEditor(); + particleUpdateDataTextArea.classList.toggle('multi-diff', particleUpdateDataMultiValue.isMultiDiffValue); + particleUpdateDataTextArea.value = particleUpdateDataMultiValue.isMultiDiffValue ? "" : particleUpdateDataMultiValue.value; + + showParticleUpdateDataTextArea(); + showNewJSONParticleUpdateEditorButton(); + hideSaveParticleUpdateDataButton(); + hideParticleUpdateDataSaved(); + } + + let particleRenderDataMultiValue = getMultiplePropertyValue("particleRenderData"); + let particleRenderDataTextArea = getPropertyInputElement("particleRenderData"); + let particleRenderJSON = null; + if (!particleRenderDataMultiValue.isMultiDiffValue) { + try { + particleRenderJSON = JSON.parse(particleRenderDataMultiValue.value); + } catch (e) { + + } + } + if (particleRenderJSON !== null && !lockedMultiValue.isMultiDiffValue && !lockedMultiValue.value) { + if (particleRenderEditor === null) { + createJSONParticleRenderEditor(); + } + particleRenderDataTextArea.classList.remove('multi-diff'); + setParticleRenderEditorJSON(particleRenderJSON); + showSaveParticleRenderDataButton(); + hideParticleRenderDataTextArea(); + hideNewJSONParticleRenderEditorButton(); + hideParticleRenderDataSaved(); + } else { + // normal text + deleteJSONParticleRenderEditor(); + particleRenderDataTextArea.classList.toggle('multi-diff', particleRenderDataMultiValue.isMultiDiffValue); + particleRenderDataTextArea.value = particleRenderDataMultiValue.isMultiDiffValue ? "" : particleRenderDataMultiValue.value; + + showParticleRenderDataTextArea(); + showNewJSONParticleRenderEditorButton(); + hideSaveParticleRenderDataButton(); + hideParticleRenderDataSaved(); + } + let activeElement = document.activeElement; if (doSelectElement && typeof activeElement.select !== "undefined") { activeElement.select(); @@ -4895,6 +5351,37 @@ function loaded() { elDiv.insertBefore(elMaterialDataEditor, elMaterialData); elDiv.insertBefore(elMaterialDataEditorStatus, elMaterialData); + // Particle Update + Render Data + let particleUpdateDataProperty = properties["particleUpdateData"]; + let elParticleUpdateData = particleUpdateDataProperty.elInput; + let particleUpdateDataElementID = particleUpdateDataProperty.elementID; + elDiv = elParticleUpdateData.parentNode; + let elParticleUpdateDataEditor = document.createElement('div'); + elParticleUpdateDataEditor.setAttribute("id", particleUpdateDataElementID + "-editor"); + let elParticleUpdateDataEditorStatus = document.createElement('div'); + elParticleUpdateDataEditorStatus.setAttribute("id", particleUpdateDataElementID + "-editorStatus"); + let elParticleUpdateDataSaved = document.createElement('span'); + elParticleUpdateDataSaved.setAttribute("id", particleUpdateDataElementID + "-saved"); + elParticleUpdateDataSaved.innerText = "Saved!"; + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elParticleUpdateDataSaved); + elDiv.insertBefore(elParticleUpdateDataEditor, elParticleUpdateData); + elDiv.insertBefore(elParticleUpdateDataEditorStatus, elParticleUpdateData); + + let particleRenderDataProperty = properties["particleRenderData"]; + let elParticleRenderData = particleRenderDataProperty.elInput; + let particleRenderDataElementID = particleRenderDataProperty.elementID; + elDiv = elParticleRenderData.parentNode; + let elParticleRenderDataEditor = document.createElement('div'); + elParticleRenderDataEditor.setAttribute("id", particleRenderDataElementID + "-editor"); + let elParticleRenderDataEditorStatus = document.createElement('div'); + elParticleRenderDataEditorStatus.setAttribute("id", particleRenderDataElementID + "-editorStatus"); + let elParticleRenderDataSaved = document.createElement('span'); + elParticleRenderDataSaved.setAttribute("id", particleRenderDataElementID + "-saved"); + elParticleRenderDataSaved.innerText = "Saved!"; + elDiv.childNodes[JSON_EDITOR_ROW_DIV_INDEX].appendChild(elParticleRenderDataSaved); + elDiv.insertBefore(elParticleRenderDataEditor, elParticleRenderData); + elDiv.insertBefore(elParticleRenderDataEditorStatus, elParticleRenderData); + // Textarea scrollbars let elTextareas = document.getElementsByTagName("TEXTAREA"); @@ -5008,7 +5495,8 @@ function loaded() { return; } - if (elUserDataEditor.contains(keyUpEvent.target) || elMaterialDataEditor.contains(keyUpEvent.target)) { + if (elUserDataEditor.contains(keyUpEvent.target) || elMaterialDataEditor.contains(keyUpEvent.target) || elParticleUpdateDataEditor.contains(keyUpEvent.target) + || elParticleRenderDataEditor.contains(keyUpEvent.target)) { return; } diff --git a/scripts/system/create/entityProperties/html/tabs/particles_procedural.png b/scripts/system/create/entityProperties/html/tabs/particles_procedural.png new file mode 100644 index 0000000000..6a0d47cacb Binary files /dev/null and b/scripts/system/create/entityProperties/html/tabs/particles_procedural.png differ diff --git a/scripts/system/create/entitySelectionTool/entitySelectionTool.js b/scripts/system/create/entitySelectionTool/entitySelectionTool.js index 5238b2d374..c2edc517a6 100644 --- a/scripts/system/create/entitySelectionTool/entitySelectionTool.js +++ b/scripts/system/create/entitySelectionTool/entitySelectionTool.js @@ -2241,7 +2241,7 @@ SelectionDisplay = (function() { Entities.editEntity(selectionBox, selectionBoxGeometry); // UPDATE ICON TRANSLATE HANDLE - if (SelectionManager.entityType === "ParticleEffect" || SelectionManager.entityType === "Light" || SelectionManager.entityType === "Sound") { + if (SelectionManager.entityType === "ParticleEffect" || SelectionManager.entityType === "ProceduralParticleEffect" || SelectionManager.entityType === "Light" || SelectionManager.entityType === "Sound") { var iconSelectionBoxGeometry = { position: position, rotation: rotation diff --git a/scripts/system/create/qml/NewMaterialDialog.qml b/scripts/system/create/qml/NewMaterialDialog.qml index e08ca868b6..ab61bc05a2 100644 --- a/scripts/system/create/qml/NewMaterialDialog.qml +++ b/scripts/system/create/qml/NewMaterialDialog.qml @@ -27,7 +27,6 @@ Rectangle { signal sendToScript(var message); property bool keyboardEnabled: false property bool punctuationMode: false - property bool keyboardRasied: false function errorMessageBox(message) { try { diff --git a/scripts/system/create/qml/NewModelDialog.qml b/scripts/system/create/qml/NewModelDialog.qml index c03e7c1c35..8df6bad33f 100644 --- a/scripts/system/create/qml/NewModelDialog.qml +++ b/scripts/system/create/qml/NewModelDialog.qml @@ -27,7 +27,6 @@ Rectangle { property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false - property bool keyboardRasied: false function errorMessageBox(message) { try { diff --git a/scripts/system/create/qml/NewParticleDialog.qml b/scripts/system/create/qml/NewParticleDialog.qml new file mode 100644 index 0000000000..c07843f30a --- /dev/null +++ b/scripts/system/create/qml/NewParticleDialog.qml @@ -0,0 +1,108 @@ +// +// NewParticleDialog.qml +// qml/hifi +// +// Created by HifiExperiments on 11/22/23 +// Copyright 2023 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Dialogs 1.2 as OriginalDialogs + +import stylesUit 1.0 +import controlsUit 1.0 +import hifi.dialogs 1.0 + +Rectangle { + id: newParticleDialog + // width: parent.width + // height: parent.height + HifiConstants { id: hifi } + color: hifi.colors.baseGray; + signal sendToScript(var message); + property bool keyboardEnabled: false + property bool keyboardRaised: false + property bool punctuationMode: false + + function errorMessageBox(message) { + try { + return desktop.messageBox({ + icon: hifi.icons.warning, + defaultButton: OriginalDialogs.StandardButton.Ok, + title: "Error", + text: message + }); + } catch(e) { + Window.alert(message); + } + } + + Item { + id: column1 + anchors.rightMargin: 10 + anchors.leftMargin: 10 + anchors.bottomMargin: 10 + anchors.topMargin: 10 + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: keyboard.top + + Column { + id: column2 + height: 400 + spacing: 10 + + CheckBox { + id: procedural + text: qsTr("Procedural (GPU) Particles?") + checked: false + } + + Row { + id: row1 + width: 200 + height: 400 + spacing: 5 + + Button { + id: button1 + text: qsTr("Create") + z: -1 + onClicked: { + newParticleDialog.sendToScript({ + method: "newParticleDialogAdd", + params: { + procedural: procedural.checked + } + }); + } + } + + Button { + id: button2 + z: -1 + text: qsTr("Cancel") + onClicked: { + newParticleDialog.sendToScript({method: "newParticleDialogCancel"}) + } + } + } + } + } + + Keyboard { + id: keyboard + raised: parent.keyboardEnabled && parent.keyboardRaised + numeric: parent.punctuationMode + anchors { + bottom: parent.bottom + bottomMargin: 40 + left: parent.left + right: parent.right + } + } +} diff --git a/scripts/system/create/qml/NewParticleWindow.qml b/scripts/system/create/qml/NewParticleWindow.qml new file mode 100644 index 0000000000..2b19500807 --- /dev/null +++ b/scripts/system/create/qml/NewParticleWindow.qml @@ -0,0 +1,20 @@ +import QtQuick 2.7 +import QtQuick.Controls 2.2 + +StackView { + id: stackView + anchors.fill: parent + anchors.leftMargin: 10 + anchors.rightMargin: 10 + anchors.topMargin: 40 + + signal sendToScript(var message); + + NewParticleDialog { + id: dialog + anchors.fill: parent + Component.onCompleted:{ + dialog.sendToScript.connect(stackView.sendToScript); + } + } +} diff --git a/scripts/system/create/qml/NewPolyVoxDialog.qml b/scripts/system/create/qml/NewPolyVoxDialog.qml index 0f8ab5541d..c10b32840b 100644 --- a/scripts/system/create/qml/NewPolyVoxDialog.qml +++ b/scripts/system/create/qml/NewPolyVoxDialog.qml @@ -29,7 +29,6 @@ Rectangle { property bool keyboardEnabled: false property bool keyboardRaised: false property bool punctuationMode: false - property bool keyboardRasied: false function errorMessageBox(message) { try { diff --git a/scripts/system/html/css/edit-style.css b/scripts/system/html/css/edit-style.css index 70696abdab..4a87df5659 100644 --- a/scripts/system/html/css/edit-style.css +++ b/scripts/system/html/css/edit-style.css @@ -1712,7 +1712,9 @@ input#property-scale-button-reset { } #property-userData-static, -#property-materialData-static { +#property-materialData-static, +#property-particleUpdateData-static, +#property-particleRenderData-static { display: none; z-index: 99; position: absolute; @@ -1724,7 +1726,9 @@ input#property-scale-button-reset { } #property-userData-saved, -#property-materialData-saved { +#property-materialData-saved, +#property-particleUpdateData-saved +#property-particleRenderData-saved { margin-top: 5px; font-size: 16px; display: none; @@ -1930,23 +1934,17 @@ input.number-slider { cursor: pointer; } -#property-userData-editor.error { +#property-userData-editor.error, +#property-materialData-editor.error, +#property-particleUpdateData-editor.error, +#property-particleRenderData-editor.error { border: 2px solid red; } -#property-userData-editorStatus { - color: white; - background-color: red; - padding: 5px; - display: none; - cursor: pointer; -} - -#property-materialData-editor.error { - border: 2px solid red; -} - -#property-materialData-editorStatus { +#property-userData-editorStatus, +#property-materialData-editorStatus, +#property-particleUpdateData-editorStatus, +#property-particleRenderData-editorStatus { color: white; background-color: red; padding: 5px; diff --git a/scripts/system/html/js/includes.js b/scripts/system/html/js/includes.js index 69ae9bed2d..e313162831 100644 --- a/scripts/system/html/js/includes.js +++ b/scripts/system/html/js/includes.js @@ -16,6 +16,7 @@ const ENTITY_TYPE_ICON = { Material: "", Model: "", ParticleEffect: "", + ProceduralParticleEffect: "", PolyVox: "", PolyLine: "", Shape: "n",