// Copyright (c) 2008-2022 the Urho3D project // License: MIT #include "../Precompiled.h" #include "../Core/Context.h" #include "../Core/Profiler.h" #include "../Graphics/AnimatedModel.h" #include "../Graphics/Batch.h" #include "../Graphics/Camera.h" #include "../Graphics/DecalSet.h" #include "../Graphics/Geometry.h" #include "../Graphics/Graphics.h" #include "../Graphics/Material.h" #include "../Graphics/Tangent.h" #include "../GraphicsAPI/IndexBuffer.h" #include "../GraphicsAPI/VertexBuffer.h" #include "../IO/Log.h" #include "../IO/MemoryBuffer.h" #include "../Resource/ResourceCache.h" #include "../Scene/Scene.h" #include "../Scene/SceneEvents.h" #include "../DebugNew.h" #ifdef _MSC_VER #pragma warning(disable:6293) #endif namespace Urho3D { extern const char* GEOMETRY_CATEGORY; static const unsigned MIN_VERTICES = 4; static const unsigned MIN_INDICES = 6; static const unsigned MAX_VERTICES = 65536; static const unsigned DEFAULT_MAX_VERTICES = 512; static const unsigned DEFAULT_MAX_INDICES = 1024; static const VertexMaskFlags STATIC_ELEMENT_MASK = MASK_POSITION | MASK_NORMAL | MASK_TEXCOORD1 | MASK_TANGENT; static const VertexMaskFlags SKINNED_ELEMENT_MASK = MASK_POSITION | MASK_NORMAL | MASK_TEXCOORD1 | MASK_TANGENT | MASK_BLENDWEIGHTS | MASK_BLENDINDICES; static DecalVertex ClipEdge(const DecalVertex& v0, const DecalVertex& v1, float d0, float d1, bool skinned) { DecalVertex ret; float t = d0 / (d0 - d1); ret.position_ = v0.position_ + t * (v1.position_ - v0.position_); ret.normal_ = v0.normal_ + t * (v1.normal_ - v0.normal_); if (skinned) { if (*((unsigned*)v0.blendIndices_) != *((unsigned*)v1.blendIndices_)) { // Blend weights and indices: if indices are different, choose the vertex nearer to the split plane const DecalVertex& src = Abs(d0) < Abs(d1) ? v0 : v1; for (unsigned i = 0; i < 4; ++i) { ret.blendWeights_[i] = src.blendWeights_[i]; ret.blendIndices_[i] = src.blendIndices_[i]; } } else { // If indices are same, can interpolate the weights for (unsigned i = 0; i < 4; ++i) { ret.blendWeights_[i] = v0.blendWeights_[i] + t * (v1.blendWeights_[i] - v0.blendWeights_[i]); ret.blendIndices_[i] = v0.blendIndices_[i]; } } } return ret; } static void ClipPolygon(PODVector& dest, const PODVector& src, const Plane& plane, bool skinned) { unsigned last = 0; float lastDistance = 0.0f; dest.Clear(); if (src.Empty()) return; for (unsigned i = 0; i < src.Size(); ++i) { float distance = plane.Distance(src[i].position_); if (distance >= 0.0f) { if (lastDistance < 0.0f) dest.Push(ClipEdge(src[last], src[i], lastDistance, distance, skinned)); dest.Push(src[i]); } else { if (lastDistance >= 0.0f && i != 0) dest.Push(ClipEdge(src[last], src[i], lastDistance, distance, skinned)); } last = i; lastDistance = distance; } // Recheck the distances of the last and first vertices and add the final clipped vertex if applicable float distance = plane.Distance(src[0].position_); if ((lastDistance < 0.0f && distance >= 0.0f) || (lastDistance >= 0.0f && distance < 0.0f)) dest.Push(ClipEdge(src[last], src[0], lastDistance, distance, skinned)); } void Decal::AddVertex(const DecalVertex& vertex) { for (unsigned i = 0; i < vertices_.Size(); ++i) { if (vertex.position_.Equals(vertices_[i].position_) && vertex.normal_.Equals(vertices_[i].normal_)) { indices_.Push((unsigned short)i); return; } } auto newIndex = (unsigned short)vertices_.Size(); vertices_.Push(vertex); indices_.Push(newIndex); } void Decal::CalculateBoundingBox() { boundingBox_.Clear(); for (unsigned i = 0; i < vertices_.Size(); ++i) boundingBox_.Merge(vertices_[i].position_); } DecalSet::DecalSet(Context* context) : Drawable(context, DRAWABLE_GEOMETRY), geometry_(new Geometry(context)), vertexBuffer_(new VertexBuffer(context_)), indexBuffer_(new IndexBuffer(context_)), numVertices_(0), numIndices_(0), maxVertices_(DEFAULT_MAX_VERTICES), maxIndices_(DEFAULT_MAX_INDICES), optimizeBufferSize_(false), skinned_(false), bufferDirty_(true), boundingBoxDirty_(true), skinningDirty_(false), assignBonesPending_(false), subscribed_(false) { geometry_->SetIndexBuffer(indexBuffer_); batches_.Resize(1); batches_[0].geometry_ = geometry_; batches_[0].geometryType_ = GEOM_STATIC_NOINSTANCING; } DecalSet::~DecalSet() = default; void DecalSet::RegisterObject(Context* context) { context->RegisterFactory(GEOMETRY_CATEGORY); URHO3D_ACCESSOR_ATTRIBUTE("Is Enabled", IsEnabled, SetEnabled, bool, true, AM_DEFAULT); URHO3D_MIXED_ACCESSOR_ATTRIBUTE("Material", GetMaterialAttr, SetMaterialAttr, ResourceRef, ResourceRef(Material::GetTypeStatic()), AM_DEFAULT); URHO3D_ACCESSOR_ATTRIBUTE("Max Vertices", GetMaxVertices, SetMaxVertices, unsigned, DEFAULT_MAX_VERTICES, AM_DEFAULT); URHO3D_ACCESSOR_ATTRIBUTE("Max Indices", GetMaxIndices, SetMaxIndices, unsigned, DEFAULT_MAX_INDICES, AM_DEFAULT); URHO3D_ACCESSOR_ATTRIBUTE("Optimize Buffer Size", GetOptimizeBufferSize, SetOptimizeBufferSize, bool, false, AM_DEFAULT); URHO3D_ACCESSOR_ATTRIBUTE("Can Be Occluded", IsOccludee, SetOccludee, bool, true, AM_DEFAULT); URHO3D_ACCESSOR_ATTRIBUTE("Draw Distance", GetDrawDistance, SetDrawDistance, float, 0.0f, AM_DEFAULT); URHO3D_COPY_BASE_ATTRIBUTES(Drawable); URHO3D_MIXED_ACCESSOR_ATTRIBUTE("Decals", GetDecalsAttr, SetDecalsAttr, PODVector, Variant::emptyBuffer, AM_FILE | AM_NOEDIT); } void DecalSet::ApplyAttributes() { if (assignBonesPending_) AssignBoneNodes(); } void DecalSet::OnSetEnabled() { Drawable::OnSetEnabled(); UpdateEventSubscription(true); } void DecalSet::ProcessRayQuery(const RayOctreeQuery& query, PODVector& results) { // Do not return raycast hits } void DecalSet::UpdateBatches(const FrameInfo& frame) { const BoundingBox& worldBoundingBox = GetWorldBoundingBox(); const Matrix3x4& worldTransform = node_->GetWorldTransform(); distance_ = frame.camera_->GetDistance(worldBoundingBox.Center()); float scale = worldBoundingBox.Size().DotProduct(DOT_SCALE); lodDistance_ = frame.camera_->GetLodDistance(distance_, scale, lodBias_); batches_[0].distance_ = distance_; if (!skinned_) batches_[0].worldTransform_ = &worldTransform; } void DecalSet::UpdateGeometry(const FrameInfo& frame) { if (bufferDirty_ || vertexBuffer_->IsDataLost() || indexBuffer_->IsDataLost()) UpdateBuffers(); if (skinningDirty_) UpdateSkinning(); } UpdateGeometryType DecalSet::GetUpdateGeometryType() { if (bufferDirty_ || vertexBuffer_->IsDataLost() || indexBuffer_->IsDataLost()) return UPDATE_MAIN_THREAD; else if (skinningDirty_) return UPDATE_WORKER_THREAD; else return UPDATE_NONE; } void DecalSet::SetMaterial(Material* material) { batches_[0].material_ = material; MarkNetworkUpdate(); } void DecalSet::SetMaxVertices(unsigned num) { // Never expand to 32 bit indices num = (unsigned)Clamp(num, MIN_VERTICES, MAX_VERTICES); if (num != maxVertices_) { if (!optimizeBufferSize_) bufferDirty_ = true; maxVertices_ = num; while (decals_.Size() && numVertices_ > maxVertices_) RemoveDecals(1); MarkNetworkUpdate(); } } void DecalSet::SetMaxIndices(unsigned num) { if (num < MIN_INDICES) num = MIN_INDICES; if (num != maxIndices_) { if (!optimizeBufferSize_) bufferDirty_ = true; maxIndices_ = num; while (decals_.Size() && numIndices_ > maxIndices_) RemoveDecals(1); MarkNetworkUpdate(); } } void DecalSet::SetOptimizeBufferSize(bool enable) { if (enable != optimizeBufferSize_) { optimizeBufferSize_ = enable; bufferDirty_ = true; MarkNetworkUpdate(); } } bool DecalSet::AddDecal(Drawable* target, const Vector3& worldPosition, const Quaternion& worldRotation, float size, float aspectRatio, float depth, const Vector2& topLeftUV, const Vector2& bottomRightUV, float timeToLive, float normalCutoff, unsigned subGeometry) { URHO3D_PROFILE(AddDecal); // Do not add decals in headless mode if (!node_ || !GetSubsystem()) return false; if (!target || !target->GetNode()) { URHO3D_LOGERROR("Null target drawable for decal"); return false; } // Check for animated target and switch into skinned/static mode if necessary auto* animatedModel = dynamic_cast(target); if ((animatedModel && !skinned_) || (!animatedModel && skinned_)) { RemoveAllDecals(); skinned_ = animatedModel != nullptr; bufferDirty_ = true; } // Center the decal frustum on the world position Vector3 adjustedWorldPosition = worldPosition - 0.5f * depth * (worldRotation * Vector3::FORWARD); /// \todo target transform is not right if adding a decal to StaticModelGroup Matrix3x4 targetTransform = target->GetNode()->GetWorldTransform().Inverse(); // For an animated model, adjust the decal position back to the bind pose // To do this, need to find the bone the decal is colliding with if (animatedModel) { Skeleton& skeleton = animatedModel->GetSkeleton(); unsigned numBones = skeleton.GetNumBones(); Bone* bestBone = nullptr; float bestSize = 0.0f; for (unsigned i = 0; i < numBones; ++i) { Bone* bone = skeleton.GetBone(i); if (!bone->node_ || !bone->collisionMask_) continue; // Represent the decal as a sphere, try to find the biggest colliding bone Sphere decalSphere (bone->node_->GetWorldTransform().Inverse() * worldPosition, 0.5f * size / bone->node_->GetWorldScale().Length()); if (bone->collisionMask_ & BONECOLLISION_BOX) { float size = bone->boundingBox_.HalfSize().Length(); if (bone->boundingBox_.IsInside(decalSphere) && size > bestSize) { bestBone = bone; bestSize = size; } } else if (bone->collisionMask_ & BONECOLLISION_SPHERE) { Sphere boneSphere(Vector3::ZERO, bone->radius_); float size = bone->radius_; if (boneSphere.IsInside(decalSphere) && size > bestSize) { bestBone = bone; bestSize = size; } } } if (bestBone) targetTransform = (bestBone->node_->GetWorldTransform() * bestBone->offsetMatrix_).Inverse(); } // Build the decal frustum Frustum decalFrustum; Matrix3x4 frustumTransform = targetTransform * Matrix3x4(adjustedWorldPosition, worldRotation, 1.0f); decalFrustum.DefineOrtho(size, aspectRatio, 1.0, 0.0f, depth, frustumTransform); Vector3 decalNormal = (targetTransform * Vector4(worldRotation * Vector3::BACK, 0.0f)).Normalized(); decals_.Resize(decals_.Size() + 1); Decal& newDecal = decals_.Back(); newDecal.timeToLive_ = timeToLive; Vector> faces; PODVector tempFace; unsigned numBatches = target->GetBatches().Size(); // Use either a specified subgeometry in the target, or all if (subGeometry < numBatches) GetFaces(faces, target, subGeometry, decalFrustum, decalNormal, normalCutoff); else { for (unsigned i = 0; i < numBatches; ++i) GetFaces(faces, target, i, decalFrustum, decalNormal, normalCutoff); } // Clip the acquired faces against all frustum planes for (const auto& plane : decalFrustum.planes_) { for (unsigned j = 0; j < faces.Size(); ++j) { PODVector& face = faces[j]; if (face.Empty()) continue; ClipPolygon(tempFace, face, plane, skinned_); face = tempFace; } } // Now triangulate the resulting faces into decal vertices for (unsigned i = 0; i < faces.Size(); ++i) { PODVector& face = faces[i]; if (face.Size() < 3) continue; for (unsigned j = 2; j < face.Size(); ++j) { newDecal.AddVertex(face[0]); newDecal.AddVertex(face[j - 1]); newDecal.AddVertex(face[j]); } } // Check if resulted in no triangles if (newDecal.vertices_.Empty()) { decals_.Pop(); return true; } if (newDecal.vertices_.Size() > maxVertices_) { URHO3D_LOGWARNING("Can not add decal, vertex count " + String(newDecal.vertices_.Size()) + " exceeds maximum " + String(maxVertices_)); decals_.Pop(); return false; } if (newDecal.indices_.Size() > maxIndices_) { URHO3D_LOGWARNING("Can not add decal, index count " + String(newDecal.indices_.Size()) + " exceeds maximum " + String(maxIndices_)); decals_.Pop(); return false; } // Calculate UVs Matrix4 projection(Matrix4::ZERO); projection.m11_ = (1.0f / (size * 0.5f)); projection.m00_ = projection.m11_ / aspectRatio; projection.m22_ = 1.0f / depth; projection.m33_ = 1.0f; CalculateUVs(newDecal, frustumTransform.Inverse(), projection, topLeftUV, bottomRightUV); // Transform vertices to this node's local space and generate tangents Matrix3x4 decalTransform = node_->GetWorldTransform().Inverse() * target->GetNode()->GetWorldTransform(); TransformVertices(newDecal, skinned_ ? Matrix3x4::IDENTITY : decalTransform); GenerateTangents(&newDecal.vertices_[0], sizeof(DecalVertex), &newDecal.indices_[0], sizeof(unsigned short), 0, newDecal.indices_.Size(), offsetof(DecalVertex, normal_), offsetof(DecalVertex, texCoord_), offsetof(DecalVertex, tangent_)); newDecal.CalculateBoundingBox(); numVertices_ += newDecal.vertices_.Size(); numIndices_ += newDecal.indices_.Size(); // Remove oldest decals if total vertices exceeded while (decals_.Size() && (numVertices_ > maxVertices_ || numIndices_ > maxIndices_)) RemoveDecals(1); URHO3D_LOGDEBUG("Added decal with " + String(newDecal.vertices_.Size()) + " vertices"); // If new decal is time limited, subscribe to scene post-update if (newDecal.timeToLive_ > 0.0f && !subscribed_) UpdateEventSubscription(false); MarkDecalsDirty(); return true; } void DecalSet::RemoveDecals(unsigned num) { while (num-- && decals_.Size()) RemoveDecal(decals_.Begin()); } void DecalSet::RemoveAllDecals() { if (!decals_.Empty()) { decals_.Clear(); numVertices_ = 0; numIndices_ = 0; MarkDecalsDirty(); } // Remove all bones and skinning matrices and stop listening to the bone nodes for (Vector::Iterator i = bones_.Begin(); i != bones_.End(); ++i) { if (i->node_) i->node_->RemoveListener(this); } bones_.Clear(); skinMatrices_.Clear(); UpdateBatch(); } Material* DecalSet::GetMaterial() const { return batches_[0].material_; } void DecalSet::SetMaterialAttr(const ResourceRef& value) { auto* cache = GetSubsystem(); SetMaterial(cache->GetResource(value.name_)); } void DecalSet::SetDecalsAttr(const PODVector& value) { RemoveAllDecals(); if (value.Empty()) return; MemoryBuffer buffer(value); skinned_ = buffer.ReadBool(); unsigned numDecals = buffer.ReadVLE(); while (numDecals--) { decals_.Resize(decals_.Size() + 1); Decal& newDecal = decals_.Back(); newDecal.timer_ = buffer.ReadFloat(); newDecal.timeToLive_ = buffer.ReadFloat(); newDecal.vertices_.Resize(buffer.ReadVLE()); newDecal.indices_.Resize(buffer.ReadVLE()); for (PODVector::Iterator i = newDecal.vertices_.Begin(); i != newDecal.vertices_.End(); ++i) { i->position_ = buffer.ReadVector3(); i->normal_ = buffer.ReadVector3(); i->texCoord_ = buffer.ReadVector2(); i->tangent_ = buffer.ReadVector4(); if (skinned_) { for (float& blendWeight : i->blendWeights_) blendWeight = buffer.ReadFloat(); for (unsigned char& blendIndex : i->blendIndices_) blendIndex = buffer.ReadUByte(); } } for (PODVector::Iterator i = newDecal.indices_.Begin(); i != newDecal.indices_.End(); ++i) *i = buffer.ReadUShort(); newDecal.CalculateBoundingBox(); numVertices_ += newDecal.vertices_.Size(); numIndices_ += newDecal.indices_.Size(); } if (skinned_) { unsigned numBones = buffer.ReadVLE(); skinMatrices_.Resize(numBones); bones_.Resize(numBones); for (unsigned i = 0; i < numBones; ++i) { Bone& newBone = bones_[i]; newBone.name_ = buffer.ReadString(); newBone.collisionMask_ = BoneCollisionShapeFlags(buffer.ReadUByte()); if (newBone.collisionMask_ & BONECOLLISION_SPHERE) newBone.radius_ = buffer.ReadFloat(); if (newBone.collisionMask_ & BONECOLLISION_BOX) newBone.boundingBox_ = buffer.ReadBoundingBox(); buffer.Read(&newBone.offsetMatrix_.m00_, sizeof(Matrix3x4)); } assignBonesPending_ = true; skinningDirty_ = true; } UpdateEventSubscription(true); UpdateBatch(); MarkDecalsDirty(); } ResourceRef DecalSet::GetMaterialAttr() const { return GetResourceRef(batches_[0].material_, Material::GetTypeStatic()); } PODVector DecalSet::GetDecalsAttr() const { VectorBuffer ret; ret.WriteBool(skinned_); ret.WriteVLE(decals_.Size()); for (List::ConstIterator i = decals_.Begin(); i != decals_.End(); ++i) { ret.WriteFloat(i->timer_); ret.WriteFloat(i->timeToLive_); ret.WriteVLE(i->vertices_.Size()); ret.WriteVLE(i->indices_.Size()); for (PODVector::ConstIterator j = i->vertices_.Begin(); j != i->vertices_.End(); ++j) { ret.WriteVector3(j->position_); ret.WriteVector3(j->normal_); ret.WriteVector2(j->texCoord_); ret.WriteVector4(j->tangent_); if (skinned_) { for (float blendWeight : j->blendWeights_) ret.WriteFloat(blendWeight); for (unsigned char blendIndex : j->blendIndices_) ret.WriteUByte(blendIndex); } } for (PODVector::ConstIterator j = i->indices_.Begin(); j != i->indices_.End(); ++j) ret.WriteUShort(*j); } if (skinned_) { ret.WriteVLE(bones_.Size()); for (Vector::ConstIterator i = bones_.Begin(); i != bones_.End(); ++i) { ret.WriteString(i->name_); ret.WriteUByte(i->collisionMask_); if (i->collisionMask_ & BONECOLLISION_SPHERE) ret.WriteFloat(i->radius_); if (i->collisionMask_ & BONECOLLISION_BOX) ret.WriteBoundingBox(i->boundingBox_); ret.Write(i->offsetMatrix_.Data(), sizeof(Matrix3x4)); } } return ret.GetBuffer(); } void DecalSet::OnMarkedDirty(Node* node) { Drawable::OnMarkedDirty(node); if (skinned_) { // If the scene node or any of the bone nodes move, mark skinning dirty skinningDirty_ = true; } } void DecalSet::OnWorldBoundingBoxUpdate() { if (!skinned_) { if (boundingBoxDirty_) CalculateBoundingBox(); worldBoundingBox_ = boundingBox_.Transformed(node_->GetWorldTransform()); } else { // When using skinning, update world bounding box based on the bones BoundingBox worldBox; for (Vector::ConstIterator i = bones_.Begin(); i != bones_.End(); ++i) { Node* boneNode = i->node_; if (!boneNode) continue; // Use hitbox if available. If not, use only half of the sphere radius /// \todo The sphere radius should be multiplied with bone scale if (i->collisionMask_ & BONECOLLISION_BOX) worldBox.Merge(i->boundingBox_.Transformed(boneNode->GetWorldTransform())); else if (i->collisionMask_ & BONECOLLISION_SPHERE) worldBox.Merge(Sphere(boneNode->GetWorldPosition(), i->radius_ * 0.5f)); } worldBoundingBox_ = worldBox; } } void DecalSet::GetFaces(Vector>& faces, Drawable* target, unsigned batchIndex, const Frustum& frustum, const Vector3& decalNormal, float normalCutoff) { // Try to use the most accurate LOD level if possible Geometry* geometry = target->GetLodGeometry(batchIndex, 0); if (!geometry || geometry->GetPrimitiveType() != TRIANGLE_LIST) return; const unsigned char* positionData = nullptr; const unsigned char* normalData = nullptr; const unsigned char* skinningData = nullptr; const unsigned char* indexData = nullptr; unsigned positionStride = 0; unsigned normalStride = 0; unsigned skinningStride = 0; unsigned indexStride = 0; IndexBuffer* ib = geometry->GetIndexBuffer(); if (ib) { indexData = ib->GetShadowData(); indexStride = ib->GetIndexSize(); } // For morphed models positions, normals and skinning may be in different buffers for (unsigned i = 0; i < geometry->GetNumVertexBuffers(); ++i) { VertexBuffer* vb = geometry->GetVertexBuffer(i); if (!vb) continue; unsigned elementMask = vb->GetElementMask(); unsigned char* data = vb->GetShadowData(); if (!data) continue; if (elementMask & MASK_POSITION) { positionData = data; positionStride = vb->GetVertexSize(); } if (elementMask & MASK_NORMAL) { normalData = data + vb->GetElementOffset(SEM_NORMAL); normalStride = vb->GetVertexSize(); } if (elementMask & MASK_BLENDWEIGHTS) { skinningData = data + vb->GetElementOffset(SEM_BLENDWEIGHTS); skinningStride = vb->GetVertexSize(); } } // Positions and indices are needed if (!positionData) { // As a fallback, try to get the geometry's raw vertex/index data const PODVector* elements; geometry->GetRawData(positionData, positionStride, indexData, indexStride, elements); if (!positionData) { URHO3D_LOGWARNING("Can not add decal, target drawable has no CPU-side geometry data"); return; } } if (indexData) { unsigned indexStart = geometry->GetIndexStart(); unsigned indexCount = geometry->GetIndexCount(); // 16-bit indices if (indexStride == sizeof(unsigned short)) { const unsigned short* indices = ((const unsigned short*)indexData) + indexStart; const unsigned short* indicesEnd = indices + indexCount; while (indices < indicesEnd) { GetFace(faces, target, batchIndex, indices[0], indices[1], indices[2], positionData, normalData, skinningData, positionStride, normalStride, skinningStride, frustum, decalNormal, normalCutoff); indices += 3; } } else // 32-bit indices { const unsigned* indices = ((const unsigned*)indexData) + indexStart; const unsigned* indicesEnd = indices + indexCount; while (indices < indicesEnd) { GetFace(faces, target, batchIndex, indices[0], indices[1], indices[2], positionData, normalData, skinningData, positionStride, normalStride, skinningStride, frustum, decalNormal, normalCutoff); indices += 3; } } } else { // Non-indexed geometry unsigned indices = geometry->GetVertexStart(); unsigned indicesEnd = indices + geometry->GetVertexCount(); while (indices + 2 < indicesEnd) { GetFace(faces, target, batchIndex, indices, indices + 1, indices + 2, positionData, normalData, skinningData, positionStride, normalStride, skinningStride, frustum, decalNormal, normalCutoff); indices += 3; } } } void DecalSet::GetFace(Vector>& faces, Drawable* target, unsigned batchIndex, unsigned i0, unsigned i1, unsigned i2, const unsigned char* positionData, const unsigned char* normalData, const unsigned char* skinningData, unsigned positionStride, unsigned normalStride, unsigned skinningStride, const Frustum& frustum, const Vector3& decalNormal, float normalCutoff) { bool hasNormals = normalData != nullptr; bool hasSkinning = skinned_ && skinningData != nullptr; const Vector3& v0 = *((const Vector3*)(&positionData[i0 * positionStride])); const Vector3& v1 = *((const Vector3*)(&positionData[i1 * positionStride])); const Vector3& v2 = *((const Vector3*)(&positionData[i2 * positionStride])); // Calculate unsmoothed face normals if no normal data Vector3 faceNormal = Vector3::ZERO; if (!hasNormals) { Vector3 dist1 = v1 - v0; Vector3 dist2 = v2 - v0; faceNormal = (dist1.CrossProduct(dist2)).Normalized(); } const Vector3& n0 = hasNormals ? *((const Vector3*)(&normalData[i0 * normalStride])) : faceNormal; const Vector3& n1 = hasNormals ? *((const Vector3*)(&normalData[i1 * normalStride])) : faceNormal; const Vector3& n2 = hasNormals ? *((const Vector3*)(&normalData[i2 * normalStride])) : faceNormal; const unsigned char* s0 = hasSkinning ? &skinningData[i0 * skinningStride] : nullptr; const unsigned char* s1 = hasSkinning ? &skinningData[i1 * skinningStride] : nullptr; const unsigned char* s2 = hasSkinning ? &skinningData[i2 * skinningStride] : nullptr; // Check if face is too much away from the decal normal if (decalNormal.DotProduct((n0 + n1 + n2) / 3.0f) < normalCutoff) return; // Check if face is culled completely by any of the planes for (unsigned i = PLANE_FAR; i < NUM_FRUSTUM_PLANES; --i) { const Plane& plane = frustum.planes_[i]; if (plane.Distance(v0) < 0.0f && plane.Distance(v1) < 0.0f && plane.Distance(v2) < 0.0f) return; } faces.Resize(faces.Size() + 1); PODVector& face = faces.Back(); if (!hasSkinning) { face.Reserve(3); face.Push(DecalVertex(v0, n0)); face.Push(DecalVertex(v1, n1)); face.Push(DecalVertex(v2, n2)); } else { const auto* bw0 = (const float*)s0; const auto* bw1 = (const float*)s1; const auto* bw2 = (const float*)s2; const unsigned char* bi0 = s0 + sizeof(float) * 4; const unsigned char* bi1 = s1 + sizeof(float) * 4; const unsigned char* bi2 = s2 + sizeof(float) * 4; unsigned char nbi0[4]; unsigned char nbi1[4]; unsigned char nbi2[4]; // Make sure all bones are found and that there is room in the skinning matrices if (!GetBones(target, batchIndex, bw0, bi0, nbi0) || !GetBones(target, batchIndex, bw1, bi1, nbi1) || !GetBones(target, batchIndex, bw2, bi2, nbi2)) return; face.Reserve(3); face.Push(DecalVertex(v0, n0, bw0, nbi0)); face.Push(DecalVertex(v1, n1, bw1, nbi1)); face.Push(DecalVertex(v2, n2, bw2, nbi2)); } } bool DecalSet::GetBones(Drawable* target, unsigned batchIndex, const float* blendWeights, const unsigned char* blendIndices, unsigned char* newBlendIndices) { auto* animatedModel = dynamic_cast(target); if (!animatedModel) return false; // Check whether target is using global or per-geometry skinning const Vector>& geometrySkinMatrices = animatedModel->GetGeometrySkinMatrices(); const Vector>& geometryBoneMappings = animatedModel->GetGeometryBoneMappings(); for (unsigned i = 0; i < 4; ++i) { if (blendWeights[i] > 0.0f) { Bone* bone = nullptr; if (geometrySkinMatrices.Empty()) bone = animatedModel->GetSkeleton().GetBone(blendIndices[i]); else if (blendIndices[i] < geometryBoneMappings[batchIndex].Size()) bone = animatedModel->GetSkeleton().GetBone(geometryBoneMappings[batchIndex][blendIndices[i]]); if (!bone) { URHO3D_LOGWARNING("Out of range bone index for skinned decal"); return false; } bool found = false; unsigned index; for (index = 0; index < bones_.Size(); ++index) { if (bones_[index].node_ == bone->node_) { // Check also that the offset matrix matches, in case we for example have a separate attachment AnimatedModel // with a different bind pose if (bones_[index].offsetMatrix_.Equals(bone->offsetMatrix_)) { found = true; break; } } } if (!found) { if (bones_.Size() >= Graphics::GetMaxBones()) { URHO3D_LOGWARNING("Maximum skinned decal bone count reached"); return false; } else { // Copy the bone from the model to the decal index = bones_.Size(); bones_.Resize(bones_.Size() + 1); bones_[index] = *bone; skinMatrices_.Resize(skinMatrices_.Size() + 1); skinningDirty_ = true; // Start listening to bone transform changes to update skinning bone->node_->AddListener(this); } } newBlendIndices[i] = (unsigned char)index; } else newBlendIndices[i] = 0; } // Update amount of shader data in the decal batch UpdateBatch(); return true; } void DecalSet::CalculateUVs(Decal& decal, const Matrix3x4& view, const Matrix4& projection, const Vector2& topLeftUV, const Vector2& bottomRightUV) { Matrix4 viewProj = projection * view; for (PODVector::Iterator i = decal.vertices_.Begin(); i != decal.vertices_.End(); ++i) { Vector3 projected = viewProj * i->position_; i->texCoord_ = Vector2( Lerp(topLeftUV.x_, bottomRightUV.x_, projected.x_ * 0.5f + 0.5f), Lerp(bottomRightUV.y_, topLeftUV.y_, projected.y_ * 0.5f + 0.5f) ); } } void DecalSet::TransformVertices(Decal& decal, const Matrix3x4& transform) { for (PODVector::Iterator i = decal.vertices_.Begin(); i != decal.vertices_.End(); ++i) { i->position_ = transform * i->position_; i->normal_ = (transform * Vector4(i->normal_, 0.0f)).Normalized(); } } List::Iterator DecalSet::RemoveDecal(List::Iterator i) { numVertices_ -= i->vertices_.Size(); numIndices_ -= i->indices_.Size(); MarkDecalsDirty(); return decals_.Erase(i); } void DecalSet::MarkDecalsDirty() { if (!boundingBoxDirty_) { boundingBoxDirty_ = true; OnMarkedDirty(node_); } bufferDirty_ = true; } void DecalSet::CalculateBoundingBox() { boundingBox_.Clear(); for (List::ConstIterator i = decals_.Begin(); i != decals_.End(); ++i) boundingBox_.Merge(i->boundingBox_); boundingBoxDirty_ = false; } void DecalSet::UpdateBuffers() { const VertexMaskFlags newElementMask = skinned_ ? SKINNED_ELEMENT_MASK : STATIC_ELEMENT_MASK; unsigned newVBSize = optimizeBufferSize_ ? numVertices_ : maxVertices_; unsigned newIBSize = optimizeBufferSize_ ? numIndices_ : maxIndices_; if (vertexBuffer_->GetElementMask() != newElementMask || vertexBuffer_->GetVertexCount() != newVBSize) vertexBuffer_->SetSize(newVBSize, newElementMask); if (indexBuffer_->GetIndexCount() != newIBSize) indexBuffer_->SetSize(newIBSize, false); geometry_->SetVertexBuffer(0, vertexBuffer_); geometry_->SetDrawRange(TRIANGLE_LIST, 0, numIndices_, 0, numVertices_); float* vertices = numVertices_ ? (float*)vertexBuffer_->Lock(0, numVertices_) : nullptr; unsigned short* indices = numIndices_ ? (unsigned short*)indexBuffer_->Lock(0, numIndices_) : nullptr; if (vertices && indices) { unsigned short indexStart = 0; for (List::ConstIterator i = decals_.Begin(); i != decals_.End(); ++i) { for (unsigned j = 0; j < i->vertices_.Size(); ++j) { const DecalVertex& vertex = i->vertices_[j]; *vertices++ = vertex.position_.x_; *vertices++ = vertex.position_.y_; *vertices++ = vertex.position_.z_; *vertices++ = vertex.normal_.x_; *vertices++ = vertex.normal_.y_; *vertices++ = vertex.normal_.z_; *vertices++ = vertex.texCoord_.x_; *vertices++ = vertex.texCoord_.y_; *vertices++ = vertex.tangent_.x_; *vertices++ = vertex.tangent_.y_; *vertices++ = vertex.tangent_.z_; *vertices++ = vertex.tangent_.w_; if (skinned_) { *vertices++ = vertex.blendWeights_[0]; *vertices++ = vertex.blendWeights_[1]; *vertices++ = vertex.blendWeights_[2]; *vertices++ = vertex.blendWeights_[3]; *vertices++ = *((float*)vertex.blendIndices_); } } for (unsigned j = 0; j < i->indices_.Size(); ++j) *indices++ = i->indices_[j] + indexStart; indexStart += i->vertices_.Size(); } } vertexBuffer_->Unlock(); vertexBuffer_->ClearDataLost(); indexBuffer_->Unlock(); indexBuffer_->ClearDataLost(); bufferDirty_ = false; } void DecalSet::UpdateSkinning() { // Use model's world transform in case a bone is missing const Matrix3x4& worldTransform = node_->GetWorldTransform(); for (unsigned i = 0; i < bones_.Size(); ++i) { const Bone& bone = bones_[i]; if (bone.node_) skinMatrices_[i] = bone.node_->GetWorldTransform() * bone.offsetMatrix_; else skinMatrices_[i] = worldTransform; } skinningDirty_ = false; } void DecalSet::UpdateBatch() { if (skinMatrices_.Size()) { batches_[0].geometryType_ = GEOM_SKINNED; batches_[0].worldTransform_ = &skinMatrices_[0]; batches_[0].numWorldTransforms_ = skinMatrices_.Size(); } else { batches_[0].geometryType_ = GEOM_STATIC; batches_[0].worldTransform_ = &node_->GetWorldTransform(); batches_[0].numWorldTransforms_ = 1; } } void DecalSet::AssignBoneNodes() { assignBonesPending_ = false; if (!node_) return; // Find the bone nodes from the node hierarchy and add listeners for (Vector::Iterator i = bones_.Begin(); i != bones_.End(); ++i) { Node* boneNode = node_->GetChild(i->name_, true); if (boneNode) boneNode->AddListener(this); i->node_ = boneNode; } } void DecalSet::UpdateEventSubscription(bool checkAllDecals) { Scene* scene = GetScene(); if (!scene) return; bool enabled = IsEnabledEffective(); if (enabled && checkAllDecals) { bool hasTimeLimitedDecals = false; for (List::ConstIterator i = decals_.Begin(); i != decals_.End(); ++i) { if (i->timeToLive_ > 0.0f) { hasTimeLimitedDecals = true; break; } } // If no time limited decals, no need to subscribe to scene update enabled = hasTimeLimitedDecals; } if (enabled && !subscribed_) { SubscribeToEvent(scene, E_SCENEPOSTUPDATE, URHO3D_HANDLER(DecalSet, HandleScenePostUpdate)); subscribed_ = true; } else if (!enabled && subscribed_) { UnsubscribeFromEvent(scene, E_SCENEPOSTUPDATE); subscribed_ = false; } } void DecalSet::HandleScenePostUpdate(StringHash eventType, VariantMap& eventData) { using namespace ScenePostUpdate; float timeStep = eventData[P_TIMESTEP].GetFloat(); for (List::Iterator i = decals_.Begin(); i != decals_.End();) { i->timer_ += timeStep; // Remove the decal if time to live expired if (i->timeToLive_ > 0.0f && i->timer_ > i->timeToLive_) i = RemoveDecal(i); else ++i; } } }