diff --git a/source/collada/PSAConvert.cpp b/source/collada/PSAConvert.cpp index 70f0d6cc80..054694a501 100644 --- a/source/collada/PSAConvert.cpp +++ b/source/collada/PSAConvert.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -91,8 +91,8 @@ public: // (TODO: sort out the timing/looping problems) const size_t boneCount = skeleton.GetBoneCount(); - if (boneCount > 64) - Log(LOG_ERROR, "Skeleton has too many bones %zu/64", boneCount); + if (boneCount > 192) + Log(LOG_ERROR, "Skeleton has too many bones %zu/192", boneCount); std::vector boneTransforms; diff --git a/source/graphics/Model.cpp b/source/graphics/Model.cpp index 982961bab9..cc7e390520 100644 --- a/source/graphics/Model.cpp +++ b/source/graphics/Model.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -239,8 +239,7 @@ void CModel::ValidatePosition() // in the vertex shader instead. // Using g_RenderingOptions to detect CPU vs GPU is a bit hacky, // and this doesn't allow the setting to change at runtime, but there isn't - // an obvious cleaner way to determine what data needs to be computed, - // and GPU skinning is a rarely-used experimental feature anyway. + // an obvious cleaner way to determine what data needs to be computed. bool worldSpaceBoneMatrices = !g_RenderingOptions.GetGPUSkinning(); bool computeBlendMatrices = !g_RenderingOptions.GetGPUSkinning(); diff --git a/source/ps/CStrInternStatic.h b/source/ps/CStrInternStatic.h index 76e8f5326a..33f91ebcd0 100644 --- a/source/ps/CStrInternStatic.h +++ b/source/ps/CStrInternStatic.h @@ -67,7 +67,6 @@ X(USE_DESCRIPTOR_INDEXING) X(USE_FANCY_EFFECTS) X(USE_FP_SHADOW) X(USE_GPU_INSTANCING) -X(USE_GPU_SKINNING) X(USE_INSTANCING) X(USE_NORMALS) X(USE_OBJECTCOLOR) @@ -81,6 +80,10 @@ X(USE_FOG) X(WATERTYPE_CLAP) X(WATERTYPE_LAKE) X2(_emptystring, "") +X(OutputNormalsTangents) +X(OutputPositions) +X(SkinData) +X(InputVertices) X(ambient) X(baseTex) X(blendTex) @@ -96,6 +99,7 @@ X(color) X(colorAdd) X(colorMul) X(compute_rcas) +X(compute_skinning) X(compute_upscale_fsr) X(debug_line) X(debug_overlay) @@ -127,6 +131,7 @@ X(murkiness) X(normalMap) X(normalMap2) X(objectColor) +X(offset) X(overlay_line) X(overlay_solid) X(outTex) @@ -175,6 +180,7 @@ X(transform) X(translation) X(upscale_bilinear) X(upscale_nearest) +X(vertexCount) X(viewInvTransform) X(water_high) X(water_simple) diff --git a/source/renderer/GPUSkinnedModelRenderer.cpp b/source/renderer/GPUSkinnedModelRenderer.cpp new file mode 100644 index 0000000000..75bc145f1c --- /dev/null +++ b/source/renderer/GPUSkinnedModelRenderer.cpp @@ -0,0 +1,483 @@ +/* Copyright (C) 2024 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "renderer/GPUSkinnedModelRenderer.h" + +#include "graphics/Color.h" +#include "graphics/LightEnv.h" +#include "graphics/Model.h" +#include "graphics/ModelDef.h" +#include "graphics/ShaderManager.h" +#include "maths/MathUtil.h" +#include "maths/Vector3D.h" +#include "maths/Vector4D.h" +#include "ps/CLogger.h" +#include "ps/containers/StaticVector.h" +#include "ps/CStrInternStatic.h" +#include "renderer/Renderer.h" +#include "renderer/RenderModifiers.h" +#include "renderer/VertexArray.h" +#include "third_party/mikktspace/weldmesh.h" + +namespace +{ + +// We have the following structure for input and output vertices: +// +// Created on a model load and read by a compute shader for each frame where +// the model is visible. +// InputVertex(size/stride=64): +// vec4/CVector4D tangent (offset=0) +// vec3/CVector3D normal (offset=16) +// vec3/CVector3D position (offset=32) +// +// Written by a compute shader for each frame where the model is visible. +// OutputPosition(size/stride=16): +// vec3/CVector3D position (offset=0) +// +// Written by a compute shader for each frame where the model is visible. +// Each component is 16-bits float to consume less memory. +// OutputNormalTangent(size/stride=16) +// 16bits vec3 normal (offset=0) +// 16bits vec4 tangent (offset=8) + +constexpr uint32_t INPUT_VERTEX_ATTRIBUTE_ALIGNMENT{16}; +constexpr uint32_t INPUT_VERTEX_TANGENT_OFFSET{0}; +constexpr uint32_t INPUT_VERTEX_NORMAL_OFFSET{16}; +constexpr uint32_t INPUT_VERTEX_POSITION_OFFSET{32}; + +constexpr uint32_t OUTPUT_POSITION_STRIDE{16}; +constexpr uint32_t OUTPUT_NORMAL_TANGENT_STRIDE{16}; +constexpr uint32_t OUTPUT_NORMAL_OFFSET{0}; +constexpr uint32_t OUTPUT_TANGENT_OFFSET{8}; + +class ModelDefRData : public CModelDefRPrivate +{ +public: + // Static per-CModel vertex array + VertexArray m_Array; + + // Position and normals are static + VertexArray::Attribute m_Position; + VertexArray::Attribute m_Normal; + VertexArray::Attribute m_Tangent; + + VertexArray m_BlendArray; + VertexArray::Attribute m_BlendJoints; + VertexArray::Attribute m_BlendWeights; + + VertexArray m_UVArray; + + // The number of UVs is determined by the model + std::vector m_UVs; + + Renderer::Backend::IVertexInputLayout* m_VertexInputLayout{nullptr}; + + // Indices are the same for all models, so share them + VertexIndexArray m_IndexArray; + + ModelDefRData(const CModelDefPtr& mdef); +}; + +ModelDefRData::ModelDefRData(const CModelDefPtr& modelDef) + : m_IndexArray(Renderer::Backend::IBuffer::Usage::TRANSFER_DST), + m_Array(Renderer::Backend::IBuffer::Type::VERTEX, + Renderer::Backend::IBuffer::Usage::TRANSFER_DST | + Renderer::Backend::IBuffer::Usage::STORAGE), + m_BlendArray(Renderer::Backend::IBuffer::Type::VERTEX, + Renderer::Backend::IBuffer::Usage::TRANSFER_DST | + Renderer::Backend::IBuffer::Usage::STORAGE), + m_UVArray(Renderer::Backend::IBuffer::Type::VERTEX, + Renderer::Backend::IBuffer::Usage::TRANSFER_DST) +{ + m_Position.format = Renderer::Backend::Format::R32G32B32_SFLOAT; + m_Array.AddAttribute(&m_Position); + + m_Normal.format = Renderer::Backend::Format::R32G32B32_SFLOAT; + m_Array.AddAttribute(&m_Normal); + + // TODO: switch to 16-bits tangents when possible. + m_Tangent.format = Renderer::Backend::Format::R32G32B32A32_SFLOAT; + m_Array.AddAttribute(&m_Tangent); + + m_UVs.resize(modelDef->GetNumUVsPerVertex()); + for (uint32_t index{0}; index < modelDef->GetNumUVsPerVertex(); ++index) + { + m_UVs[index].format = Renderer::Backend::Format::R32G32_SFLOAT; + m_UVArray.AddAttribute(&m_UVs[index]); + } + + // We can't use a lot of bones because it costs uniform memory. Recommended + // number of bones per model is 32. + // Add 1 to NumBones because of the special 'root' bone. + if (modelDef->GetNumBones() + 1 > 192) + LOGERROR("Model '%s' has too many bones %zu/192", modelDef->GetName().string8().c_str(), modelDef->GetNumBones() + 1); + ENSURE(modelDef->GetNumBones() + 1 <= 192); + + m_BlendJoints.format = Renderer::Backend::Format::R8G8B8A8_UINT; + m_BlendArray.AddAttribute(&m_BlendJoints); + + m_BlendWeights.format = Renderer::Backend::Format::R8G8B8A8_UNORM; + m_BlendArray.AddAttribute(&m_BlendWeights); + + // Generate tangents for the geometry: + + // floats per vertex; position + normal + tangent + UV*sets + GPUskinning (joint index and weight) + const uint32_t numberOfFloatsPerVertex{ + static_cast(3 + 3 + 4 + 2 * modelDef->GetNumUVsPerVertex() + 8)}; + + // the tangent generation can increase the number of vertices temporarily + // so reserve a bit more memory to avoid reallocations in GenTangents (in most cases) + std::vector newVertices; + newVertices.reserve(numberOfFloatsPerVertex * modelDef->GetNumVertices() * 2); + + // Generate the tangents. + ModelRenderer::GenTangents(modelDef, newVertices, true); + + // How many vertices do we have after generating tangents? + const uint32_t newNumberOfVertices{static_cast(newVertices.size()) / numberOfFloatsPerVertex}; + + std::vector remapTable(newNumberOfVertices); + std::vector vertexDataOut(newNumberOfVertices * numberOfFloatsPerVertex); + + // Re-weld the mesh to remove duplicated vertices. + const int finalNumberOfVertices{WeldMesh( + remapTable.data(), vertexDataOut.data(), newVertices.data(), newNumberOfVertices, numberOfFloatsPerVertex)}; + + // Copy the model data to graphics memory. + + m_Array.SetNumberOfVertices(finalNumberOfVertices); + m_Array.SetMinimumAttributeAlignment(INPUT_VERTEX_ATTRIBUTE_ALIGNMENT); + m_Array.Layout(); + + m_BlendArray.SetNumberOfVertices(finalNumberOfVertices); + m_BlendArray.Layout(); + + m_UVArray.SetNumberOfVertices(finalNumberOfVertices); + m_UVArray.Layout(); + + VertexArrayIterator positionIt{m_Position.GetIterator()}; + VertexArrayIterator normalIt{m_Normal.GetIterator()}; + VertexArrayIterator tangentIt{m_Tangent.GetIterator()}; + + VertexArrayIterator blendJointsIt{m_BlendJoints.GetIterator()}; + VertexArrayIterator blendWeightsIt{m_BlendWeights.GetIterator()}; + + // Copy everything into the vertex array. + for (int index{0}; index < finalNumberOfVertices; ++index) + { + uint32_t inputDataOffset{numberOfFloatsPerVertex * index}; + + positionIt[index] = CVector3D{ + vertexDataOut[inputDataOffset + 0], + vertexDataOut[inputDataOffset + 1], + vertexDataOut[inputDataOffset + 2]}; + inputDataOffset += 3; + + normalIt[index] = CVector3D{ + vertexDataOut[inputDataOffset + 0], + vertexDataOut[inputDataOffset + 1], + vertexDataOut[inputDataOffset + 2]}; + inputDataOffset += 3; + + tangentIt[index] = CVector4D{ + vertexDataOut[inputDataOffset + 0], + vertexDataOut[inputDataOffset + 1], + vertexDataOut[inputDataOffset + 2], + vertexDataOut[inputDataOffset + 3]}; + inputDataOffset += 4; + + for (uint32_t j{0}; j < 4; ++j) + { + blendJointsIt[index][j] = static_cast(vertexDataOut[inputDataOffset + 0 + 2 * j]); + blendWeightsIt[index][j] = static_cast(vertexDataOut[inputDataOffset + 1 + 2 * j]); + } + inputDataOffset += 8; + + for (uint32_t uvIndex{0}; uvIndex < modelDef->GetNumUVsPerVertex(); uvIndex++) + { + VertexArrayIterator UVit{m_UVs[uvIndex].GetIterator()}; + UVit[index][0] = vertexDataOut[inputDataOffset + 0 + 2 * uvIndex]; + UVit[index][1] = vertexDataOut[inputDataOffset + 1 + 2 * uvIndex]; + } + } + + // Upload vertex data. + m_Array.Upload(); + m_Array.FreeBackingStore(); + + m_BlendArray.Upload(); + m_BlendArray.FreeBackingStore(); + + if (m_UVArray.GetStride() > 0) + { + m_UVArray.Upload(); + m_UVArray.FreeBackingStore(); + } + + ENSURE(m_Array.GetStride() == INPUT_VERTEX_ATTRIBUTE_ALIGNMENT * 4); + ENSURE(m_Position.offset == INPUT_VERTEX_POSITION_OFFSET); + ENSURE(m_Normal.offset == INPUT_VERTEX_NORMAL_OFFSET); + ENSURE(m_Tangent.offset == INPUT_VERTEX_TANGENT_OFFSET); + + ENSURE(m_BlendArray.GetStride() == 8); + ENSURE(m_BlendJoints.offset % 4 == 0); + ENSURE(m_BlendWeights.offset % 4 == 0); + + m_IndexArray.SetNumberOfVertices(modelDef->GetNumFaces() * 3); + m_IndexArray.Layout(); + + // Re-index geometry and upload index. + VertexArrayIterator indices{m_IndexArray.GetIterator()}; + for (uint32_t index{0}; index < modelDef->GetNumFaces() * 3; ++index) + indices[index] = remapTable[index]; + m_IndexArray.Upload(); + m_IndexArray.FreeBackingStore(); + + constexpr size_t MAX_UV{2}; + + PS::StaticVector attributes{ + {Renderer::Backend::VertexAttributeStream::POSITION, + m_Position.format, 0, OUTPUT_POSITION_STRIDE, + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}, + {Renderer::Backend::VertexAttributeStream::NORMAL, + Renderer::Backend::Format::R16G16B16_SFLOAT, OUTPUT_NORMAL_OFFSET, OUTPUT_NORMAL_TANGENT_STRIDE, + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1}, + {Renderer::Backend::VertexAttributeStream::UV2, + Renderer::Backend::Format::R16G16B16A16_SFLOAT, OUTPUT_TANGENT_OFFSET, OUTPUT_NORMAL_TANGENT_STRIDE, + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 1} + }; + + for (size_t uv{0}; uv < std::min(MAX_UV, modelDef->GetNumUVsPerVertex()); ++uv) + { + const Renderer::Backend::VertexAttributeStream stream = + static_cast( + static_cast(Renderer::Backend::VertexAttributeStream::UV0) + uv); + attributes.push_back({ + stream, m_UVs[uv].format, m_UVs[uv].offset, m_UVArray.GetStride(), + Renderer::Backend::VertexAttributeRate::PER_VERTEX, 2}); + } + + m_VertexInputLayout = g_Renderer.GetVertexInputLayout({attributes.begin(), attributes.end()}); +} + +struct ModelRData : public CModelRData +{ + // We have a separate position array because we don't need other attributes + // for some passes (like shadows). + CVertexBufferManager::Handle m_PositionHandle; + CVertexBufferManager::Handle m_NormalTangentHandle; + + ModelRData(const void* key) + : CModelRData(key) + {} +}; + +} // anonymous namespace + +struct GPUSkinnedModelModelRenderer::Internals +{ + // Previously prepared modeldef + ModelDefRData* modelDefRData; + + // Shader technique for models with up to 64 bones. + CShaderTechniquePtr skinningShaderTechnique64; + // Shader technique for models with up to 192 bones. + CShaderTechniquePtr skinningShaderTechnique192; +}; + +GPUSkinnedModelModelRenderer::GPUSkinnedModelModelRenderer() + : m(std::make_unique()) +{ + m->modelDefRData = nullptr; + CShaderDefines shaderDefines64; + shaderDefines64.Add(CStrIntern{"MAX_BONES"}, CStrIntern{"64"}); + m->skinningShaderTechnique64 = g_Renderer.GetShaderManager().LoadEffect(str_compute_skinning, shaderDefines64); + CShaderDefines shaderDefines192; + shaderDefines192.Add(CStrIntern{"MAX_BONES"}, CStrIntern{"192"}); + m->skinningShaderTechnique192 = g_Renderer.GetShaderManager().LoadEffect(str_compute_skinning, shaderDefines192); +} + +GPUSkinnedModelModelRenderer::~GPUSkinnedModelModelRenderer() = default; + +CModelRData* GPUSkinnedModelModelRenderer::CreateModelData(const void* key, CModel* model) +{ + ENSURE(model->IsSkinned()); + CModelDefPtr modelDef{model->GetModelDef()}; + ModelDefRData* modelDefRData{static_cast(modelDef->GetRenderData(m.get()))}; + + if (!modelDefRData) + { + modelDefRData = new ModelDefRData(modelDef); + modelDef->SetRenderData(m.get(), modelDefRData); + } + + ModelRData* modelRData{new ModelRData(key)}; + + const size_t numberOfVertices{modelDefRData->m_Array.GetNumberOfVertices()}; + modelRData->m_PositionHandle = g_Renderer.GetVertexBufferManager().AllocateChunk( + OUTPUT_POSITION_STRIDE, numberOfVertices, Renderer::Backend::IBuffer::Type::VERTEX, + Renderer::Backend::IBuffer::Usage::STORAGE, + nullptr, CVertexBufferManager::Group::WATER); + modelRData->m_NormalTangentHandle = g_Renderer.GetVertexBufferManager().AllocateChunk( + OUTPUT_NORMAL_TANGENT_STRIDE, numberOfVertices, Renderer::Backend::IBuffer::Type::VERTEX, + Renderer::Backend::IBuffer::Usage::STORAGE, + nullptr, CVertexBufferManager::Group::WATER); + + return modelRData; +} + +void GPUSkinnedModelModelRenderer::UpdateModelsData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + PS::span models) +{ + if (models.empty()) + return; + + GPU_SCOPED_LABEL(deviceCommandContext, "Compute Skinning"); + + // Models with up to 192 bones. + std::vector models192; + + deviceCommandContext->InsertMemoryBarrier( + Renderer::Backend::PipelineStage::VERTEX_INPUT, Renderer::Backend::PipelineStage::COMPUTE_SHADER, + Renderer::Backend::Access::VERTEX_ATTRIBUTE_READ | Renderer::Backend::Access::INDEX_READ, + Renderer::Backend::Access::SHADER_READ | Renderer::Backend::Access::SHADER_WRITE); + deviceCommandContext->BeginComputePass(); + deviceCommandContext->SetComputePipelineState( + m->skinningShaderTechnique64->GetComputePipelineState()); + + for (CModel* model : models) + { + ENSURE(model->IsSkinned()); + CModelDefPtr modelDef{model->GetModelDef()}; + if (modelDef->GetNumBones() + 1 > 64) + { + models192.emplace_back(model); + continue; + } + CModelRData* rdata{static_cast(model->GetRenderData())}; + UpdateModelData(deviceCommandContext, m->skinningShaderTechnique64->GetShader(), model, rdata, rdata->m_UpdateFlags); + } + + if (!models192.empty()) + { + deviceCommandContext->SetComputePipelineState( + m->skinningShaderTechnique192->GetComputePipelineState()); + for (CModel* model : models192) + { + CModelRData* rdata{static_cast(model->GetRenderData())}; + UpdateModelData(deviceCommandContext, m->skinningShaderTechnique192->GetShader(), model, rdata, rdata->m_UpdateFlags); + } + } + + deviceCommandContext->EndComputePass(); + deviceCommandContext->InsertMemoryBarrier( + Renderer::Backend::PipelineStage::COMPUTE_SHADER, Renderer::Backend::PipelineStage::VERTEX_INPUT, + Renderer::Backend::Access::SHADER_READ | Renderer::Backend::Access::SHADER_WRITE, + Renderer::Backend::Access::VERTEX_ATTRIBUTE_READ | Renderer::Backend::Access::INDEX_READ); +} + +void GPUSkinnedModelModelRenderer::UpdateModelData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + Renderer::Backend::IShaderProgram* shaderProgram, + CModel* model, CModelRData* data, int updateflags) +{ + CModelDefPtr modelDef{model->GetModelDef()}; + ModelDefRData* modelDefRData{static_cast(modelDef->GetRenderData(m.get()))}; + ModelRData* modelRData{static_cast(data)}; + + if (updateflags & RENDERDATA_UPDATE_VERTICES) + { + constexpr uint32_t threadGroupWorkRegionDim{64}; + const uint32_t vertexCount{static_cast(modelRData->m_PositionHandle->m_Count)}; + const uint32_t dispatchGroupCountX{DivideRoundUp(vertexCount, threadGroupWorkRegionDim)}; + + // Bind matrices for current animation state. + // Add 1 to NumBones because of the special 'root' bone. + deviceCommandContext->SetUniform( + shaderProgram->GetBindingSlot(str_skinBlendMatrices), + PS::span( + model->GetAnimatedBoneMatrices()[0]._data, + model->GetAnimatedBoneMatrices()[0].AsFloatArray().size() * (modelDef->GetNumBones() + 1))); + + ENSURE(modelRData->m_PositionHandle->m_Count == modelRData->m_NormalTangentHandle->m_Count); + deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_vertexCount), + static_cast(vertexCount)); + deviceCommandContext->SetUniform(shaderProgram->GetBindingSlot(str_offset), + static_cast(modelDefRData->m_Array.GetOffset()), + static_cast(modelDefRData->m_BlendArray.GetOffset()), + static_cast(modelRData->m_PositionHandle->m_Index), + static_cast(modelRData->m_NormalTangentHandle->m_Index)); + deviceCommandContext->SetStorageBuffer(shaderProgram->GetBindingSlot(str_InputVertices), modelDefRData->m_Array.GetBuffer()); + deviceCommandContext->SetStorageBuffer(shaderProgram->GetBindingSlot(str_SkinData), modelDefRData->m_BlendArray.GetBuffer()); + deviceCommandContext->SetStorageBuffer(shaderProgram->GetBindingSlot(str_OutputPositions), modelRData->m_PositionHandle->m_Owner->GetBuffer()); + deviceCommandContext->SetStorageBuffer(shaderProgram->GetBindingSlot(str_OutputNormalsTangents), modelRData->m_NormalTangentHandle->m_Owner->GetBuffer()); + deviceCommandContext->Dispatch(dispatchGroupCountX, 1, 1); + } +} + +void GPUSkinnedModelModelRenderer::UploadModelsData( + Renderer::Backend::IDeviceCommandContext* UNUSED(deviceCommandContext), + PS::span UNUSED(models)) +{ +} + +void GPUSkinnedModelModelRenderer::PrepareModelDef( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + const CModelDef& def) +{ + m->modelDefRData = static_cast(def.GetRenderData(m.get())); + ENSURE(m->modelDefRData); + + deviceCommandContext->SetVertexInputLayout(m->modelDefRData->m_VertexInputLayout); + deviceCommandContext->SetIndexBuffer(m->modelDefRData->m_IndexArray.GetBuffer()); + + if (m->modelDefRData->m_UVArray.GetStride() > 0) + { + const uint32_t firstVertexOffset{ + m->modelDefRData->m_UVArray.GetOffset() * m->modelDefRData->m_UVArray.GetStride()}; + deviceCommandContext->SetVertexBuffer( + 2, m->modelDefRData->m_UVArray.GetBuffer(), firstVertexOffset); + } +} + +void GPUSkinnedModelModelRenderer::RenderModel( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + Renderer::Backend::IShaderProgram* UNUSED(shader), CModel* model, CModelRData* data) +{ + ModelRData* modelRData{static_cast(data)}; + + // Render the lot. + const size_t numberOfFaces{model->GetModelDef()->GetNumFaces()}; + + deviceCommandContext->SetVertexBuffer( + 0, modelRData->m_PositionHandle->m_Owner->GetBuffer(), + modelRData->m_PositionHandle->m_Index * OUTPUT_POSITION_STRIDE); + deviceCommandContext->SetVertexBuffer( + 1, modelRData->m_NormalTangentHandle->m_Owner->GetBuffer(), + modelRData->m_NormalTangentHandle->m_Index * OUTPUT_NORMAL_TANGENT_STRIDE); + + deviceCommandContext->DrawIndexed( + m->modelDefRData->m_IndexArray.GetOffset(), numberOfFaces * 3, 0); + + // Bump stats. + g_Renderer.m_Stats.m_DrawCalls++; + g_Renderer.m_Stats.m_ModelTris += numberOfFaces; +} diff --git a/source/renderer/GPUSkinnedModelRenderer.h b/source/renderer/GPUSkinnedModelRenderer.h new file mode 100644 index 0000000000..51fd4686a6 --- /dev/null +++ b/source/renderer/GPUSkinnedModelRenderer.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2024 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_RENDERER_GPUSKINNEDMODELRENDERER +#define INCLUDED_RENDERER_GPUSKINNEDMODELRENDERER + +#include "renderer/ModelVertexRenderer.h" + +#include + +/** + * Render animated models using a ShaderRenderModifier. + * It calculates vertex data for models on the GPU side. + * This computes and binds per-vertex data; the modifier is responsible + * for setting any shader uniforms etc. + */ +class GPUSkinnedModelModelRenderer : public ModelVertexRenderer +{ +public: + GPUSkinnedModelModelRenderer(); + ~GPUSkinnedModelModelRenderer(); + + CModelRData* CreateModelData(const void* key, CModel* model) override; + + void UpdateModelsData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + PS::span models) override; + + void UploadModelsData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + PS::span models) override; + + void PrepareModelDef( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + const CModelDef& def) override; + void RenderModel(Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + Renderer::Backend::IShaderProgram* shader, CModel* model, CModelRData* data) override; + +private: + void UpdateModelData( + Renderer::Backend::IDeviceCommandContext* deviceCommandContext, + Renderer::Backend::IShaderProgram* shaderProgram, + CModel* model, CModelRData* data, int updateflags); + + struct Internals; + const std::unique_ptr m; +}; + +#endif // INCLUDED_RENDERER_GPUSKINNEDMODELRENDERER diff --git a/source/renderer/InstancingModelRenderer.cpp b/source/renderer/InstancingModelRenderer.cpp index 663a1077db..2bddcb2914 100644 --- a/source/renderer/InstancingModelRenderer.cpp +++ b/source/renderer/InstancingModelRenderer.cpp @@ -42,8 +42,6 @@ struct IModelDef : public CModelDefRPrivate VertexArray::Attribute m_Position; VertexArray::Attribute m_Normal; VertexArray::Attribute m_Tangent; - VertexArray::Attribute m_BlendJoints; // valid iff gpuSkinning == true - VertexArray::Attribute m_BlendWeights; // valid iff gpuSkinning == true /// The number of UVs is determined by the model std::vector m_UVs; @@ -53,11 +51,11 @@ struct IModelDef : public CModelDefRPrivate /// Indices are the same for all models, so share them VertexIndexArray m_IndexArray; - IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateTangents); + IModelDef(const CModelDefPtr& mdef, bool calculateTangents); }; -IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateTangents) +IModelDef::IModelDef(const CModelDefPtr& mdef, bool calculateTangents) : m_IndexArray(Renderer::Backend::IBuffer::Usage::TRANSFER_DST), m_Array(Renderer::Backend::IBuffer::Type::VERTEX, Renderer::Backend::IBuffer::Usage::TRANSFER_DST) { @@ -76,22 +74,6 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT m_Array.AddAttribute(&m_UVs[i]); } - if (gpuSkinning) - { - // We can't use a lot of bones because it costs uniform memory. Recommended - // number of bones per model is 32. - // Add 1 to NumBones because of the special 'root' bone. - if (mdef->GetNumBones() + 1 > 64) - LOGERROR("Model '%s' has too many bones %zu/64", mdef->GetName().string8().c_str(), mdef->GetNumBones() + 1); - ENSURE(mdef->GetNumBones() + 1 <= 64); - - m_BlendJoints.format = Renderer::Backend::Format::R8G8B8A8_UINT; - m_Array.AddAttribute(&m_BlendJoints); - - m_BlendWeights.format = Renderer::Backend::Format::R8G8B8A8_UNORM; - m_Array.AddAttribute(&m_BlendWeights); - } - if (calculateTangents) { // Generate tangents for the geometry:- @@ -99,12 +81,8 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT m_Tangent.format = Renderer::Backend::Format::R32G32B32A32_SFLOAT; m_Array.AddAttribute(&m_Tangent); - // floats per vertex; position + normal + tangent + UV*sets [+ GPUskinning] + // floats per vertex; position + normal + tangent + UV*sets int numVertexAttrs = 3 + 3 + 4 + 2 * mdef->GetNumUVsPerVertex(); - if (gpuSkinning) - { - numVertexAttrs += 8; - } // the tangent generation can increase the number of vertices temporarily // so reserve a bit more memory to avoid reallocations in GenTangents (in most cases) @@ -112,7 +90,7 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT newVertices.reserve(numVertexAttrs * numVertices * 2); // Generate the tangents - ModelRenderer::GenTangents(mdef, newVertices, gpuSkinning); + ModelRenderer::GenTangents(mdef, newVertices, false); // how many vertices do we have after generating tangents? int newNumVert = newVertices.size() / numVertexAttrs; @@ -133,14 +111,6 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT VertexArrayIterator Normal = m_Normal.GetIterator(); VertexArrayIterator Tangent = m_Tangent.GetIterator(); - VertexArrayIterator BlendJoints; - VertexArrayIterator BlendWeights; - if (gpuSkinning) - { - BlendJoints = m_BlendJoints.GetIterator(); - BlendWeights = m_BlendWeights.GetIterator(); - } - // copy everything into the vertex array for (int i = 0; i < numVertices2; i++) { @@ -156,16 +126,6 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT vertexDataOut[q + 3]); q += 4; - if (gpuSkinning) - { - for (size_t j = 0; j < 4; ++j) - { - BlendJoints[i][j] = (u8)vertexDataOut[q + 0 + 2 * j]; - BlendWeights[i][j] = (u8)vertexDataOut[q + 1 + 2 * j]; - } - q += 8; - } - for (size_t j = 0; j < mdef->GetNumUVsPerVertex(); j++) { VertexArrayIterator UVit = m_UVs[j].GetIterator(); @@ -214,21 +174,6 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT ModelRenderer::BuildUV(mdef, UVit, i); } - if (gpuSkinning) - { - VertexArrayIterator BlendJoints = m_BlendJoints.GetIterator(); - VertexArrayIterator BlendWeights = m_BlendWeights.GetIterator(); - for (size_t i = 0; i < numVertices; ++i) - { - const SModelVertex& vtx = mdef->GetVertices()[i]; - for (size_t j = 0; j < 4; ++j) - { - BlendJoints[i][j] = vtx.m_Blend.m_Bone[j]; - BlendWeights[i][j] = (u8)(255.f * vtx.m_Blend.m_Weight[j]); - } - } - } - m_Array.Upload(); m_Array.FreeBackingStore(); @@ -261,23 +206,10 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}); } - // GPU skinning requires extra attributes to compute positions/normals. - if (gpuSkinning) - { - attributes.push_back({ - Renderer::Backend::VertexAttributeStream::UV2, - m_BlendJoints.format, m_BlendJoints.offset, stride, - Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}); - attributes.push_back({ - Renderer::Backend::VertexAttributeStream::UV3, - m_BlendWeights.format, m_BlendWeights.offset, stride, - Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}); - } - if (calculateTangents) { attributes.push_back({ - Renderer::Backend::VertexAttributeStream::UV4, + Renderer::Backend::VertexAttributeStream::UV2, m_Tangent.format, m_Tangent.offset, stride, Renderer::Backend::VertexAttributeRate::PER_VERTEX, 0}); } @@ -287,23 +219,17 @@ IModelDef::IModelDef(const CModelDefPtr& mdef, bool gpuSkinning, bool calculateT struct InstancingModelRendererInternals { - bool gpuSkinning; - bool calculateTangents; /// Previously prepared modeldef IModelDef* imodeldef; - - /// Index base for imodeldef - u8* imodeldefIndexBase; }; // Construction and Destruction -InstancingModelRenderer::InstancingModelRenderer(bool gpuSkinning, bool calculateTangents) +InstancingModelRenderer::InstancingModelRenderer(bool calculateTangents) { m = new InstancingModelRendererInternals; - m->gpuSkinning = gpuSkinning; m->calculateTangents = calculateTangents; m->imodeldef = 0; } @@ -320,14 +246,11 @@ CModelRData* InstancingModelRenderer::CreateModelData(const void* key, CModel* m CModelDefPtr mdef = model->GetModelDef(); IModelDef* imodeldef = (IModelDef*)mdef->GetRenderData(m); - if (m->gpuSkinning) - ENSURE(model->IsSkinned()); - else - ENSURE(!model->IsSkinned()); + ENSURE(!model->IsSkinned()); if (!imodeldef) { - imodeldef = new IModelDef(mdef, m->gpuSkinning, m->calculateTangents); + imodeldef = new IModelDef(mdef, m->calculateTangents); mdef->SetRenderData(m, imodeldef); } @@ -371,21 +294,10 @@ void InstancingModelRenderer::PrepareModelDef( // Render one model void InstancingModelRenderer::RenderModel( Renderer::Backend::IDeviceCommandContext* deviceCommandContext, - Renderer::Backend::IShaderProgram* shader, CModel* model, CModelRData* UNUSED(data)) + Renderer::Backend::IShaderProgram* UNUSED(shader), CModel* model, CModelRData* UNUSED(data)) { const CModelDefPtr& mdldef = model->GetModelDef(); - if (m->gpuSkinning) - { - // Bind matrices for current animation state. - // Add 1 to NumBones because of the special 'root' bone. - deviceCommandContext->SetUniform( - shader->GetBindingSlot(str_skinBlendMatrices), - PS::span( - model->GetAnimatedBoneMatrices()[0]._data, - model->GetAnimatedBoneMatrices()[0].AsFloatArray().size() * (mdldef->GetNumBones() + 1))); - } - // Render the lot. const size_t numberOfFaces = mdldef->GetNumFaces(); diff --git a/source/renderer/InstancingModelRenderer.h b/source/renderer/InstancingModelRenderer.h index 41e99fffb4..7b44a00f2e 100644 --- a/source/renderer/InstancingModelRenderer.h +++ b/source/renderer/InstancingModelRenderer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -35,7 +35,7 @@ struct InstancingModelRendererInternals; class InstancingModelRenderer : public ModelVertexRenderer { public: - InstancingModelRenderer(bool gpuSkinning, bool calculateTangents); + InstancingModelRenderer(bool calculateTangents); ~InstancingModelRenderer(); // Implementations diff --git a/source/renderer/ModelRenderer.cpp b/source/renderer/ModelRenderer.cpp index b793aead57..03906065bd 100644 --- a/source/renderer/ModelRenderer.cpp +++ b/source/renderer/ModelRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify diff --git a/source/renderer/ModelVertexRenderer.h b/source/renderer/ModelVertexRenderer.h index cb7e14183b..dcbfb62501 100644 --- a/source/renderer/ModelVertexRenderer.h +++ b/source/renderer/ModelVertexRenderer.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify diff --git a/source/renderer/Renderer.h b/source/renderer/Renderer.h index 10d93d01c0..e0c482fe36 100644 --- a/source/renderer/Renderer.h +++ b/source/renderer/Renderer.h @@ -152,6 +152,7 @@ protected: friend class CPatchRData; friend class CPUSkinnedModelVertexRenderer; friend class CRenderingOptions; + friend class GPUSkinnedModelModelRenderer; friend class InstancingModelRenderer; bool ShouldRender() const; diff --git a/source/renderer/RenderingOptions.cpp b/source/renderer/RenderingOptions.cpp index aa9f521b33..701b31a2ed 100644 --- a/source/renderer/RenderingOptions.cpp +++ b/source/renderer/RenderingOptions.cpp @@ -254,14 +254,14 @@ void CRenderingOptions::ReadConfigAndSetupHooks() m_ConfigHooks->Setup("gpuskinning", [this]() { bool enabled; CFG_GET_VAL("gpuskinning", enabled); + const Renderer::Backend::IDevice::Capabilities& capabilities{ + g_VideoMode.GetBackendDevice()->GetCapabilities()}; if (enabled) { - if (g_VideoMode.GetBackendDevice()->GetBackend() == Renderer::Backend::Backend::GL_ARB) - LOGWARNING("GPUSkinning has been disabled, because it is not supported with ARB shaders."); - else if (g_VideoMode.GetBackendDevice()->GetBackend() == Renderer::Backend::Backend::VULKAN) - LOGWARNING("GPUSkinning has been disabled, because it is not supported for Vulkan backend yet."); - else + if (capabilities.computeShaders && capabilities.storage) m_GPUSkinning = true; + else + LOGMESSAGE("GPU skinning isn't supported on the current hardware."); } }); diff --git a/source/renderer/SceneRenderer.cpp b/source/renderer/SceneRenderer.cpp index 932f0f3d3b..bd34c599e3 100644 --- a/source/renderer/SceneRenderer.cpp +++ b/source/renderer/SceneRenderer.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -46,6 +46,7 @@ #include "renderer/backend/IDevice.h" #include "renderer/CPUSkinnedModelRenderer.h" #include "renderer/DebugRenderer.h" +#include "renderer/GPUSkinnedModelRenderer.h" #include "renderer/InstancingModelRenderer.h" #include "renderer/ModelRenderer.h" #include "renderer/OverlayRenderer.h" @@ -151,10 +152,7 @@ public: { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) - { contextSkinned.Add(str_USE_INSTANCING, str_1); - contextSkinned.Add(str_USE_GPU_SKINNING, str_1); - } Model.NormalSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.NormalUnskinned != Model.NormalSkinned) @@ -174,10 +172,7 @@ public: { CShaderDefines contextSkinned = context; if (g_RenderingOptions.GetGPUSkinning()) - { contextSkinned.Add(str_USE_INSTANCING, str_1); - contextSkinned.Add(str_USE_GPU_SKINNING, str_1); - } Model.TranspSkinned->Render(deviceCommandContext, Model.ModShader, contextSkinned, cullGroup, flags); if (Model.TranspUnskinned != Model.TranspSkinned) @@ -246,11 +241,11 @@ void CSceneRenderer::ReloadShaders(Renderer::Backend::IDevice* device) ENSURE(g_RenderingOptions.GetRenderPath() != RenderPath::FIXED); m->Model.VertexRendererShader = ModelVertexRendererPtr(new CPUSkinnedModelVertexRenderer()); - m->Model.VertexInstancingShader = ModelVertexRendererPtr(new InstancingModelRenderer(false, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); + m->Model.VertexInstancingShader = ModelVertexRendererPtr(new InstancingModelRenderer(device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); - if (g_RenderingOptions.GetGPUSkinning()) // TODO: should check caps and GLSL etc too + if (g_RenderingOptions.GetGPUSkinning()) { - m->Model.VertexGPUSkinningShader = ModelVertexRendererPtr(new InstancingModelRenderer(true, device->GetBackend() != Renderer::Backend::Backend::GL_ARB)); + m->Model.VertexGPUSkinningShader = ModelVertexRendererPtr(new GPUSkinnedModelModelRenderer()); m->Model.NormalSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); m->Model.TranspSkinned = ModelRendererPtr(new ShaderModelRenderer(m->Model.VertexGPUSkinningShader)); } diff --git a/source/renderer/VertexArray.cpp b/source/renderer/VertexArray.cpp index 0f52d3b720..273524fc27 100644 --- a/source/renderer/VertexArray.cpp +++ b/source/renderer/VertexArray.cpp @@ -18,6 +18,7 @@ #include "precompiled.h" #include "lib/alignment.h" +#include "lib/bits.h" #include "lib/sysdep/rtl.h" #include "maths/Vector3D.h" #include "maths/Vector4D.h" @@ -43,12 +44,18 @@ uint32_t GetAttributeSize(const Renderer::Backend::Format format) return sizeof(u8); case Renderer::Backend::Format::R16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16_UINT: FALLTHROUGH; - case Renderer::Backend::Format::R16_SINT: + case Renderer::Backend::Format::R16_SINT: FALLTHROUGH; + case Renderer::Backend::Format::R16_SFLOAT: return sizeof(u16); case Renderer::Backend::Format::R16G16_UNORM: FALLTHROUGH; case Renderer::Backend::Format::R16G16_UINT: FALLTHROUGH; - case Renderer::Backend::Format::R16G16_SINT: + case Renderer::Backend::Format::R16G16_SINT: FALLTHROUGH; + case Renderer::Backend::Format::R16G16_SFLOAT: return sizeof(u16) * 2; + case Renderer::Backend::Format::R16G16B16_SFLOAT: + return sizeof(u16) * 3; + case Renderer::Backend::Format::R16G16B16A16_SFLOAT: + return sizeof(u16) * 4; case Renderer::Backend::Format::R32_SFLOAT: return sizeof(float); case Renderer::Backend::Format::R32G32_SFLOAT: @@ -69,10 +76,6 @@ VertexArray::VertexArray( const Renderer::Backend::IBuffer::Type type, const uint32_t usage) : m_Type(type), m_Usage(usage) { - m_NumberOfVertices = 0; - - m_BackingStore = 0; - m_Stride = 0; } VertexArray::~VertexArray() @@ -99,6 +102,16 @@ void VertexArray::SetNumberOfVertices(const size_t numberOfVertices) m_NumberOfVertices = numberOfVertices; } +void VertexArray::SetMinimumAttributeAlignment(const uint32_t minimumAttributeAlignment) +{ + ENSURE(minimumAttributeAlignment >= 4 || is_pow2(minimumAttributeAlignment)); + if (minimumAttributeAlignment == m_MinimumAttributeAlignment) + return; + + Free(); + m_MinimumAttributeAlignment = minimumAttributeAlignment; +} + // Add vertex attributes like Position, Normal, UV void VertexArray::AddAttribute(Attribute* attr) { @@ -248,6 +261,8 @@ void VertexArray::Layout() if (m_Type == Renderer::Backend::IBuffer::Type::VERTEX) m_Stride = Align<4>(m_Stride); + if (m_MinimumAttributeAlignment > 0) + m_Stride = (m_Stride + m_MinimumAttributeAlignment - 1) & ~(m_MinimumAttributeAlignment - 1); } if (m_Type == Renderer::Backend::IBuffer::Type::VERTEX) diff --git a/source/renderer/VertexArray.h b/source/renderer/VertexArray.h index 47956aa04d..e023c0a24e 100644 --- a/source/renderer/VertexArray.h +++ b/source/renderer/VertexArray.h @@ -168,6 +168,10 @@ public: // Add vertex attributes void AddAttribute(Attribute* attr); + // Sets the minimum alignment for each attribute to have offsets multiples + // of `minimumAttributeAlignment`. + void SetMinimumAttributeAlignment(const uint32_t minimumAttributeAlignment); + size_t GetNumberOfVertices() const { return m_NumberOfVertices; } uint32_t GetStride() const { return m_Stride; } @@ -203,13 +207,14 @@ private: } Renderer::Backend::IBuffer::Type m_Type; - uint32_t m_Usage = 0; - size_t m_NumberOfVertices; + uint32_t m_Usage{0}; + uint32_t m_MinimumAttributeAlignment{0}; + size_t m_NumberOfVertices{0}; std::vector m_Attributes; CVertexBufferManager::Handle m_VB; - uint32_t m_Stride; - char* m_BackingStore; // 16-byte aligned, to allow fast SSE access + uint32_t m_Stride{0}; + char* m_BackingStore{nullptr}; // 16-byte aligned, to allow fast SSE access }; /** diff --git a/source/renderer/VertexBufferManager.cpp b/source/renderer/VertexBufferManager.cpp index 69b4e6aace..8015f8ccee 100644 --- a/source/renderer/VertexBufferManager.cpp +++ b/source/renderer/VertexBufferManager.cpp @@ -149,8 +149,10 @@ CVertexBufferManager::Handle CVertexBufferManager::AllocateChunk( char bufferName[64] = {0}; snprintf( - bufferName, std::size(bufferName), "%s (%s, %zu%s)", - GetBufferTypeName(type), GetGroupName(group), vertexSize, ((usage & Renderer::Backend::IBuffer::Usage::DYNAMIC) ? ", dynamic" : "")); + bufferName, std::size(bufferName), "%s (%s, %zu%s%s)", + GetBufferTypeName(type), GetGroupName(group), vertexSize, + ((usage & Renderer::Backend::IBuffer::Usage::DYNAMIC) ? ", dynamic" : ""), + ((usage & Renderer::Backend::IBuffer::Usage::STORAGE) ? ", storage" : "")); // got this far; need to allocate a new buffer buffers.emplace_back(