// // RenderableParticleEffectEntityItem.cpp // interface/src // // Created by Jason Rickwald on 3/2/15. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "RenderableParticleEffectEntityItem.h" #include #include #include #include using namespace render; using namespace render::entities; static uint8_t CUSTOM_PIPELINE_NUMBER = 0; static gpu::Stream::FormatPointer _vertexFormat; static std::weak_ptr _texturedPipeline; static ShapePipelinePointer shapePipelineFactory(const ShapePlumber& plumber, const ShapeKey& key, RenderArgs* args) { auto texturedPipeline = _texturedPipeline.lock(); if (!texturedPipeline) { auto state = std::make_shared(); state->setCullMode(gpu::State::CULL_BACK); state->setDepthTest(true, false, gpu::LESS_EQUAL); state->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); PrepareStencil::testMask(*state); auto program = gpu::Shader::createProgram(shader::entities_renderer::program::textured_particle); _texturedPipeline = texturedPipeline = gpu::Pipeline::create(program, state); } return std::make_shared(texturedPipeline, nullptr, nullptr, nullptr); } struct GpuParticle { GpuParticle(const glm::vec3& xyzIn, const glm::vec2& uvIn) : xyz(xyzIn), uv(uvIn) {} glm::vec3 xyz; // Position glm::vec2 uv; // Lifetime + seed }; using GpuParticles = std::vector; ParticleEffectEntityRenderer::ParticleEffectEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { ParticleUniforms uniforms; _uniformBuffer = std::make_shared(sizeof(ParticleUniforms), (const gpu::Byte*) &uniforms); static std::once_flag once; std::call_once(once, [] { // As we create the first ParticuleSystem entity, let s register its special shapePIpeline factory: CUSTOM_PIPELINE_NUMBER = render::ShapePipeline::registerCustomShapePipelineFactory(shapePipelineFactory); _vertexFormat = std::make_shared(); _vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element::VEC3F_XYZ, offsetof(GpuParticle, xyz), gpu::Stream::PER_INSTANCE); _vertexFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element::VEC2F_UV, offsetof(GpuParticle, uv), gpu::Stream::PER_INSTANCE); }); } void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePointer& scene, Transaction& transaction, const TypedEntityPointer& entity) { auto newParticleProperties = entity->getParticleProperties(); if (!newParticleProperties.valid()) { qCWarning(entitiesrenderer) << "Bad particle properties"; } if (resultWithReadLock([&] { return _particleProperties != newParticleProperties; })) { _timeUntilNextEmit = 0; withWriteLock([&] { _particleProperties = newParticleProperties; if (!_prevEmitterShouldTrailInitialized) { _prevEmitterShouldTrailInitialized = true; _prevEmitterShouldTrail = _particleProperties.emission.shouldTrail; } }); } withWriteLock([&] { _pulseProperties = entity->getPulseProperties(); _shapeType = entity->getShapeType(); QString compoundShapeURL = entity->getCompoundShapeURL(); if (_compoundShapeURL != compoundShapeURL) { _compoundShapeURL = compoundShapeURL; _hasComputedTriangles = false; fetchGeometryResource(); } }); _emitting = entity->getIsEmitting(); bool textureEmpty = resultWithReadLock([&] { return _particleProperties.textures.isEmpty(); }); if (textureEmpty) { if (_networkTexture) { withWriteLock([&] { _networkTexture.reset(); }); } withWriteLock([&] { entity->setVisuallyReady(true); }); } else { bool textureNeedsUpdate = resultWithReadLock([&] { return !_networkTexture || _networkTexture->getURL() != QUrl(_particleProperties.textures); }); if (textureNeedsUpdate) { withWriteLock([&] { _networkTexture = DependencyManager::get()->getTexture(_particleProperties.textures); }); } if (_networkTexture) { withWriteLock([&] { entity->setVisuallyReady(_networkTexture->isFailed() || _networkTexture->isLoaded()); }); } } void* key = (void*)this; AbstractViewStateInterface::instance()->pushPostUpdateLambda(key, [this] () { withWriteLock([&] { updateModelTransformAndBound(); _renderTransform = getModelTransform(); }); }); } void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEntityPointer& entity) { // Fill in Uniforms structure ParticleUniforms particleUniforms; withReadLock([&] { particleUniforms.radius.start = _particleProperties.radius.range.start; particleUniforms.radius.middle = _particleProperties.radius.gradient.target; particleUniforms.radius.finish = _particleProperties.radius.range.finish; particleUniforms.radius.spread = _particleProperties.radius.gradient.spread; particleUniforms.spin.start = _particleProperties.spin.range.start; particleUniforms.spin.middle = _particleProperties.spin.gradient.target; particleUniforms.spin.finish = _particleProperties.spin.range.finish; particleUniforms.spin.spread = _particleProperties.spin.gradient.spread; particleUniforms.lifespan = _particleProperties.lifespan; particleUniforms.rotateWithEntity = _particleProperties.rotateWithEntity ? 1 : 0; }); // Update particle uniforms memcpy(&_uniformBuffer.edit(), &particleUniforms, sizeof(ParticleUniforms)); } ItemKey ParticleEffectEntityRenderer::getKey() { // FIXME: implement isTransparent() for particles and an opaque pipeline if (_visible) { return ItemKey::Builder::transparentShape().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()); } else { return ItemKey::Builder().withInvisible().withTagBits(getTagMask()).withLayer(getHifiRenderLayer()).build(); } } ShapeKey ParticleEffectEntityRenderer::getShapeKey() { auto builder = ShapeKey::Builder().withCustom(CUSTOM_PIPELINE_NUMBER).withTranslucent(); if (_primitiveMode == PrimitiveMode::LINES) { builder.withWireframe(); } return builder.build(); } Item::Bound ParticleEffectEntityRenderer::getBound() { return _bound; } // FIXME: these methods assume uniform emitDimensions, need to importance sample based on dimensions float importanceSample2DDimension(float startDim) { float dimension = 1.0f; if (startDim < 1.0f) { float innerDimensionSquared = startDim * startDim; float outerDimensionSquared = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 2); float randDimensionSquared = randFloatInRange(innerDimensionSquared, outerDimensionSquared); dimension = std::sqrt(randDimensionSquared); } return dimension; } float importanceSample3DDimension(float startDim) { float dimension = 1.0f; if (startDim < 1.0f) { float innerDimensionCubed = startDim * startDim * startDim; float outerDimensionCubed = 1.0f; // pow(particle::MAXIMUM_EMIT_RADIUS_START, 3); float randDimensionCubed = randFloatInRange(innerDimensionCubed, outerDimensionCubed); dimension = std::cbrt(randDimensionCubed); } return dimension; } ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties, const ShapeType& shapeType, const ModelResource::Pointer& geometryResource, const TriangleInfo& triangleInfo) { CpuParticle particle; const auto& accelerationSpread = particleProperties.emission.acceleration.spread; const auto& azimuthStart = particleProperties.azimuth.start; const auto& azimuthFinish = particleProperties.azimuth.finish; const auto& emitDimensions = particleProperties.emission.dimensions; const auto& emitAcceleration = particleProperties.emission.acceleration.target; auto emitOrientation = baseTransform.getRotation() * particleProperties.emission.orientation; const auto& emitRadiusStart = glm::max(particleProperties.radiusStart, EPSILON); // Avoid math complications at center const auto& emitSpeed = particleProperties.emission.speed.target; const auto& speedSpread = particleProperties.emission.speed.spread; const auto& polarStart = particleProperties.polar.start; const auto& polarFinish = particleProperties.polar.finish; particle.seed = randFloatInRange(-1.0f, 1.0f); particle.expiration = now + (uint64_t)(particleProperties.lifespan * USECS_PER_SECOND); particle.relativePosition = glm::vec3(0.0f); particle.basePosition = baseTransform.getTranslation(); // Position, velocity, and acceleration glm::vec3 emitDirection; if (polarStart == 0.0f && polarFinish == 0.0f && emitDimensions.z == 0.0f) { // Emit along z-axis from position emitDirection = Vectors::UNIT_Z; } else { // Emit around point or from ellipsoid // - Distribute directions evenly around point // - Distribute points relatively evenly over ellipsoid surface // - Distribute points relatively evenly within ellipsoid volume float elevationMinZ = sinf(PI_OVER_TWO - polarFinish); float elevationMaxZ = sinf(PI_OVER_TWO - polarStart); float elevation = asinf(elevationMinZ + (elevationMaxZ - elevationMinZ) * randFloat()); float azimuth; if (azimuthFinish >= azimuthStart) { azimuth = azimuthStart + (azimuthFinish - azimuthStart) * randFloat(); } else { azimuth = azimuthStart + (TWO_PI + azimuthFinish - azimuthStart) * randFloat(); } // TODO: azimuth and elevation are only used for ellipsoids/circles, but could be used for other shapes too if (emitDimensions == Vectors::ZERO) { // Point emitDirection = glm::quat(glm::vec3(PI_OVER_TWO - elevation, 0.0f, azimuth)) * Vectors::UNIT_Z; } else { glm::vec3 emitPosition; switch (shapeType) { case SHAPE_TYPE_BOX: { glm::vec3 dim = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; int side = randIntInRange(0, 5); int axis = side % 3; float direction = side > 2 ? 1.0f : -1.0f; emitDirection[axis] = direction; emitPosition[axis] = direction * dim[axis]; axis = (axis + 1) % 3; emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); axis = (axis + 1) % 3; emitPosition[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); break; } case SHAPE_TYPE_CYLINDER_X: case SHAPE_TYPE_CYLINDER_Y: case SHAPE_TYPE_CYLINDER_Z: { glm::vec3 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * emitDimensions; int axis = shapeType - SHAPE_TYPE_CYLINDER_X; emitPosition[axis] = emitDimensions[axis] * randFloatInRange(-0.5f, 0.5f); emitDirection[axis] = 0.0f; axis = (axis + 1) % 3; emitPosition[axis] = radii[axis] * glm::cos(azimuth); emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; axis = (axis + 1) % 3; emitPosition[axis] = radii[axis] * glm::sin(azimuth); emitDirection[axis] = radii[axis] > 0.0f ? emitPosition[axis] / (radii[axis] * radii[axis]) : 0.0f; emitDirection = glm::normalize(emitDirection); break; } case SHAPE_TYPE_CIRCLE: { glm::vec2 radii = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); float x = radii.x * glm::cos(azimuth); float z = radii.y * glm::sin(azimuth); emitPosition = glm::vec3(x, 0.0f, z); emitDirection = Vectors::UP; break; } case SHAPE_TYPE_PLANE: { glm::vec2 dim = importanceSample2DDimension(emitRadiusStart) * 0.5f * glm::vec2(emitDimensions.x, emitDimensions.z); int side = randIntInRange(0, 3); int axis = side % 2; float direction = side > 1 ? 1.0f : -1.0f; glm::vec2 pos; pos[axis] = direction * dim[axis]; axis = (axis + 1) % 2; pos[axis] = dim[axis] * randFloatInRange(-1.0f, 1.0f); emitPosition = glm::vec3(pos.x, 0.0f, pos.y); emitDirection = Vectors::UP; break; } case SHAPE_TYPE_COMPOUND: { // if we get here we know that geometryResource is loaded size_t index = randFloat() * triangleInfo.totalSamples; Triangle triangle; for (size_t i = 0; i < triangleInfo.samplesPerTriangle.size(); i++) { size_t numSamples = triangleInfo.samplesPerTriangle[i]; if (index < numSamples) { triangle = triangleInfo.triangles[i]; break; } index -= numSamples; } float edgeLength1 = glm::length(triangle.v1 - triangle.v0); float edgeLength2 = glm::length(triangle.v2 - triangle.v1); float edgeLength3 = glm::length(triangle.v0 - triangle.v2); float perimeter = edgeLength1 + edgeLength2 + edgeLength3; float fraction1 = randFloatInRange(0.0f, 1.0f); float fractionEdge1 = glm::min(fraction1 * perimeter / edgeLength1, 1.0f); float fraction2 = fraction1 - edgeLength1 / perimeter; float fractionEdge2 = glm::clamp(fraction2 * perimeter / edgeLength2, 0.0f, 1.0f); float fraction3 = fraction2 - edgeLength2 / perimeter; float fractionEdge3 = glm::clamp(fraction3 * perimeter / edgeLength3, 0.0f, 1.0f); float dim = importanceSample2DDimension(emitRadiusStart); triangle = triangle * (glm::scale(emitDimensions) * triangleInfo.transform); glm::vec3 center = (triangle.v0 + triangle.v1 + triangle.v2) / 3.0f; glm::vec3 v0 = (dim * (triangle.v0 - center)) + center; glm::vec3 v1 = (dim * (triangle.v1 - center)) + center; glm::vec3 v2 = (dim * (triangle.v2 - center)) + center; emitPosition = glm::mix(v0, glm::mix(v1, glm::mix(v2, v0, fractionEdge3), fractionEdge2), fractionEdge1); emitDirection = triangle.getNormal(); break; } case SHAPE_TYPE_SPHERE: case SHAPE_TYPE_ELLIPSOID: default: { glm::vec3 radii = importanceSample3DDimension(emitRadiusStart) * 0.5f * emitDimensions; float x = radii.x * glm::cos(elevation) * glm::cos(azimuth); float y = radii.y * glm::cos(elevation) * glm::sin(azimuth); float z = radii.z * glm::sin(elevation); emitPosition = glm::vec3(x, y, z); emitDirection = glm::normalize(glm::vec3(radii.x > 0.0f ? x / (radii.x * radii.x) : 0.0f, radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f, radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f)); break; } } particle.relativePosition += emitOrientation * emitPosition; } } particle.velocity = (emitSpeed + randFloatInRange(-1.0f, 1.0f) * speedSpread) * (emitOrientation * emitDirection); particle.acceleration = emitAcceleration + glm::vec3(randFloatInRange(-1.0f, 1.0f), randFloatInRange(-1.0f, 1.0f), randFloatInRange(-1.0f, 1.0f)) * accelerationSpread; return particle; } void ParticleEffectEntityRenderer::stepSimulation() { if (_lastSimulated == 0) { _lastSimulated = usecTimestampNow(); return; } const auto now = usecTimestampNow(); const auto interval = std::min(USECS_PER_SECOND / 60, now - _lastSimulated); _lastSimulated = now; particle::Properties particleProperties; ShapeType shapeType; ModelResource::Pointer geometryResource; withReadLock([&] { particleProperties = _particleProperties; shapeType = _shapeType; geometryResource = _geometryResource; }); const auto& modelTransform = getModelTransform(); if (_emitting && particleProperties.emitting() && (shapeType != SHAPE_TYPE_COMPOUND || (geometryResource && geometryResource->isLoaded()))) { uint64_t emitInterval = particleProperties.emitIntervalUsecs(); if (emitInterval > 0 && interval >= _timeUntilNextEmit) { auto timeRemaining = interval; while (timeRemaining > _timeUntilNextEmit) { if (_shapeType == SHAPE_TYPE_COMPOUND && !_hasComputedTriangles) { computeTriangles(geometryResource->getHFMModel()); } // emit particle _cpuParticles.push_back(createParticle(now, modelTransform, particleProperties, shapeType, geometryResource, _triangleInfo)); _timeUntilNextEmit = emitInterval; if (emitInterval < timeRemaining) { timeRemaining -= emitInterval; } } } else { _timeUntilNextEmit -= interval; } } // Kill any particles that have expired or are over the max size while (_cpuParticles.size() > particleProperties.maxParticles || (!_cpuParticles.empty() && _cpuParticles.front().expiration <= now)) { _cpuParticles.pop_front(); } const float deltaTime = (float)interval / (float)USECS_PER_SECOND; // update the particles for (auto& particle : _cpuParticles) { if (_prevEmitterShouldTrail != particleProperties.emission.shouldTrail) { if (_prevEmitterShouldTrail) { particle.relativePosition = particle.relativePosition + particle.basePosition - modelTransform.getTranslation(); } particle.basePosition = modelTransform.getTranslation(); } particle.integrate(deltaTime); } _prevEmitterShouldTrail = particleProperties.emission.shouldTrail; // Build particle primitives static GpuParticles gpuParticles; gpuParticles.clear(); gpuParticles.reserve(_cpuParticles.size()); // Reserve space std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform] (const CpuParticle& particle) { glm::vec3 position = particle.relativePosition + (particleProperties.emission.shouldTrail ? particle.basePosition : modelTransform.getTranslation()); return GpuParticle(position, glm::vec2(particle.lifetime, particle.seed)); }); // Update particle buffer auto& particleBuffer = _particleBuffer; size_t numBytes = sizeof(GpuParticle) * gpuParticles.size(); particleBuffer->resize(numBytes); if (numBytes != 0) { particleBuffer->setData(numBytes, (const gpu::Byte*)gpuParticles.data()); } } void ParticleEffectEntityRenderer::doRender(RenderArgs* args) { if (!_visible || !(_networkTexture && _networkTexture->isLoaded())) { return; } // FIXME migrate simulation to a compute stage stepSimulation(); gpu::Batch& batch = *args->_batch; batch.setResourceTexture(0, _networkTexture->getGPUTexture()); Transform transform; // The particles are in world space, so the transform is unused, except for the rotation, which we use // if the particles are marked rotateWithEntity withReadLock([&] { transform.setRotation(_renderTransform.getRotation()); auto& color = _uniformBuffer.edit().color; color.start = EntityRenderer::calculatePulseColor(_particleProperties.getColorStart(), _pulseProperties, _created); color.middle = EntityRenderer::calculatePulseColor(_particleProperties.getColorMiddle(), _pulseProperties, _created); color.finish = EntityRenderer::calculatePulseColor(_particleProperties.getColorFinish(), _pulseProperties, _created); color.spread = EntityRenderer::calculatePulseColor(_particleProperties.getColorSpread(), _pulseProperties, _created); }); batch.setModelTransform(transform); batch.setUniformBuffer(0, _uniformBuffer); batch.setInputFormat(_vertexFormat); batch.setInputBuffer(0, _particleBuffer, 0, sizeof(GpuParticle)); auto numParticles = _particleBuffer->getSize() / sizeof(GpuParticle); static const size_t VERTEX_PER_PARTICLE = 4; batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE); } void ParticleEffectEntityRenderer::fetchGeometryResource() { QUrl hullURL(_compoundShapeURL); if (hullURL.isEmpty()) { _geometryResource.reset(); } else { _geometryResource = DependencyManager::get()->getCollisionModelResource(hullURL); } } // FIXME: this is very similar to Model::calculateTriangleSets void ParticleEffectEntityRenderer::computeTriangles(const hfm::Model& hfmModel) { PROFILE_RANGE(render, __FUNCTION__); uint32_t numberOfMeshes = (uint32_t)hfmModel.meshes.size(); _hasComputedTriangles = true; _triangleInfo.triangles.clear(); _triangleInfo.samplesPerTriangle.clear(); std::vector areas; float minArea = FLT_MAX; AABox bounds; for (uint32_t i = 0; i < numberOfMeshes; i++) { const HFMMesh& mesh = hfmModel.meshes.at(i); const uint32_t numberOfParts = (uint32_t)mesh.parts.size(); for (uint32_t j = 0; j < numberOfParts; j++) { const HFMMeshPart& part = mesh.parts.at(j); const int INDICES_PER_TRIANGLE = 3; const int INDICES_PER_QUAD = 4; const int TRIANGLES_PER_QUAD = 2; // tell our triangleSet how many triangles to expect. int numberOfQuads = part.quadIndices.size() / INDICES_PER_QUAD; int numberOfTris = part.triangleIndices.size() / INDICES_PER_TRIANGLE; int totalTriangles = (numberOfQuads * TRIANGLES_PER_QUAD) + numberOfTris; _triangleInfo.triangles.reserve(_triangleInfo.triangles.size() + totalTriangles); areas.reserve(areas.size() + totalTriangles); auto meshTransform = hfmModel.offset * mesh.modelTransform; if (part.quadIndices.size() > 0) { int vIndex = 0; for (int q = 0; q < numberOfQuads; q++) { int i0 = part.quadIndices[vIndex++]; int i1 = part.quadIndices[vIndex++]; int i2 = part.quadIndices[vIndex++]; int i3 = part.quadIndices[vIndex++]; // track the model space version... these points will be transformed by the FST's offset, // which includes the scaling, rotation, and translation specified by the FST/FBX, // this can't change at runtime, so we can safely store these in our TriangleSet glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); glm::vec3 v3 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i3], 1.0f)); Triangle tri1 = { v0, v1, v3 }; Triangle tri2 = { v1, v2, v3 }; _triangleInfo.triangles.push_back(tri1); _triangleInfo.triangles.push_back(tri2); float area1 = tri1.getArea(); areas.push_back(area1); if (area1 > EPSILON) { minArea = std::min(minArea, area1); } float area2 = tri2.getArea(); areas.push_back(area2); if (area2 > EPSILON) { minArea = std::min(minArea, area2); } bounds += v0; bounds += v1; bounds += v2; bounds += v3; } } if (part.triangleIndices.size() > 0) { int vIndex = 0; for (int t = 0; t < numberOfTris; t++) { int i0 = part.triangleIndices[vIndex++]; int i1 = part.triangleIndices[vIndex++]; int i2 = part.triangleIndices[vIndex++]; // track the model space version... these points will be transformed by the FST's offset, // which includes the scaling, rotation, and translation specified by the FST/FBX, // this can't change at runtime, so we can safely store these in our TriangleSet glm::vec3 v0 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i0], 1.0f)); glm::vec3 v1 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i1], 1.0f)); glm::vec3 v2 = glm::vec3(meshTransform * glm::vec4(mesh.vertices[i2], 1.0f)); Triangle tri = { v0, v1, v2 }; _triangleInfo.triangles.push_back(tri); float area = tri.getArea(); areas.push_back(area); if (area > EPSILON) { minArea = std::min(minArea, area); } bounds += v0; bounds += v1; bounds += v2; } } } } _triangleInfo.totalSamples = 0; for (auto& area : areas) { size_t numSamples = area / minArea; _triangleInfo.samplesPerTriangle.push_back(numSamples); _triangleInfo.totalSamples += numSamples; } glm::vec3 scale = bounds.getScale(); _triangleInfo.transform = glm::scale(1.0f / scale) * glm::translate(-bounds.calcCenter()); }