Compare commits

...
5 Commits
Author SHA1 Message Date
atdunbg dd7c67a116 feat: ImGui docking Editor + 显示链/帧同步/输入语义重构
- App/editor: Hierarchy/Inspector/Viewport/Stats 四面板 + DockBuilder 默认布局,main.cpp 瘦身为场景搭建与生命周期
- 显示链: Tonemap→PresentTarget 经 ImGui::Image 呈现(零色偏),backbuffer 仅承载 ImGui;Direct/Viewport 双显示模式 + 面板分辨率上报
- 帧首同步 BeginFrameSync: resize/vsync/尺寸/模式意图统一帧首落定(修描述符悬垂);修复 vsync 意图未落 swapchain 导致的每帧重建交换链
- 输入: Editor 每帧判定键盘/鼠标归属(打字让位、Viewport hover/focus 放行、Fullscreen 全放行)
- 材质: roughness 独立槽位 + 非 glTF 金属/粗糙贴图映射;透明队列 depthWrite=FALSE + translucent 独立 shader + scratch 清帧
- FBX: 非蒙皮网格烘焙节点链全局变换(Z-up 旋转/单位换算落顶点)
- Hierarchy 行 ImGui ID 以 UUID 作用域(重名不冲突、改名不丢选中)
2026-09-30 00:24:53 +08:00
atdunbg b76922824c feat: 透明材质 alpha blending——独立队列 + 线性 over 混合 + 距离排序
透明来源:glTF alphaMode=BLEND(解析期读取,OBJ/FBX 恒不透明——
资产格式定义写死);MASK 后续演进。Material 加透明语义(入缓存键),
程序化测试资产 CreateTransparentPlane(RGBA 纯色纹理)。

SceneRenderer:透明材质实例收集进独立 scratch(仅主视图,不投
阴影——alpha 阴影后续演进),batch 按平均距离降序排序(远→近,
batchMats 同步重排),SSBO 扁平化顺序 = 排序后顺序。

ForwardPass 两段绘制:不透明(无混合 + 深度写)→ 透明(Shader
Object 动态状态切 SRC_ALPHA/ONE_MINUS_SRC_ALPHA 线性 over + 深度
只测不写);velocity 附件透明段直写(透明表面 velocity 即自身运动)。
frag alpha = albedo 贴图 a 直通,预乘在 tonemap 段(hard constraint
不变)。已知边界:TAA alpha 时域混合(透明边缘几帧渐显);同 batch
多实例不做实例级排序。
2026-09-25 16:57:27 +08:00
atdunbg d9848b416a feat: LOD 运行时开关(ImGui Features 横排,关 = 实例收集恒选 LOD0) 2026-09-25 16:39:16 +08:00
atdunbg cd4eb94a3a perf: BuildFrame 收集段优化——实体级粗剔除 + 数组化 batch 查找 + 合并矩阵写
定位数据(100 背包 + SunTemple,RTX 5060):GPU 全帧仅 ~3ms
(Forward 1.91 + Shadow 0.82),CPU BuildFrame 收集段 5.94ms 才是
帧率瓶颈。三段细分测量:pre 0.2 / each+cull 5.0 / flatten 0.4。

优化(collect 5.94 → 4.6ms,-23%):
1. Mesh 全局连续槽位(GetSlot);ViewScratch batch 查找 hash →
   数组寻址(slot × kMeshLodCount 槽)
2. Model 级联合包围球(各 mesh bounds 并集):实体在四视图全部
   不可见 → 跳过整个 mesh 循环(多 mesh 模型的粗剔除)
3. worlds/prevWorlds 合并为 stride-2 InstanceMatrices 单次写
   (每实例每视图 2 次随机堆访问 → 1 次,cache miss 减半)

遥测:Renderer::GetCollectMs 暴露收集段耗时。剩余 4.6ms 的进一步
压缩(并行收集 / GPU-driven culling)单独立项。
2026-09-25 08:08:51 +08:00
atdunbg 5bdfb53566 feat: LOD 网格分级——meshopt 解析期简化 + 按距离/级联选级
vendored meshoptimizer 1.2(submodule 环境故障改直接 vendor)。Mesh
多 LOD:各级索引连续拼进同一 index buffer(共享顶点,LodRange 区间
选择),解析期 meshopt_simplify 生成链(LOD0=原始,每级目标减半,
相对误差 1e-2,小网格复用上级);都在 worker 线程,加载零额外卡顿。

SceneRenderer 按 (mesh, LOD) 聚合 batch(lookup key = 指针<<8|lod):
主视图按屏占比近似 radius/dist 选级(阈值 10/20 倍半径);阴影级联
按级联序降级(越远越简)。Draw 按 batch.lod 选索引区间。

A/B 对比(100 背包 10×10,RTX 5060 Laptop,vsync off):
- 纯背包:LOD 开 93-95 fps / 关 90-107 fps —— 无显著差异
- 加 SunTemple:LOD 开 52-56 / 关 51-55 —— 无显著差异
结论:当前场景瓶颈不在顶点数(5060 顶点吞吐富余),LOD 基础设施
保留(几何复杂资产/低端 GPU/更大场景生效),帧率问题转向 Tracy
GPU 时间线定位真实瓶颈。

坑:LodRange 记录"LOD 不足复用最后一级"时 first 不得继续累加
(buffer 无重复数据,累加必越界——VUID-08798 抓的)。
2026-09-24 20:53:19 +08:00
183 changed files with 70926 additions and 637 deletions
+8 -1
View File
@@ -1,4 +1,11 @@
add_executable(HelloVulkan main.cpp) # App 层源文件:main + editor/(docking 编辑器,纯 App 代码)
file(GLOB EDITOR_SOURCES CONFIGURE_DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.cpp
${CMAKE_CURRENT_SOURCE_DIR}/editor/*.h)
add_executable(HelloVulkan main.cpp ${EDITOR_SOURCES})
target_include_directories(HelloVulkan PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/editor)
target_link_libraries(HelloVulkan PRIVATE Engine) target_link_libraries(HelloVulkan PRIVATE Engine)
+202
View File
@@ -0,0 +1,202 @@
//
// Editor.cpp:docking 骨架实现。
//
// 布局方案(首帧 DockBuilder 一次,之后 imgui.ini 接管):
// ┌──────────────────────────────┬───────────────┐
// │ Hierarchy │ │
// ├──────────────────────────────┤ Inspector │
// │ Stats │ (28%) │
// └──────────────────────────────┴───────────────┘
//
#include "Editor.h"
#include "HierarchyPanel.h"
#include "InspectorPanel.h"
#include "ViewportPanel.h"
#include "StatsPanel.h"
#include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API)
#include <GLFW/glfw3.h>
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
Editor::Editor() = default;
Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx)
{
m_Ctx = ctx;
m_Hierarchy = std::make_unique<HierarchyPanel>();
m_Inspector = std::make_unique<InspectorPanel>();
m_Viewport = std::make_unique<ViewportPanel>();
m_Stats = std::make_unique<StatsPanel>();
}
void Editor::Draw()
{
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->Pos);
ImGui::SetNextWindowSize(viewport->Size);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::Begin("##EditorDockHost", nullptr,
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus);
ImGui::PopStyleVar(3);
DrawMenuBar();
DrawDockSpace();
ImGui::End();
// 常规面板窗口(窗口名 = docking 身份)
if (m_ShowHierarchy) m_Hierarchy->Draw(m_Ctx, m_Selected);
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
if (m_ShowViewport) m_Viewport->Draw(m_Ctx, m_VpInput);
if (m_ShowStats) m_Stats->Draw(m_Ctx);
DrawQuitModal();
}
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
// GetImGuiEnabled() 再问这里。
bool Editor::BlockingKeyboard() const
{
const ImGuiIO& io = ImGui::GetIO();
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return false;
}
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
return !m_VpInput.focused;
}
bool Editor::BlockingMouse() const
{
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
if (m_Ctx.renderer &&
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
{
return ImGui::GetIO().WantCaptureMouse;
}
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
return !m_VpInput.hovered;
}
void Editor::DrawMenuBar()
{
if (!ImGui::BeginMenuBar())
{
return;
}
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("Quit", "Alt+F4"))
{
RequestQuit();
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem("Hierarchy", nullptr, &m_ShowHierarchy);
ImGui::MenuItem("Inspector", nullptr, &m_ShowInspector);
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
void Editor::DrawDockSpace()
{
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
// 默认布局:仅当 dock node 不存在时构建一次
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
{
m_LayoutBuilt = true;
ImGui::DockBuilderRemoveNode(dockspaceId);
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
ImGuiID dockLeft = 0, dockRight = 0, dockCenter = 0, dockStats = 0;
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Left, 0.20f,
&dockLeft, &dockCenter);
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Right, 0.28f,
&dockRight, &dockCenter);
ImGui::DockBuilderSplitNode(dockLeft, ImGuiDir_Down, 0.38f,
&dockStats, &dockLeft);
ImGui::DockBuilderDockWindow("Hierarchy", dockLeft);
ImGui::DockBuilderDockWindow("Stats", dockStats);
ImGui::DockBuilderDockWindow("Inspector", dockRight);
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
ImGui::DockBuilderFinish(dockspaceId);
}
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
ImGuiDockNodeFlags_None);
}
void Editor::DrawQuitModal()
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
m_Ctx.app->Close();
}
}
+89
View File
@@ -0,0 +1,89 @@
//
// Editor:docking 编辑器骨架。
//
// 职责:
// - 顶栏菜单(File/View)+ 全屏 DockSpace + 首帧默认布局(DockBuilder)
// - 实体选中状态(entt::entity,Hierarchy 写 / Inspector 读)
// - 退出确认模态框(窗口关闭 → RequestQuit → Yes 时 Close)
//
// 面板窗口名(Hierarchy/Inspector/Viewport/Stats)即 docking 身份,
// imgui.ini 持久化布局;默认布局只在 dock node 为空时构建一次。
//
#ifndef HELLOVULKAN_EDITOR_H
#define HELLOVULKAN_EDITOR_H
#include "EditorContext.h"
#include "ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <entt/entt.hpp>
#include <memory>
class HierarchyPanel;
class InspectorPanel;
class ViewportPanel;
class StatsPanel;
class Editor {
public:
Editor();
~Editor();
Editor(const Editor&) = delete;
Editor& operator=(const Editor&) = delete;
// 注入上下文(OnInit 阶段,指针必须全部有效)
void Init(const EditorContext& ctx);
// 每帧 UI 构建(OnImGuiRender 唯一入口)
void Draw();
// ---- 输入归属判定(Window 层输入捕获谓词;UE/Unity 的
// "viewport 持焦才拥有输入" 语义,ImGui 侧经自身后端
// 链式回调永远正常收输入)----
// 键盘:编辑器形态 = Viewport 持焦且不在文本输入中;游戏形态
// (Fullscreen)只在文本输入中让位(打字不触发快捷键)
bool BlockingKeyboard() const;
// 鼠标:编辑器形态 = 悬停 Viewport 才放行;游戏形态沿用
// WantCaptureMouse(UI 隐藏时自动放行)
bool BlockingMouse() const;
// 请求退出(弹确认框;窗口关闭事件走这里)
void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- 选中状态 ----
void SelectEntity(entt::entity e) { m_Selected = e; }
entt::entity GetSelected() const { return m_Selected; }
private:
void DrawMenuBar(); // host 窗口内顶栏
void DrawDockSpace(); // 全屏 host + dockspace + 默认布局
void DrawQuitModal(); // 退出确认模态框
EditorContext m_Ctx{};
ViewportInputState m_VpInput{}; // Viewport 面板 hover/focus(每帧刷新)
std::unique_ptr<HierarchyPanel> m_Hierarchy;
std::unique_ptr<InspectorPanel> m_Inspector;
std::unique_ptr<ViewportPanel> m_Viewport;
std::unique_ptr<StatsPanel> m_Stats;
entt::entity m_Selected{entt::null};
// 面板显隐(View 菜单)
bool m_ShowHierarchy = true;
bool m_ShowInspector = true;
bool m_ShowViewport = true;
bool m_ShowStats = true;
// 默认布局只建一次(imgui.ini 已有布局则跳过)
bool m_LayoutBuilt = false;
// 退出确认链
bool m_ShowQuitConfirm = false;
bool m_QuitConfirmed = false;
};
#endif //HELLOVULKAN_EDITOR_H
+48
View File
@@ -0,0 +1,48 @@
//
// EditorContext:Editor 层与 SandboxApp 之间的桥接上下文。
//
// Editor 是 App 层代码(不属于引擎),它需要的引擎门面与沙盒能力
// 全部经此聚合注入——面板只认识 EditorContext,不反向依赖 main.cpp。
//
#ifndef HELLOVULKAN_EDITORCONTEXT_H
#define HELLOVULKAN_EDITORCONTEXT_H
#include <Engine/Core/Application.h>
#include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <functional>
// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的
// 加载逻辑共用这一张表(下标即身份)。
inline constexpr const char *kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
inline constexpr const char *kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
inline constexpr int kEnvCount = 4;
inline constexpr int kDefaultEnv = 0;
// 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示)
struct EditorContext {
// 引擎门面
Engine::Renderer *renderer = nullptr;
Engine::ECS::Scene *scene = nullptr;
Engine::Assets::AssetManager *assets = nullptr;
Engine::Application *app = nullptr;
std::function<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex;
// 冻结动画时钟(沙盒 spinners 用;保留原 Scene Debug 行为)
bool *pauseAnim = nullptr;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+86
View File
@@ -0,0 +1,86 @@
//
// HierarchyPanel.cpp:实体列表实现。
//
#include "HierarchyPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle
#include <imgui.h>
using namespace Engine;
namespace {
struct ScopedID {
explicit ScopedID(const u64 uuid) {
ImGui::PushID(reinterpret_cast<const void*>(uuid));
}
~ScopedID() {ImGui::PopID(); }
};
}
void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{
ImGui::Begin("Hierarchy");
// 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)
if (ImGui::Button("+ Create Entity"))
{
const auto e = ctx.scene->CreateEntity("Entity");
selected = e;
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent& name)
{
const bool isSelected = (selected == e);
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
const ScopedID scope (rowId);
const std::string label = name.name.empty() ? "(unnamed)" : name.name;
if (ImGui::Selectable(label.c_str(), isSelected))
{
selected = e;
}
// 右键菜单:删除(延迟销毁,帧尾统一 flush)
if (ImGui::BeginPopupContextItem("entity_ctx"))
{
if (ImGui::MenuItem("Delete"))
{
ctx.scene->DestroyEntity(e);
if (selected == e)
{
selected = entt::null;
}
}
ImGui::EndPopup();
}
// 组件徽标 + 悬停清单
std::string badge;
if (reg.all_of<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (!badge.empty())
{
ImGui::SameLine();
ImGui::TextDisabled("%s", badge.c_str());
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(
"M=Mesh A=Animation C=Camera L=Light");
}
}
});
ImGui::End();
}
+20
View File
@@ -0,0 +1,20 @@
//
// HierarchyPanel:实体列表面板。
//
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。
//
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
#define HELLOVULKAN_HIERARCHYPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class HierarchyPanel {
public:
void Draw(const EditorContext& ctx, entt::entity& selected);
};
#endif //HELLOVULKAN_HIERARCHYPANEL_H
+220
View File
@@ -0,0 +1,220 @@
//
// InspectorPanel.cpp:组件编辑实现。
//
// Transform 编辑必须走 Set* 入口(标脏 → 帧循环统一 Compose 重算
// world),直接改私有成员是编译错误——面板天然遵守引擎约束。
//
#include "InspectorPanel.h"
#include "Engine/ECS/Components.h"
#include "Engine/Renderer/Model.h"
#include <imgui.h>
#include <cstdio>
#include <vector>
namespace
{
// InputText 需要 char 缓冲;提交时写回 std::string
bool InputTextString(const char* label, std::string& str)
{
char buf[256];
std::snprintf(buf, sizeof(buf), "%s", str.c_str());
if (ImGui::InputText(label, buf, sizeof(buf)))
{
str = buf;
return true;
}
return false;
}
}
void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{
ImGui::Begin("Inspector");
auto& reg = ctx.scene->GetRegistry();
if (selected == entt::null || !reg.valid(selected))
{
ImGui::TextDisabled("(no entity selected)");
ImGui::End();
return;
}
// ---- Name ----
if (auto* name = reg.try_get<Engine::ECS::NameComponent>(selected))
{
InputTextString("Name", name->name);
}
ImGui::Separator();
// ---- Add Component ----
if (ImGui::Button("Add Component"))
{
ImGui::OpenPopup("add_component");
}
if (ImGui::BeginPopup("add_component"))
{
if (!reg.all_of<Engine::ECS::LightComponent>(selected) &&
ImGui::MenuItem("Light"))
{
reg.emplace<Engine::ECS::LightComponent>(selected);
}
if (!reg.all_of<Engine::ECS::CameraComponent>(selected) &&
ImGui::MenuItem("Camera"))
{
reg.emplace<Engine::ECS::CameraComponent>(selected);
}
if (!reg.all_of<Engine::ECS::AnimationComponent>(selected) &&
ImGui::MenuItem("Animation"))
{
reg.emplace<Engine::ECS::AnimationComponent>(selected);
}
if (!reg.all_of<Engine::ECS::NameComponent>(selected) &&
ImGui::MenuItem("Name"))
{
reg.emplace<Engine::ECS::NameComponent>(selected);
}
ImGui::EndPopup();
}
ImGui::Separator();
// ---- Transform ----
if (auto* t = reg.try_get<Engine::ECS::TransformComponent>(selected))
{
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen))
{
glm::vec3 pos = t->GetPosition();
if (ImGui::DragFloat3("position", &pos.x, 0.05f))
{
t->SetPosition(pos);
}
glm::vec3 rot = t->GetRotation(); // 欧拉角(度)
if (ImGui::DragFloat3("rotation", &rot.x, 0.5f))
{
t->SetRotation(rot);
}
glm::vec3 scl = t->GetScale();
if (ImGui::DragFloat3("scale", &scl.x, 0.02f, 0.001f, 100.0f))
{
t->SetScale(scl);
}
}
}
// ---- MeshRenderer ----
if (auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected))
{
if (ImGui::CollapsingHeader("MeshRenderer",
ImGuiTreeNodeFlags_DefaultOpen))
{
if (const Engine::Model* model = ctx.assets->GetModel(mr->model);
model != nullptr)
{
const auto& clips = model->GetClips();
ImGui::Text("model: valid (%u clips, %u bones)",
static_cast<unsigned>(clips.size()),
static_cast<unsigned>(model->GetBoneCount()));
ImGui::TextDisabled("(mesh asset is read-only here)");
}
else
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"model: INVALID (dangling ref)");
}
}
}
// ---- Light ----
if (auto* light = reg.try_get<Engine::ECS::LightComponent>(selected))
{
if (ImGui::CollapsingHeader("Light", ImGuiTreeNodeFlags_DefaultOpen))
{
int type = static_cast<int>(light->type);
if (ImGui::Combo("type", &type, "Directional\0Point\0"))
{
light->type = static_cast<Engine::ECS::LightComponent::Type>(type);
}
ImGui::ColorEdit3("color", &light->color.x);
ImGui::DragFloat("intensity", &light->color.a, 0.05f, 0.0f, 100.0f);
if (light->type == Engine::ECS::LightComponent::Type::Directional)
{
ImGui::SliderFloat3("direction", &light->direction.x, -1.0f, 1.0f);
ImGui::Checkbox("castShadow", &light->castShadow);
}
else
{
ImGui::DragFloat("radius", &light->radius, 0.1f, 0.0f, 100.0f);
ImGui::TextDisabled("(position = entity Transform)");
}
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
if (ImGui::CollapsingHeader("Camera", ImGuiTreeNodeFlags_DefaultOpen))
{
if (ImGui::Checkbox("primary", &cam->primary) && cam->primary)
{
// primary 互斥:启用本相机时清掉其余实体的 primary
//(引擎取第一个 primary 作为渲染视角)
reg.view<Engine::ECS::CameraComponent>().each(
[&](entt::entity e, Engine::ECS::CameraComponent& c)
{
if (e != selected)
{
c.primary = false;
}
});
}
}
}
// ---- Animation ----
if (auto* anim = reg.try_get<Engine::ECS::AnimationComponent>(selected))
{
if (ImGui::CollapsingHeader("Animation",
ImGuiTreeNodeFlags_DefaultOpen))
{
const auto* mr = reg.try_get<Engine::ECS::MeshRendererComponent>(selected);
const Engine::Model* model =
mr ? ctx.assets->GetModel(mr->model) : nullptr;
if (model != nullptr && model->HasAnimations())
{
const auto& clips = model->GetClips();
std::vector<const char*> clipNames;
clipNames.reserve(clips.size());
for (const auto& c : clips)
{
clipNames.push_back(c.name.c_str());
}
int cur = anim->clipIndex;
if (ImGui::Combo("clip", &cur, clipNames.data(),
static_cast<int>(clipNames.size())))
{
anim->Play(cur); // 触发 crossfade(fade 秒)
}
ImGui::Checkbox("playing", &anim->playing);
ImGui::SameLine();
ImGui::Checkbox("loop", &anim->loop);
ImGui::SliderFloat("speed", &anim->speed, -2.0f, 2.0f);
ImGui::SliderFloat("fade", &anim->fadeDuration, 0.0f, 1.0f);
if (anim->clipIndex >= 0 &&
anim->clipIndex < static_cast<int>(clips.size()))
{
ImGui::Text("t: %.2f / %.2f s", anim->time,
clips[anim->clipIndex].duration);
}
}
else
{
ImGui::TextDisabled("(no animation clips on model)");
}
}
}
ImGui::End();
}
+22
View File
@@ -0,0 +1,22 @@
//
// InspectorPanel:选中实体的组件检查/编辑面板。
//
// 泛化编辑:任何实体(不是沙盒特例)的 Name / Transform /
// MeshRenderer / Light / Camera / Animation 全部在此编辑。
// 沙盒原 Scene Debug 里的 KeyLight、动画 clip、gun scale 控件
// 全部由这里的通用控件替代。
//
#ifndef HELLOVULKAN_INSPECTORPANEL_H
#define HELLOVULKAN_INSPECTORPANEL_H
#include "EditorContext.h"
#include <entt/entt.hpp>
class InspectorPanel {
public:
void Draw(const EditorContext& ctx, entt::entity selected);
};
#endif //HELLOVULKAN_INSPECTORPANEL_H
+173
View File
@@ -0,0 +1,173 @@
//
// StatsPanel.cpp:遥测与全局设置实现(原 main.cpp Scene Debug 搬迁)。
//
#include "StatsPanel.h"
#include "Engine/ECS/Components.h"
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
using namespace Engine; // f32(Engine/Core/Types.h)
void StatsPanel::Draw(const EditorContext& ctx)
{
ImGui::Begin("Stats");
auto& renderer = *ctx.renderer;
auto& app = *ctx.app;
// ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)----
{
Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr)
{
const glm::vec3& pos = camT->GetPosition();
ImGui::Text("Camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
}
else
{
ImGui::TextDisabled("(no primary camera)");
}
ImGui::Separator();
}
// ---- Scene:动画时钟冻结(沙盒 spinners 用)----
if (ctx.pauseAnim != nullptr)
{
ImGui::Checkbox("Pause Animation", ctx.pauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
ImGui::SameLine();
bool lod = renderer.GetLodEnabled();
if (ImGui::Checkbox("LOD", &lod))
{
renderer.SetLodEnabled(lod); // 关 = 恒绘原网格
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么")----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &drawSkybox);
if (drawSkybox && ctx.getEnvIndex)
{
int envIdx = ctx.getEnvIndex();
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
ctx.setEnvIndex(envIdx);
const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]);
ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env));
}
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}
else
{
ImGui::ColorEdit4("ClearColor",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度)----
if (ImGui::CollapsingHeader("GPU Timings"))
{
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated())
{
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / app.GetDeltaTime(),
static_cast<unsigned long long>(app.GetFrameCount()));
}
ImGui::End();
}
+19
View File
@@ -0,0 +1,19 @@
//
// StatsPanel:遥测与全局设置面板(原 Scene Debug 拆入)。
//
// Camera 位姿 / Rendering 开关 / Environment(IBL+DebugView+ClearColor) /
// GPU Timings / Frame(VSync、fps)。KeyLight、动画 clip、gun scale
// 等沙盒特例编辑已泛化进 Inspector,此处不再重复。
//
#ifndef HELLOVULKAN_STATSPANEL_H
#define HELLOVULKAN_STATSPANEL_H
#include "EditorContext.h"
class StatsPanel {
public:
void Draw(const EditorContext& ctx);
};
#endif //HELLOVULKAN_STATSPANEL_H
+53
View File
@@ -0,0 +1,53 @@
//
// ViewportPanel.cpp:显示模式单选 + ImGui::Image + 分辨率上报。
// 逻辑照搬原 main.cpp 的 Viewport 窗口(行为不变)。
//
#include "ViewportPanel.h"
#include <imgui.h>
#include <vulkan/vulkan.h> // vk::Extent2D 经 Renderer 头已带入,显式无妨
using namespace Engine; // u32 / f32(Engine/Core/Types.h)
void ViewportPanel::Draw(const EditorContext &ctx, ViewportInputState &out) {
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
{
ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse);
// 输入焦点上报(悬停计入遮挡:模态框盖住时不误报)
out.hovered = ImGui::IsWindowHovered();
out.focused = ImGui::IsWindowFocused();
auto &renderer = *ctx.renderer;
const int mode = renderer.GetDisplayMode();
const void *sceneTex = renderer.GetSceneTexture();
if (sceneTex != nullptr && mode == kDisplayViewport) {
// 面板可用区域(物理像素)上报为渲染分辨率 → 画面精确填满
// 面板;下一帧帧首 BeginFrameSync 生效(一帧延迟,不可见)
const ImVec2 avail = ImGui::GetContentRegionAvail();
const ImVec2 fb = ImGui::GetIO().DisplayFramebufferScale;
const u32 wantW = static_cast<u32>(avail.x * fb.x + 0.5f);
const u32 wantH = static_cast<u32>(avail.y * fb.y + 0.5f);
if (wantW >= 16 && wantH >= 16) {
renderer.SetViewportSize(wantW, wantH);
}
// 纹理 = 渲染分辨率:等比适配面板可用区域
const vk::Extent2D ext = renderer.GetRenderExtent();
const f32 aspect =
ext.height != 0 ? f32(ext.width) / f32(ext.height) : 1.0f;
ImVec2 size = avail;
if (avail.x / avail.y > aspect) size.x = avail.y * aspect;
else size.y = avail.x / aspect;
ImGui::Image((ImTextureID) (intptr_t) sceneTex, size);
} else {
ImGui::TextDisabled("(scene texture not ready)");
}
ImGui::End();
}
ImGui::PopStyleVar();
}
+26
View File
@@ -0,0 +1,26 @@
//
// ViewportPanel:场景渲染输出面板。
//
// tonemap 输出经 ImGui::Image 显示(Editor 模式),并提供
// Editor Viewport / Fullscreen 显示模式切换。面板可用区域
// (物理像素)上报为渲染分辨率 → 画面精确填满面板。
//
#ifndef HELLOVULKAN_VIEWPORTPANEL_H
#define HELLOVULKAN_VIEWPORTPANEL_H
#include "EditorContext.h"
// Viewport 面板每帧上报的输入焦点状态(Hazel 模式:编辑器据此决定
// 游戏侧输入是否放行——悬停/持焦 Viewport 才拥有 WASD/鼠标)
struct ViewportInputState {
bool hovered = false; // 鼠标悬停在 Viewport 面板上
bool focused = false; // Viewport 面板持有键盘焦点
};
class ViewportPanel {
public:
void Draw(const EditorContext& ctx, ViewportInputState& out);
};
#endif //HELLOVULKAN_VIEWPORTPANEL_H
+108 -326
View File
@@ -3,12 +3,18 @@
// //
// SandboxApp:第一个基于 Application 框架的程序。 // SandboxApp:第一个基于 Application 框架的程序。
// //
// main 的全部职责:定义一个 Application 派生类并运行它。 // main 的职责边界:
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理; // - 定义 Application 派生类并运行它
// 这里只写"程序逻辑":创建实体、逐帧更新组件、事件响应。 // - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、
// 异步环境加载
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件)
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
// //
#include <Engine/Core/Application.h> #include <Engine/Core/Application.h>
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h> #include <Engine/Core/Stats.h>
#include <Engine/Core/AsyncTask.h> #include <Engine/Core/AsyncTask.h>
@@ -17,37 +23,20 @@
#include <Engine/ECS/Components.h> #include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h> #include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/IblGenerator.h> #include <Engine/Renderer/Model.h>
#include <imgui.h>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <GLFW/glfw3.h> #include <GLFW/glfw3.h>
#include <atomic> #include "Editor.h"
#include <thread> #include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
using namespace Engine; using namespace Engine;
using namespace Engine::ECS; using namespace Engine::ECS;
namespace namespace
{ {
// 可切换环境(IBL 动态切换演示):路径 → AssetManager 缓存去重,
// 重复选择同一环境零成本;首选路径触发一次 compute 预积分
constexpr const char* kEnvPaths[] = {
"assets/textures/venice_dawn_1_4k.hdr",
"assets/textures/rooitou_park_4k.hdr",
"assets/textures/pink_sunrise_4k.hdr",
"assets/textures/birchwood_4k.hdr",
};
constexpr const char* kEnvNames[] = {
"Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood",
};
constexpr int kEnvCount = 4;
constexpr int kDefaultEnv = 0;
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx"; constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
class SandboxApp final : public Application class SandboxApp final : public Application
@@ -60,11 +49,6 @@ namespace
~SandboxApp() override ~SandboxApp() override
{ {
// worker 只做 CPU 解码(不碰 Vulkan),在任何 GPU 资源
// 析构之前 join 即安全;派生析构先于基类成员析构执行
if (m_EnvWorker.joinable()) {
m_EnvWorker.join();
}
} }
protected: protected:
@@ -73,30 +57,56 @@ namespace
void OnInit() override void OnInit() override
{ {
Scene& scene = GetScene(); Scene& scene = GetScene();
Renderer& renderer = GetRenderer();
renderer.SetDisplayMode(kDisplayViewport);
// ImGui 叠加层(框架只做管线桥接,UI 内容全部在这里定义)
// ImGui 叠加层(框架只做管线桥接;UI 内容 = docking Editor)
EnableImGui(); EnableImGui();
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级), // Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
// 其余环境在 ImGui 里按需 lazy 加载后切换 InitEditor();
const auto env = GetAssets().LoadEnvironment(
kEnvPaths[kDefaultEnv]); SetEnv();
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env)); const auto suntemple = GetAssets().LoadModel("assets/models/demo/asset.fbx");
auto entityhandle = scene.CreateEntity("SunTemple");
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
scene.CreateEntity();
scene.CreateEntity();
scene.CreateEntity();
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
} }
/*
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
int countSize = 0;
if (countSize) {
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产, // 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5) // 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj"); const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf"); // const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
// 大模型异步加载:assimp 解析在 worker 线程(SunTemple 约 3s),
// 完成后 OnUpdate 收割 → GPU 构建 → 建实体(cm 尺度 → 0.001)
m_TempleTask.Run([] { return Model::ParseFromDisk(kTemplePath); });
m_TempleScale = 0.001f;
int countSize = 0;
for (int i = 0; i < countSize; i ++) for (int i = 0; i < countSize; i ++)
{ {
for (int j = 0; j < countSize; j ++) for (int j = 0; j < countSize; j ++)
@@ -107,21 +117,23 @@ namespace
m_Spinners.push_back(m); m_Spinners.push_back(m);
} }
} }
}
// 地面:程序化纯色平面(半边长 15,中心 y=-2) // 地面:程序化纯色平面(半边长 15,中心 y=-2)
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145); const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground"); m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>() m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground}); m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
*/
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform) // 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera"); m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>(); auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true; camera.primary = true;
m_Camera.GetComponent<TransformComponent>() m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 0.0f, 7.0f)); .SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
m_Camera.GetComponent<TransformComponent>()
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
// 灯光实体(第一个方向光 = 阴影光源) // 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight"); m_KeyLight = scene.CreateEntity("KeyLight");
@@ -133,6 +145,7 @@ namespace
.castShadow = true, .castShadow = true,
}); });
/*
scene.CreateEntity("FillLight") scene.CreateEntity("FillLight")
.AddComponent<LightComponent>( .AddComponent<LightComponent>(
LightComponent{ LightComponent{
@@ -140,7 +153,9 @@ namespace
.direction = glm::vec3(0.5f, -0.5f, 0.3f), .direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f) .color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
}); });
*/
/*
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减) // 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight"); auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f)); fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
@@ -159,28 +174,11 @@ namespace
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝 .color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f, .radius = 10.0f,
}); });
*/
} }
void OnUpdate(f32 deltaTime) override void OnUpdate(f32 deltaTime) override
{ {
// 异步模型收割:解析完成 → GPU 构建 → 建实体
if (m_TempleTask.Done()) {
ParsedModel parsed = m_TempleTask.Take();
const auto ref = GetAssets().LoadModel(kTemplePath,
std::move(parsed));
if (ref.Valid()) {
auto temple = GetScene().CreateEntity("SunTemple");
temple.AddComponent<MeshRendererComponent>(
MeshRendererComponent{ref});
temple.GetComponent<TransformComponent>()
.SetScale(glm::vec3{m_TempleScale});
ENGINE_INFO("[ASYNC] SunTemple ready, entity created");
}
}
// 异步环境加载:每帧收割完成的解码结果
PollEnvironment();
// 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff // 暂停时冻结动画时钟(帧历史验证:暂停后 Temporal Diff
// 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到) // 视图应在 2 帧内整屏变黑;相机移动同样会被 diff 捕捉到)
if (!m_PauseAnim) if (!m_PauseAnim)
@@ -202,262 +200,20 @@ namespace
} }
} }
// ---- UI(全部委托 docking Editor;本文件零 ImGui 控件)----
void OnImGuiRender() override
{
m_Editor->Draw();
}
// ---- 事件 ---- // ---- 事件 ----
// ---- 异步环境加载(decode 后台线程 / GPU 生成主线程)----
// decode 占切换耗时大头(4k ≈ 130ms);worker 只产出纯 CPU
// 像素数据,Vulkan 生成回主线程(设备非线程安全)。
// 同一时刻最多一个 in-flight,忙碌期的新请求记为 pending 接力。
void RequestEnvironment(int idx)
{
if (m_EnvLoadBusy)
{
m_EnvPending = idx;
return;
}
StartEnvDecode(idx);
}
void StartEnvDecode(int idx)
{
m_EnvLoadBusy = true;
m_EnvLoadDone.store(false);
m_EnvLoadingPath = kEnvPaths[idx];
m_EnvWorker = std::thread([this, idx]() {
m_EnvDecoded = IblGenerator::DecodeHdr(kEnvPaths[idx]);
m_EnvLoadDone.store(true); // release:解码结果对主线程可见
});
}
void PollEnvironment()
{
if (!m_EnvLoadBusy || !m_EnvLoadDone.load())
{
return;
}
m_EnvWorker.join(); // join = 同步点,解码结果此后归主线程
m_EnvLoadBusy = false;
if (m_EnvDecoded.Valid())
{
const auto ref = GetAssets().LoadEnvironment(
m_EnvLoadingPath, std::move(m_EnvDecoded));
if (ref.Valid())
{
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(ref));
}
}
m_EnvDecoded = {};
// 忙碌期间的最新请求接力(rapid switch 只保留最终意图)
if (m_EnvPending >= 0)
{
const int next = m_EnvPending;
m_EnvPending = -1;
StartEnvDecode(next);
}
}
// ---- 叠加层 UI(唯一允许出现 ImGui 控件的地方)----
void OnRenderOverlay() override
{
ImGui::Begin("Scene Debug");
auto& renderer = GetRenderer();
// ---- Scene:场景内容(灯/动画) ----
if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen))
{
// 阴影光源(无开关,控件平铺聚拢)
if (auto* light = m_KeyLight.TryGetComponent<LightComponent>())
{
ImGui::Text("Key Light (shadow caster)");
ImGui::SliderFloat3("direction",
&light->direction.x, -1.0f, 1.0f);
ImGui::SliderFloat("intensity", &light->color.a, 0.0f, 20.0f);
if (ImGui::Button("reset"))
{
light->direction = glm::vec3(-0.5f, -0.6f, -0.35f);
light->color.a = 2.0f;
}
}
ImGui::Checkbox("Pause Animation", &m_PauseAnim);
}
// ---- Rendering:渲染管线参数与开关 ----
if (ImGui::CollapsingHeader("Rendering", ImGuiTreeNodeFlags_DefaultOpen))
{
// Tonemap(无开关,平铺)
f32 exposure = renderer.GetExposure();
if (ImGui::SliderFloat("exposure", &exposure, 0.1f, 4.0f))
{
renderer.SetExposure(exposure);
}
bool aces = renderer.GetUseACES();
if (ImGui::Checkbox("ACES", &aces))
{
renderer.SetUseACES(aces);
}
// Bloom:开关即显隐——勾上才显示参数
bool bloom = renderer.GetBloomEnabled();
if (ImGui::Checkbox("Bloom", &bloom))
{
renderer.SetBloomEnabled(bloom);
}
if (bloom)
{
f32 threshold = renderer.GetBloomThreshold();
if (ImGui::SliderFloat("threshold", &threshold, 0.0f, 2.0f))
{
renderer.SetBloomThreshold(threshold);
}
f32 strength = renderer.GetBloomStrength();
if (ImGui::SliderFloat("strength", &strength, 0.0f, 2.0f))
{
renderer.SetBloomStrength(strength);
}
}
// 无参数的纯开关:横排平铺
bool shadows = renderer.GetShadowsEnabled();
if (ImGui::Checkbox("Shadows", &shadows))
{
renderer.SetShadowsEnabled(shadows);
}
ImGui::SameLine();
bool taa = renderer.GetTAAEnabled();
if (ImGui::Checkbox("TAA", &taa))
{
renderer.SetTAAEnabled(taa);
}
}
// ---- Environment:IBL 环境 + 调试视图(决定"看什么") ----
if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen))
{
// 动态环境切换(Assets 层加载 → Renderer 换绑;缓存去重)
ImGui::Text("Environment (IBL)");
bool& DrawSkybox = GetScene().GetSettings().DrawSkybox;
ImGui::Checkbox("DrawSkybox", &DrawSkybox);
if (DrawSkybox)
{
int envIdx = m_EnvIndex;
if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount))
{
// 异步:解码后台线程,完成后 OnUpdate 里换绑
m_EnvIndex = envIdx;
RequestEnvironment(envIdx);
}
ImGui::Text(m_EnvLoadBusy ? "(decoding...)" :
"(switch = compute pre-integrate, ms-level)");
// 调试视图:单看某个 pass 的中间输出(类 UE pass debug)
const auto& viewNames = renderer.GetDebugViewNames();
int view = renderer.GetDebugView();
if (ImGui::Combo("Debug View", &view, viewNames.data(),
static_cast<int>(viewNames.size())))
{
renderer.SetDebugView(view);
}
}else
{
ImGui::ColorEdit4("ClearColor", glm::value_ptr(GetScene().GetSettings().ClearColor));
}
}
// ---- GPU Timings:只读遥测(默认折叠,不占面板高度) ----
if (ImGui::CollapsingHeader("GPU Timings"))
{
// Pass 级 GPU 耗时(RG timestamp)
ImGui::Text("instances: %u main / %u shadow",
renderer.GetMainInstanceCount(),
renderer.GetShadowInstanceCount());
if (renderer.GetInstancesTruncated()) {
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f),
"TRUNCATED: instance buffer full!");
}
const auto& names = renderer.GetPassNames();
const auto& times = renderer.GetPassGpuMs();
for (size_t i = 0; i < names.size() && i < times.size(); ++i)
{
ImGui::Text(" %-12s %.3f ms", names[i].c_str(), times[i]);
}
}
// ---- Frame:帧率控制与显示 ----
if (ImGui::CollapsingHeader("Frame", ImGuiTreeNodeFlags_DefaultOpen))
{
auto& app = *this;
bool vsync = app.GetVSync();
if (ImGui::Checkbox("VSync", &vsync))
{
app.SetVSync(vsync);
if (!vsync)
{
const f32 target = app.GetTargetFps();
app.SetTargetFps(target);
}
}
if (!vsync)
{
f32 target = app.GetTargetFps();
if (ImGui::SliderFloat("target fps", &target, 24.0f, 360.0f, "%.0f"))
{
app.SetTargetFps(target);
}
}
ImGui::Text("fps: %.1f frame: %llu",
1.0f / GetDeltaTime(),
static_cast<unsigned long long>(GetFrameCount()));
}
ImGui::End();
// 自定义重载事件: 重载关闭事件模态框
{
if (m_ShowQuitConfirm)
{
ImGui::OpenPopup("Should quit?");
m_ShowQuitConfirm = false;
}
ImGui::SetNextWindowPos(
ImGui::GetMainViewport()->GetCenter(),
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal("Should quit?", nullptr,
ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::TextUnformatted("Quit the application?");
ImGui::Separator();
if (ImGui::Button("Yes", ImVec2(120, 0)))
{
m_QuitConfirmed = true;
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("No", ImVec2(120, 0)))
{
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
if (m_QuitConfirmed)
{
m_QuitConfirmed = false;
Close();
}
}
}
void OnWindowClose(const Event& event) override void OnWindowClose(const Event& event) override
{ {
// 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI)
SetImGuiEnabled(true); SetImGuiEnabled(true);
m_ShowQuitConfirm = true; m_Editor->RequestQuit();
} }
void OnKeyPress(const Event& event) override void OnKeyPress(const Event& event) override
@@ -478,25 +234,51 @@ namespace
ENGINE_INFO("App: window resized to {}x{}", event.width, event.height); ENGINE_INFO("App: window resized to {}x{}", event.width, event.height);
} }
void SetEnv(int index = 0) {
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 Stats 面板里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(kEnvPaths[index]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
}
}
private: private:
bool m_ShowQuitConfirm = false; // Editor 注入:门面指针 + 反向能力回调(面板 → 沙盒)
bool m_QuitConfirmed = false; void InitEditor()
{
EditorContext ctx;
ctx.renderer = &GetRenderer();
ctx.scene = &GetScene();
ctx.assets = &GetAssets();
ctx.app = this;
ctx.getEnvIndex = [this] { return m_EnvIndex; };
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
ctx.pauseAnim = &m_PauseAnim;
m_Editor->Init(ctx);
// 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词
//(全屏 dock host 让它恒真,输入全死)。改为 Editor 按帧
// 判定——Viewport hover/focus 拥有输入(Hazel/UE 语义);
// UI 隐藏(反引号)时 Draw 不再执行,直接全放行
GetWindow().SetInputCapture(
[this] { return GetImGuiEnabled() && m_Editor->BlockingKeyboard(); },
[this] { return GetImGuiEnabled() && m_Editor->BlockingMouse(); });
}
private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_Ground{};
EntityHandle m_KeyLight{}; EntityHandle m_KeyLight{};
EntityHandle m_Ground{};
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
std::vector<EntityHandle> m_Spinners; std::vector<EntityHandle> m_Spinners;
f32 m_Time = 0.0f; f32 m_Time = 0.0f;
bool m_PauseAnim = true; // 冻结动画时钟(帧历史 diff 验证用) bool m_PauseAnim = true; // 冻结动画时钟(Editor 面板可开关)
int m_EnvIndex = kDefaultEnv; int m_EnvIndex = kDefaultEnv;
// ---- 异步环境加载状态(单 in-flight + pending 接力)----
std::thread m_EnvWorker;
std::string m_EnvLoadingPath;
DecodedEquirect m_EnvDecoded; // worker 写 → join 后主线程读
std::atomic<bool> m_EnvLoadDone{false};
bool m_EnvLoadBusy = false;
int m_EnvPending = -1;
// ---- 异步模型加载(SunTemple)---- // ---- 异步模型加载(SunTemple)----
AsyncTask<ParsedModel> m_TempleTask; AsyncTask<ParsedModel> m_TempleTask;
f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用) f32 m_TempleScale = 1.0f; // cm 尺度资产 → 米(解析后应用)
+6
View File
@@ -47,4 +47,10 @@ target_compile_definitions(Engine PUBLIC TRACY_ON_DEMAND)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
target_link_libraries(Engine PUBLIC Threads::Threads) target_link_libraries(Engine PUBLIC Threads::Threads)
# meshoptimizer(vendored,LOD 网格简化;submodule 机制故障改为直接 vendor)
file(GLOB MESHOPT_SOURCES ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src/*.cpp)
add_library(meshopt STATIC ${MESHOPT_SOURCES})
target_include_directories(meshopt PUBLIC ${CMAKE_SOURCE_DIR}/Lib/meshoptimizer/src)
target_link_libraries(Engine PUBLIC meshopt)
target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1) target_compile_definitions(Engine PRIVATE VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1)
@@ -157,6 +157,9 @@ namespace Engine
// 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。 // 程序化平面(纯色贴图):size 为半边长。带缓存,同参数共享一份。
ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b); ModelRef CreatePlane(f32 halfSize, u8 r, u8 g, u8 b);
// 半透明平面(alpha 0-255,透明绘制队列):同参数缓存去重
ModelRef CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a);
// 环境资产(HDR → IBL 预积分四件套)。路径缓存去重, // 环境资产(HDR → IBL 预积分四件套)。路径缓存去重,
// 重复 Load 同一路径零成本(动态切换场景直接反复调用) // 重复 Load 同一路径零成本(动态切换场景直接反复调用)
@@ -37,6 +37,10 @@ namespace Engine::Assets {
// 程序化纯色平面(size 为半边长) // 程序化纯色平面(size 为半边长)
virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0; virtual Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) = 0;
// 半透明纯色平面(alpha 0-255,透明绘制队列)
virtual Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) = 0;
// 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr) // 环境 HDR → IBL 预积分四件套(compute 生成,毫秒级;失败返回 nullptr)
virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0; virtual Scope<Cubemap> LoadEnvironment(const std::string& path) = 0;
+1 -1
View File
@@ -88,7 +88,7 @@ namespace Engine {
// 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。 // 主渲染之后、当帧提交之前的叠加层钩子(ImGui/调试视图/编辑器 UI)。
// 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。 // 主渲染由框架固定执行;这里只负责"额外的"渲染,不覆写则无叠加层。
virtual void OnRenderOverlay() {} virtual void OnImGuiRender() {}
// 主循环结束后、析构前(保存、清理场景等) // 主循环结束后、析构前(保存、清理场景等)
virtual void OnShutdown() {} virtual void OnShutdown() {}
+13
View File
@@ -63,8 +63,15 @@ namespace Engine {
// 输入事件系统 // 输入事件系统
bool IsKeyPressed(int key) const; bool IsKeyPressed(int key) const;
// UI 捕获鼠标(ImGui 悬停)时返回 false——面板上的按键归 ImGui
bool IsMouseButtonPressed(int button) const;
void GetMouseDelta(float& dx, float& dy) const; void GetMouseDelta(float& dx, float& dy) const;
// 光标捕获(自由视角):DISABLED 隐藏光标出无界增量,NORMAL 恢复。
// 重复设置同值无操作;切换后鼠标增量参考点重置(防跳变)
void SetCursorCaptured(bool captured);
bool IsCursorCaptured() const { return m_CursorCaptured; }
private: private:
@@ -80,6 +87,12 @@ namespace Engine {
CaptureFn m_KeyboardCapture; CaptureFn m_KeyboardCapture;
CaptureFn m_MouseCapture; CaptureFn m_MouseCapture;
// 光标捕获状态与鼠标增量参考点(GetMouseDelta 为 const,用 mutable)
bool m_CursorCaptured = false;
mutable bool m_FirstMouse = true;
mutable double m_LastMouseX = 0.0;
mutable double m_LastMouseY = 0.0;
// GLFW 静态回调(通过 window user pointer 找回 this) // GLFW 静态回调(通过 window user pointer 找回 this)
static void FramebufferResizeCallback(GLFWwindow* window, int width, int height); static void FramebufferResizeCallback(GLFWwindow* window, int width, int height);
static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods);
+33
View File
@@ -27,6 +27,8 @@ namespace Engine::ECS {
// ECS 内部遍历不用它(用 index+generation 句柄,性能)。 // ECS 内部遍历不用它(用 index+generation 句柄,性能)。
struct IdComponent { struct IdComponent {
UUID uuid; UUID uuid;
operator UUID() const { return uuid; }
}; };
// 实体名(调试/查询用;大规模场景建议演进为哈希 id) // 实体名(调试/查询用;大规模场景建议演进为哈希 id)
@@ -83,6 +85,37 @@ namespace Engine::ECS {
ModelRef model; ModelRef model;
}; };
// 骨骼动画播放状态(纯数据 + 便捷控制)。
// 采样/局部层混合逻辑在 SceneRenderer::BuildFrame(渲染侧唯一消费者);
// ImGui 面板经 Play()/字段直接驱动。无此组件 = 按绑定姿势渲染。
struct AnimationComponent {
int clipIndex = 0; // 当前 clip(Model::GetClips 下标)
bool playing = true;
bool loop = true;
f32 speed = 1.0f;
f32 time = 0.0f; // 当前 clip 播放头(秒,wrap 由消费端处理)
// ---- crossfade(clip 切换平滑过渡)----
// 来源 clip 淡出期间与目标 clip 并行采样,局部 TRS 层混合
//(palette 数量不变,只是求值端多采一组姿态)
int fadeFrom = -1; // 来源 clip 下标(-1 = 无过渡)
f32 fadeFromTime = 0.0f; // 来源 clip 播放头
f32 fadeElapsed = 0.0f; // 已过渡时长
f32 fadeDuration = 0.3f; // 过渡总时长(秒)
// 切换 clip(带 crossfade)。越界/空 clip 保护在消费端。
void Play(int index, f32 fadeSeconds = 0.3f)
{
if (index == clipIndex) return;
fadeFrom = clipIndex;
fadeFromTime = time;
fadeElapsed = 0.0f;
fadeDuration = fadeSeconds;
clipIndex = index;
time = 0.0f;
}
};
// 相机。一个场景中 primary=true 的实体是当前渲染视角。 // 相机。一个场景中 primary=true 的实体是当前渲染视角。
struct CameraComponent { struct CameraComponent {
Camera camera; Camera camera;
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Engine::ECS {
// ---- 实体 ---- // ---- 实体 ----
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy() // 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = {}); EntityHandle CreateEntity(std::string name = "new entity");
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 void DestroyEntity(Entity e); // 延迟:帧尾统一销毁
void FlushDestroyQueue(); // 每帧末尾由 Application 调用 void FlushDestroyQueue(); // 每帧末尾由 Application 调用
+14
View File
@@ -18,6 +18,8 @@
#pragma once #pragma once
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Core/FrameContext.h> // kMaxFramesInFlight
#include <imgui.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
namespace Engine { namespace Engine {
@@ -35,6 +37,17 @@ namespace Engine {
ImGuiLayer(const ImGuiLayer&) = delete; ImGuiLayer(const ImGuiLayer&) = delete;
ImGuiLayer& operator=(const ImGuiLayer&) = delete; ImGuiLayer& operator=(const ImGuiLayer&) = delete;
// ---- 场景显示纹理(RG PresentTarget → ImGui::Image 采样)----
// RG 物理镜像按帧槽位各一份,每槽位各注册一个描述符;
// 重编译(首次/resize)后 view 变化,Renderer 调 Update 重注册。
// 返回值直接传给 ImGui::Image(无场景纹理时为 ImTextureID_Invalid)
ImTextureID GetSceneTexture(u32 slot) const
{
return slot < kMaxFramesInFlight
? m_SceneTextures[slot] : ImTextureID_Invalid;
}
void UpdateSceneTexture(u32 slot, vk::ImageView view);
// ---- 每帧三段式(由 Application::Run 驱动)---- // ---- 每帧三段式(由 Application::Run 驱动)----
// 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间, // 开始 UI 构建(ImGui::NewFrame)。此后到 BuildDrawData 之间,
@@ -57,6 +70,7 @@ namespace Engine {
Device& m_Device; Device& m_Device;
vk::DescriptorPool m_Pool; vk::DescriptorPool m_Pool;
bool m_Enabled = true; bool m_Enabled = true;
ImTextureID m_SceneTextures[kMaxFramesInFlight]{};
}; };
} // namespace Engine } // namespace Engine
@@ -98,6 +98,8 @@ namespace Engine::RG {
// 按句柄取当前帧槽位的物理资源(image/view) // 按句柄取当前帧槽位的物理资源(image/view)
vk::ImageView View(TextureHandle h) const; vk::ImageView View(TextureHandle h) const;
vk::Image Image(TextureHandle h) const; vk::Image Image(TextureHandle h) const;
// 物理尺寸(导入资源 = SetImportedExtent 设定值;创建资源 = 物理镜像)
vk::Extent2D Extent(TextureHandle h) const;
// 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。 // 帧历史资源:上一帧槽位的物理镜像((slot + framesInFlight - 1) % F)。
// 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。 // 采样描述符经 GetPhysicalView(h, 历史槽位) 预绑(同阴影图模式)。
@@ -129,6 +131,7 @@ namespace Engine::RG {
// 导入资源每帧绑定 // 导入资源每帧绑定
vk::Image importedImage{}; vk::Image importedImage{};
vk::ImageView importedView{}; vk::ImageView importedView{};
vk::Extent2D importedExtent{}; // 物理尺寸(swapchain 重建后更新)
// 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体) // 帧槽位状态:物理镜像是否已被使用过(决定 entry barrier 变体)
struct SlotState { struct SlotState {
@@ -207,6 +210,11 @@ namespace Engine::RG {
// 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源 // 每帧 acquire 后调用,把当帧 swapchain image 绑到导入资源
void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view); void SetImportedImage(TextureHandle h, vk::Image image, vk::ImageView view);
// 导入资源的物理尺寸(swapchain 重建后同步更新)。pass 渲染区域
// 跟实际附件尺寸走:backbuffer = 窗口尺寸,与目标尺寸(场景渲染
// 分辨率)解耦——Editor 视口模式下二者不同
void SetImportedExtent(TextureHandle h, vk::Extent2D extent);
// 指定帧槽位的物理镜像 view(Compile 后有效)。 // 指定帧槽位的物理镜像 view(Compile 后有效)。
// 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。 // 用途:把 RG 管理的资源绑定到外部描述符(如阴影图采样)。
// 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。 // 注意:物理镜像按槽位各一份,槽位变化/重编译后必须重新获取。
+4 -3
View File
@@ -22,7 +22,7 @@ namespace Engine {
class Camera { class Camera {
public: public:
// 输入驱动:WASD/Space/Shift 移动 + 鼠标视角(需 SetMouseLookEnabled)。 // 输入驱动:WASD/Space/Shift 移动 + 按住右键鼠标视角。
// 全部写回传入的 transform,有变更才 Compose。 // 全部写回传入的 transform,有变更才 Compose。
void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime); void Update(Window& window, ECS::TransformComponent& transform, f32 deltaTime);
@@ -35,14 +35,15 @@ namespace Engine {
void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; } void SetMoveSpeed(f32 speed) { m_MoveSpeed = speed; }
void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; } void SetMouseSensitivity(f32 sens) { m_MouseSensitivity = sens; }
// 鼠标视角默认关闭(需要先做 cursor capture 再打开) // 鼠标视角总开关(默认开)。交互 = 按住右键自由视角(光标捕获),
// 松开释放光标;右键落在 ImGui 面板上时归 UI 不触发相机
void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; } void SetMouseLookEnabled(bool enabled) { m_MouseLookEnabled = enabled; }
bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; }
private: private:
f32 m_MoveSpeed = 2.5f; f32 m_MoveSpeed = 2.5f;
f32 m_MouseSensitivity = 0.1f; f32 m_MouseSensitivity = 0.1f;
bool m_MouseLookEnabled = false; bool m_MouseLookEnabled = true;
}; };
} // namespace Engine } // namespace Engine
+26 -12
View File
@@ -7,13 +7,19 @@
// 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性) // 0 albedo(BaseColor,sRGB) 1 normal(切线空间法线,线性)
// 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定) // 2 metallicRoughness(线性:G=粗糙度,B=金属度,glTF 约定)
// 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性) // 3 emissive(自发光,sRGB) 4 ao(环境光遮蔽,线性)
// 5 材质参数 UBO(fragment,kParamsBinding) // 5 roughness(独立粗糙度图,R 通道——FBX/OBJ specular 工作流,
// glTF 合图工作流不用此槽)
// 6 材质参数 UBO(fragment,kParamsBinding)
// //
// 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。 // 纹理非拥有(Model 的纹理缓存持有);缺槽由调用方传默认纹理。
// 因子(metallicFactor/roughnessFactor)乘在 MR 贴图上: // 因子(metallicFactor/roughnessFactor)乘在贴图上:
// glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1) // glTF(MR 合图 B=金属度语义)→ 因子取 glTF 声明(默认 1/1),
// OBJ(specular 工作流,独立 roughness 图或无图)→ metallic=0, // factors.z=0 → shader 直接读 MR.g 当粗糙度
// roughness 因子按贴图有无/Ns 换算——金属度强制 0 // FBX(metalness 独立图 + roughness 独立图)→ metal 图落 MR 槽
// (灰度 R=G=B,.b 读金属度恰好正确),roughness 图落 Roughness
// 槽,factors.z=1 → shader 用 Roughness.r 替换 MR.g
// OBJ(specular 工作流)→ metallic=0,独立 roughness 图同 FBX;
// 无图时 roughness 因子按 Ns 换算
// //
#ifndef HELLOVULKAN_MATERIAL_H #ifndef HELLOVULKAN_MATERIAL_H
@@ -33,9 +39,11 @@ namespace Engine {
class Texture; class Texture;
class UniformBuffer; class UniformBuffer;
// 材质因子(乘 MR 贴图;无贴图时即最终值) // 材质因子(乘贴图;无贴图时即最终值)
// x = metallic 因子 y = roughness 因子
// z = 独立粗糙度图开关(1 = 用 Roughness 槽 R 通道替换 MR.g)
struct MaterialParams { struct MaterialParams {
glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f}; // x=metallic y=roughness glm::vec4 factors{0.0f, 0.5f, 0.0f, 0.0f};
}; };
class Material { class Material {
@@ -47,15 +55,17 @@ namespace Engine {
MetallicRoughness = 2, MetallicRoughness = 2,
Emissive = 3, Emissive = 3,
Ao = 4, Ao = 4,
kSlotCount = 5, // 纹理槽数 Roughness = 5, // 独立粗糙度图(R 通道)
kParamsBinding = 5, // 参数 UBO binding kSlotCount = 6, // 纹理槽数
kParamsBinding = 6, // 参数 UBO binding
}; };
Material(Device& device, MemoryAllocator& allocator, Material(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout, vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures, const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params); const MaterialParams& params,
bool transparent = false);
~Material(); ~Material();
Material(const Material&) = delete; Material(const Material&) = delete;
@@ -63,15 +73,19 @@ namespace Engine {
vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; } vk::DescriptorSet GetDescriptorSet() const { return m_DescriptorSet; }
Texture* GetTexture(Slot slot) const { return m_Textures[slot]; } Texture* GetTexture(Slot slot) const { return m_Textures[slot]; }
// 透明队列归属(glTF alphaMode=BLEND;CPU 收集侧用)
bool IsTransparent() const { return m_Transparent; }
// 材质缓存键:同贴图组合+同因子共享一份(Model 层使用) // 材质缓存键:同贴图组合+同因子+同透明语义共享一份(Model 层使用)
static std::string MakeCacheKey(const std::string paths[kSlotCount], static std::string MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params); const MaterialParams& params,
bool transparent);
private: private:
std::array<Texture*, kSlotCount> m_Textures{}; std::array<Texture*, kSlotCount> m_Textures{};
vk::DescriptorSet m_DescriptorSet; vk::DescriptorSet m_DescriptorSet;
Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死) Scope<UniformBuffer> m_ParamsBuffer; // per-slot 参数 ring(创建时写死)
bool m_Transparent = false;
}; };
} // namespace Engine } // namespace Engine
+34 -5
View File
@@ -8,6 +8,7 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Vertex.h> #include <Engine/Renderer/Vertex.h>
#include <Engine/Vulkan/Buffer.h>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <vector> #include <vector>
@@ -18,13 +19,18 @@ namespace Engine
class Buffer; class Buffer;
class Material; class Material;
// LOD 级数:LOD0 = 原始网格,越高越简(解析期 meshopt_simplify 生成,
// 共享顶点数据,只换索引区间)
inline constexpr u32 kMeshLodCount = 3;
class Mesh class Mesh
{ {
public: public:
// material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份) // material 非拥有指针:Model 的材质缓存拥有(同贴图共享一份)
// lods[0] 必须为原始索引(与 vertices 对应),各级简化链由解析期生成
Mesh(Device& device, MemoryAllocator& allocator, Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<std::vector<u32>>& lods,
Material* material); Material* material);
~Mesh(); ~Mesh();
@@ -32,11 +38,27 @@ namespace Engine
Mesh& operator=(const Mesh&) = delete; Mesh& operator=(const Mesh&) = delete;
// instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后, // instanceCount/firstInstance:GPUDriven 按 mesh 分 batch 后,
// 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取 // 同 mesh 的 N 个实体一次 draw 完成,模型矩阵经实例索引取。
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1, u32 firstInstance = 0) const; // lod:索引区间选择(越界取最高级)
void Draw(vk::CommandBuffer cmd, u32 instanceCount = 1,
u32 firstInstance = 0, u32 lod = 0) const;
Material* GetMaterial() const { return m_Material; } Material* GetMaterial() const { return m_Material; }
u32 GetIndexCount() const { return m_IndexCount; } u32 GetLodIndexCount(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].indexCount;
}
// 全局连续槽位(SceneRenderer 数组化 batch 查找用,避免逐实例 hash)
u32 GetSlot() const { return m_Slot; }
// ---- MDI 兼容取数(per-mesh buffer:偏移恒 0,firstIndex 即全局)----
vk::Buffer GetVertexBuffer() const { return m_VertexBuffer->Get(); }
vk::Buffer GetIndexBuffer() const { return m_IndexBuffer->Get(); }
u32 GetVertexOffset() const { return 0; }
u32 GetGlobalFirstIndex(u32 lod) const
{
return m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1].firstIndex;
}
// 局部空间包围球(构造时从顶点算出),视锥剔除用 // 局部空间包围球(构造时从顶点算出),视锥剔除用
struct Bounds { struct Bounds {
@@ -46,10 +68,17 @@ namespace Engine
const Bounds& GetBounds() const { return m_Bounds; } const Bounds& GetBounds() const { return m_Bounds; }
private: private:
// 同一 index buffer 内连续排布的各级索引区间
struct LodRange {
u32 firstIndex = 0;
u32 indexCount = 0;
};
Scope<Buffer> m_VertexBuffer; Scope<Buffer> m_VertexBuffer;
Scope<Buffer> m_IndexBuffer; Scope<Buffer> m_IndexBuffer;
std::array<LodRange, kMeshLodCount> m_Lods{};
Material* m_Material; Material* m_Material;
u32 m_IndexCount = 0; u32 m_Slot = 0;
Bounds m_Bounds; Bounds m_Bounds;
u64 m_VertexBytes = 0; u64 m_VertexBytes = 0;
+104 -7
View File
@@ -10,7 +10,9 @@
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <assimp/matrix4x4.h> // 默认参数需完整类型(烘焙节点变换用)
#include <array> #include <array>
#include <filesystem> #include <filesystem>
#include <string> #include <string>
@@ -34,14 +36,59 @@ namespace Engine
// worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。 // worker 线程产出(ParseFromDisk),主线程消费(FromParsed)。
struct ParsedMesh { struct ParsedMesh {
std::vector<Vertex> vertices; std::vector<Vertex> vertices;
std::vector<u32> indices; // LOD 链([0] = 原始索引,越往后越简):解析期 meshopt_simplify
// 生成(worker 线程),消费端共享顶点只换索引
std::vector<std::vector<u32>> lods;
std::array<std::string, Material::kSlotCount> texPaths; std::array<std::string, Material::kSlotCount> texPaths;
MaterialParams params; MaterialParams params;
bool transparent = false; // glTF alphaMode=BLEND(透明绘制队列)
}; };
// ---- 骨骼动画数据(assimp aiNode/aiAnimation/aiBone 的引擎侧镜像)----
// 关键帧(time 单位 = clip 原生 tick,运行时按 tps 换算秒)
struct PosKey { f32 time; glm::vec3 value; };
struct RotKey { f32 time; glm::quat value; };
struct SclKey { f32 time; glm::vec3 value; };
// 一条节点动画轨道:作用于 node 表中下标为 nodeIdx 的节点;
// 无轨道的分量运行时用节点静止姿态(localTransform)
struct AnimChannel {
u32 nodeIdx = 0;
std::vector<PosKey> positions;
std::vector<RotKey> rotations;
std::vector<SclKey> scales;
};
struct AnimationClip {
std::string name;
f32 duration = 0.0f; // 秒(已按 tps 换算)
f32 tps = 1.0f; // ticks per second(0 按 1,assimp 惯例)
std::vector<AnimChannel> channels;
};
// aiNode 树拍平表:骨骼 global 变换的累乘骨架(解析期 ProcessNode
// 原本丢弃 mTransformation,骨骼层级必须保留)
struct ModelNode {
std::string name;
i32 parent = -1; // 根节点 = -1
glm::mat4 localTransform{1.0f}; // 静止姿态 local TRS(assimp 行主→glm 列主已转置)
};
// 骨骼 = palette 槽位 ↔ 节点 ↔ 逆绑定矩阵
// palette[i] = global(bone.nodeIdx) × invBind
struct ModelBone {
std::string name;
u32 nodeIdx = 0;
glm::mat4 invBind{1.0f}; // aiBone mOffsetMatrix(绑定姿势全局逆)
};
struct ParsedModel { struct ParsedModel {
std::string path; std::string path;
bool isGltf = false; bool isGltf = false;
std::vector<ParsedMesh> meshes; std::vector<ParsedMesh> meshes;
std::vector<ModelNode> nodes;
std::vector<ModelBone> bones;
std::vector<AnimationClip> clips;
bool Valid() const { return !meshes.empty(); } bool Valid() const { return !meshes.empty(); }
}; };
@@ -72,6 +119,12 @@ namespace Engine
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b); f32 halfSize, u8 r, u8 g, u8 b);
// 半透明纯色平面(alpha 0-255,预乘 alpha 链):透明管线验证用
static Scope<Model> CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a);
~Model(); ~Model();
@@ -79,12 +132,23 @@ namespace Engine
Model& operator=(const Model&) = delete; Model& operator=(const Model&) = delete;
const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; } const std::vector<Scope<Mesh>>& GetMeshes() const { return m_Meshes; }
// 全模型联合包围球(各 mesh bounds 的并集,局部空间):
// 实体级粗剔除用——整个模型在视锥外直接跳过全部 mesh 循环
const Mesh::Bounds& GetBounds() const { return m_Bounds; }
// ---- 骨骼动画运行时数据(无动画模型全部为空,零开销)----
bool HasAnimations() const { return !m_Clips.empty(); }
u32 GetBoneCount() const { return static_cast<u32>(m_Bones.size()); }
const std::vector<AnimationClip>& GetClips() const { return m_Clips; }
const std::vector<ModelNode>& GetNodes() const { return m_Nodes; }
const std::vector<ModelBone>& GetBones() const { return m_Bones; }
private: private:
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color); ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent);
// 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填 // 空模型骨架(FromParsed 用):只建默认纹理,mesh 由调用方填
Model(Device& device, MemoryAllocator& allocator, Model(Device& device, MemoryAllocator& allocator,
@@ -92,22 +156,49 @@ namespace Engine
vk::DescriptorSetLayout textureLayout); vk::DescriptorSetLayout textureLayout);
void LoadModel(const std::string& path); void LoadModel(const std::string& path);
// aiNode 树拍平成 node 表(DFS 序,parent 下标回填)
static u32 FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out);
// 每帧动画的 channel 轨道提取(nodeName → node 表下标)
static void ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out);
static void ProcessNode(const aiNode* node, const aiScene* scene, static void ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out, std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir, const std::filesystem::path& modelDir,
bool isGltf); bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal
= aiMatrix4x4());
static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene, static ParsedMesh ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir, const std::filesystem::path& modelDir,
bool isGltf); bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform
= aiMatrix4x4());
// mesh 的 aiBone 权重灌进顶点 + 模型级骨骼表注册去重
static void CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots);
void InitDefaultTextures(); void InitDefaultTextures();
// 聚合各 mesh bounds 为全模型联合包围球(实体级粗剔除)
void RebuildUnionBounds();
Texture* GetOrLoadTexture(const std::string& path, bool srgb); Texture* GetOrLoadTexture(const std::string& path, bool srgb);
// 材质缓存入口:同贴图组合+因子共享同一 Material(一个描述符集), // 材质缓存入口:同贴图组合+同因子+同透明语义共享一份 Material,
// 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。 // 槽位路径为空/文件缺失折到对应默认纹理。路径已做反斜杠归一化。
// paths 下标 = Material::Slot;params = metallic/roughness 因子 // paths 下标 = Material::Slot;params = metallic/roughness 因子;
// slotOverrides = 程序化模型的槽位纹理直供(非空项优先于路径/默认)
Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount], Material* GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params); const MaterialParams& params,
bool transparent = false,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides = {});
Device& m_Device; Device& m_Device;
MemoryAllocator& m_Allocator; MemoryAllocator& m_Allocator;
@@ -122,8 +213,14 @@ namespace Engine
std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures; std::array<Scope<Texture>, Material::kSlotCount> m_DefaultTextures;
std::vector<Scope<Mesh>> m_Meshes; std::vector<Scope<Mesh>> m_Meshes;
Mesh::Bounds m_Bounds; // mesh 建完后聚合(实体级粗剔除)
std::string m_ModelPath; std::string m_ModelPath;
bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV bool m_IsGltf = false; // glTF 家族:MR 合图语义 + 不翻转 UV
// ---- 骨骼动画运行时数据(FromParsed/LoadModel 从解析产物 move)----
std::vector<ModelNode> m_Nodes;
std::vector<ModelBone> m_Bones;
std::vector<AnimationClip> m_Clips;
}; };
} // namespace Engine } // namespace Engine
@@ -0,0 +1,72 @@
//
// Created by atdunbg on 2026/9/29.
//
// BlitPass:显示模式适配器(kDisplayDirect 专用)。
// Direct 模式:vkCmdBlitImage 把 PresentTarget(tonemap 输出)拷进
// backbuffer,ImGui 随后 loadOp=Load 叠加 —— 场景铺满窗口的游戏形态。
// Viewport(Editor)模式:空转,backbuffer 由 ImGui 清屏独占,场景
// 经 ImGui::Image 呈现。
//
// 用 vkCmdBlitImage 而非着色器:同格式全屏拷贝零 shader 成本(声明
// TransferSrc/TransferDst use,layout/barrier 由 RG 推导)。Blit 不在
// rendering scope 内,直接录在 cmd 上,合法。
//
// 注:Blit 只在 Direct 模式运行,此时 PresentTarget 与 backbuffer 同为
// 目标尺寸(1:1),src/dst offset 均取本 pass 渲染区域即可。
//
#ifndef HELLOVULKAN_BLITPASS_H
#define HELLOVULKAN_BLITPASS_H
#include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h>
#include <array>
namespace Engine::Passes {
class BlitPass final : public RenderPass {
public:
BlitPass(FrameSceneData& frame, RG::TextureHandle src,
RG::TextureHandle dst)
: m_Frame(frame), m_Src(src), m_Dst(dst) {}
void Register(RG::RenderGraph& rg) override
{
rg.AddPass("Blit", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.displayMode != kDisplayDirect) return; // Editor 模式空转
vk::ImageBlit region;
region.setSrcSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1})
.setDstSubresource({vk::ImageAspectFlagBits::eColor, 0, 0, 1});
// src/dst 各按自己附件的物理尺寸取 offset(Direct 模式下
// 二者同为窗口尺寸;尺寸解耦时此写法也各自正确)
const vk::Extent2D srcExt = ctx.Extent(m_Src);
const vk::Extent2D dstExt = ctx.Extent(m_Dst);
const std::array<vk::Offset3D, 2> srcSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(srcExt.width),
static_cast<i32>(srcExt.height), 1}};
const std::array<vk::Offset3D, 2> dstSpan = {
vk::Offset3D{0, 0, 0},
vk::Offset3D{static_cast<i32>(dstExt.width),
static_cast<i32>(dstExt.height), 1}};
region.setSrcOffsets(srcSpan).setDstOffsets(dstSpan);
ctx.cmd.blitImage(
ctx.Image(m_Src), vk::ImageLayout::eTransferSrcOptimal,
ctx.Image(m_Dst), vk::ImageLayout::eTransferDstOptimal,
{region}, vk::Filter::eLinear);
})
.Read(m_Src, RG::ResourceUse::TransferSrc)
.Write(m_Dst, RG::ResourceUse::TransferDst);
}
private:
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Src; // PresentTarget(tonemap 输出)
RG::TextureHandle m_Dst; // backbuffer
};
} // namespace Engine::Passes
#endif //HELLOVULKAN_BLITPASS_H
@@ -28,6 +28,12 @@ namespace Engine::Passes {
void Register(RG::RenderGraph& rg) override; void Register(RG::RenderGraph& rg) override;
private: private:
// 两段渲染共享的命令录制(shader/描述符绑定 + 几何/光栅/视口状态)。
// translucent=true 绑定 gl_FragDepth=1.0 的透明 frag 变体;
// 深度写与混合状态由各渲染实例作为初始态设置,见 ForwardPass.cpp
void RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd, bool translucent) const;
SceneGpu& m_Gpu; SceneGpu& m_Gpu;
FrameSceneData& m_Frame; FrameSceneData& m_Frame;
RG::TextureHandle m_HDRColor; RG::TextureHandle m_HDRColor;
@@ -56,6 +56,7 @@ namespace Engine {
kDebugIrradiance, // IBL 漫反射辐照度(32² 低频) kDebugIrradiance, // IBL 漫反射辐照度(32² 低频)
kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级) kDebugPrefilter, // IBL pre-filter(纵向 = mip/roughness 分级)
kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道) kDebugBrdfLut, // BRDF 积分 LUT(RG 双通道)
kDebugTransparent, // 仅透明队列(Skybox/Forward 跳过绘制,看透明段内容)
kDebugViewCount kDebugViewCount
}; };
@@ -11,6 +11,7 @@
#define HELLOVULKAN_IMGUIPASS_H #define HELLOVULKAN_IMGUIPASS_H
#include <Engine/Renderer/Passes/RenderPass.h> #include <Engine/Renderer/Passes/RenderPass.h>
#include <Engine/Renderer/SceneTypes.h> // FrameSceneData(displayMode 帧契约)
namespace Engine { namespace Engine {
@@ -21,15 +22,22 @@ namespace Engine {
class ImGuiPass final : public RenderPass { class ImGuiPass final : public RenderPass {
public: public:
// 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调 // 二级指针:Renderer::SetImGuiLayer 在 Register 之后才可能被调
//(EnableImGui 时机由派生类决定),执行期解引用取最新值 //(EnableImGui 时机由派生类决定),执行期解引用取最新值。
ImGuiPass(ImGuiLayer** layer, RG::TextureHandle backbuffer) // presentTarget:ImGui::Image 采样的场景纹理(tonemap 输出)。
: m_Layer(layer), m_Backbuffer(backbuffer) {} // loadOp 按 displayMode 分支:Direct = Load(blit 已铺场景),
// Viewport = Clear(唯一写入者,清"桌面底色")
ImGuiPass(ImGuiLayer** layer, FrameSceneData& frame,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget)
: m_Layer(layer), m_Frame(frame), m_Backbuffer(backbuffer),
m_PresentTarget(presentTarget) {}
void Register(RG::RenderGraph& rg) override; void Register(RG::RenderGraph& rg) override;
private: private:
ImGuiLayer** m_Layer; ImGuiLayer** m_Layer;
FrameSceneData& m_Frame; // displayMode(帧契约)
RG::TextureHandle m_Backbuffer; RG::TextureHandle m_Backbuffer;
RG::TextureHandle m_PresentTarget;
}; };
} // namespace Passes } // namespace Passes
@@ -25,22 +25,36 @@ namespace Engine {
class ShaderManager; class ShaderManager;
class UniformBuffer; class UniformBuffer;
class Cubemap; class Cubemap;
class Buffer;
class MemoryAllocator;
namespace Passes { namespace Passes {
class SceneGpu { class SceneGpu {
public: public:
SceneGpu(Device& device, DescriptorManager& descriptors, SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders, ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer); UniformBuffer& prevObjectBuffer,
UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer);
~SceneGpu(); ~SceneGpu();
SceneGpu(const SceneGpu&) = delete; SceneGpu(const SceneGpu&) = delete;
SceneGpu& operator=(const SceneGpu&) = delete; SceneGpu& operator=(const SceneGpu&) = delete;
// ---- MDI 间接绘制 ----
// CPU 打包的命令/count 落进本槽位 ring(Renderer 在 fence
// 等待后调用——与 UBO/SSBO 上传同一安全窗口)
void UploadIndirect(u32 frameSlot, const FrameSceneData& frame);
// 一次组绘制:bind 共享 megabuffer + drawIndexedIndirectCount
void DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g);
// ---- set 布局(shader 契约,改绑定必须同步 shaders/)---- // ---- set 布局(shader 契约,改绑定必须同步 shaders/)----
vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer} vk::DescriptorSetLayout uboSetLayout; // Set0 = {UBO, ObjectBuffer, PrevObjectBuffer,
// SkinOffsets, SkinPalettes}
vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO} vk::DescriptorSetLayout textureSetLayout; // Set1 = {材质 4 槽 + 参数 UBO}
vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube} vk::DescriptorSetLayout shadowSetLayout; // Set2 = {级联 0/1/2 阴影图, irradiance cube}
vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO) vk::PipelineLayout forwardLayout; // 3 sets,无 push(GPUDriven 经 SSBO)
@@ -54,7 +68,16 @@ namespace Engine {
//(省 shader 出界分支) //(省 shader 出界分支)
vk::Sampler shadowSampler; vk::Sampler shadowSampler;
// ---- MDI per-slot ring ----
// 布局:[main | cascade0 | cascade1 | cascade2],每区
// kMaxInstances 条命令预算(每实例至多 1 条命令,必不越界)。
// count buffer 同分区(每组一个 u32,下标 = 组首命令下标)
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCmdBuffers;
std::array<Scope<Buffer>, kMaxFramesInFlight> indirectCountBuffers;
vk::ShaderEXT forwardVert{}, forwardFrag{}; vk::ShaderEXT forwardVert{}, forwardFrag{};
// 透明变体:同 simple_shader + gl_FragDepth=1.0(见 frag 注释)
vk::ShaderEXT translucentFrag{};
vk::ShaderEXT shadowVert{}, shadowFrag{}; vk::ShaderEXT shadowVert{}, shadowFrag{};
// RG 重编译后重绑阴影图(物理镜像按槽位各一份) // RG 重编译后重绑阴影图(物理镜像按槽位各一份)
@@ -66,6 +89,7 @@ namespace Engine {
private: private:
Device& m_Device; Device& m_Device;
MemoryAllocator& m_Allocator;
DescriptorManager& m_Descriptors; DescriptorManager& m_Descriptors;
}; };
@@ -1,9 +1,10 @@
// //
// Created by atdunbg on 2026/9/23. // Created by atdunbg on 2026/9/23.
// //
// TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 Backbuffer。 // TonemapPass:HDR(+bloom)→ 显示空间,全屏三角写 PresentTarget。
// 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。 // 显示变换在此显式建模(对齐 UE/Unity:HDR SceneColor → tonemapper → UI)。
// Backbuffer 从此只接收"显示空间"颜色:tonemap 直接写、ImGui 叠加。 // PresentTarget 是 offscreen 可采样纹理:由 ImGui::Image 呈现(输出位置
// 可自由摆放),backbuffer 不再接收场景画面。
// //
#ifndef HELLOVULKAN_TONEMAPPASS_H #ifndef HELLOVULKAN_TONEMAPPASS_H
@@ -28,7 +29,8 @@ namespace Engine {
PostProcessParams& params, PostProcessParams& params,
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA, RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle backbuffer, RG::TextureHandle temporalDiff,
RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr, RG::TextureHandle velocity, RG::TextureHandle rawHdr, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1, RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap1,
RG::TextureHandle shadowMap2); RG::TextureHandle shadowMap2);
@@ -46,7 +48,7 @@ namespace Engine {
RG::TextureHandle m_HDR; RG::TextureHandle m_HDR;
RG::TextureHandle m_A; RG::TextureHandle m_A;
RG::TextureHandle m_TemporalDiff; RG::TextureHandle m_TemporalDiff;
RG::TextureHandle m_Backbuffer; RG::TextureHandle m_Target; // PresentTarget(offscreen,ImGui 采样呈现)
// ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)---- // ---- 调试视图源(binding 3-7,Register 时声明读防止剔除)----
// 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑 // 注:TAAColor 视图直接用 binding0(主输入即 TAA 输出),无需重复绑
RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前) RG::TextureHandle m_RawHDR; // Forward 直出(TAA 前)
+44 -4
View File
@@ -58,7 +58,7 @@ namespace Engine {
Renderer(const Renderer&) = delete; Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete; Renderer& operator=(const Renderer&) = delete;
void Render(ECS::Scene& scene, Assets::AssetManager& assets); void Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt);
// ---- 组合根(Application)装配其他模块时需要的访问器 ---- // ---- 组合根(Application)装配其他模块时需要的访问器 ----
Device& GetDevice() const { return *m_Device; } Device& GetDevice() const { return *m_Device; }
@@ -72,7 +72,18 @@ namespace Engine {
// 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。 // 可选叠加层(ImGui):不设置则 RG 里的 ImGui pass 为空转。
// 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。 // 生命周期:层必须先于 Renderer 析构(Application 成员序保证)。
void SetImGuiLayer(ImGuiLayer* layer) { m_ImGuiLayer = layer; } void SetImGuiLayer(ImGuiLayer* layer)
{
m_ImGuiLayer = layer;
if (layer) RefreshSceneTextures(); // 后端就绪:注册场景纹理
}
// ---- 场景显示纹理(tonemap 输出,ImGui::Image 可采样显示)----
// 按 m_CurrentFrame 取当帧槽位的描述符(UI 构建段与紧随的
// Render 同槽位)。无 ImGui 层时返回 nullptr
void* GetSceneTexture() const;
// 渲染分辨率(PresentTarget 尺寸,等比适配用)
vk::Extent2D GetRenderExtent() const { return m_Extent; }
// ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)---- // ---- 后处理参数(ImGui 可调,转发到 pass 共享的 PostProcessParams)----
f32 GetExposure() const; f32 GetExposure() const;
@@ -92,6 +103,8 @@ namespace Engine {
void SetTAAEnabled(bool b); void SetTAAEnabled(bool b);
bool GetBloomEnabled() const; bool GetBloomEnabled() const;
void SetBloomEnabled(bool b); void SetBloomEnabled(bool b);
bool GetLodEnabled() const;
void SetLodEnabled(bool b); // 关 = 全部强制 LOD0
int GetDebugView() const; int GetDebugView() const;
void SetDebugView(int v); // Passes::DebugView 枚举值 void SetDebugView(int v); // Passes::DebugView 枚举值
// 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举) // 调试视图名表(下标 = DebugView 枚举值,ImGui combo 直接枚举)
@@ -111,15 +124,34 @@ namespace Engine {
u32 GetMainInstanceCount() const; u32 GetMainInstanceCount() const;
u32 GetShadowInstanceCount() const; u32 GetShadowInstanceCount() const;
bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字) bool GetInstancesTruncated() const; // 实例预算溢出(遥测红字)
f32 GetCollectMs() const; // CPU 收集+剔除段耗时
// ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)----
int GetDisplayMode() const { return m_DesiredDisplayMode; }
void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明)
// Editor 视口的期望渲染尺寸(像素;UI 面板每帧上报,帧首统一生效)
void SetViewportSize(u32 w, u32 h)
{
m_DesiredViewport = vk::Extent2D{w, h};
}
// 垂直同步开关(present mode 切换会重建 swapchain), // 垂直同步开关(present mode 切换会重建 swapchain),
// 只应在帧 UI 构建段调用(Render 之前) // 只应在帧 UI 构建段调用(Render 之前)
void SetVSync(bool vsync); void SetVSync(bool vsync) { m_DesiredVSync = vsync; } // 帧首同步生效
// 帧首同步:窗口 resize / 视口尺寸 / 显示模式 / vsync 意图统一在
// UI 构建之前落定(waitIdle → 重编译 → 重绑描述符)。必须先于
// UI 构建段调用:本帧 draw data 引用的描述符与物理资源由此保证
// 全程新鲜(resize 中途换描述符会绑出已销毁句柄)
void BeginFrameSync();
private: private:
void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象 void BuildGraph(); // 声明 RG 资源 + 注册 pass 对象
void HandleResize(); void HandleResize(bool recreateSwapchain);
void ReadGpuTimes(u32 frameSlot); void ReadGpuTimes(u32 frameSlot);
void RefreshSceneTextures(); // 物理 view → ImGui 描述符(重建后刷新)
// 期望渲染分辨率:Editor 视口 = 面板尺寸(未上报时回退 swapchain);
// Direct = swapchain 尺寸
vk::Extent2D DesiredRenderExtent() const;
Window& m_Window; Window& m_Window;
@@ -131,6 +163,8 @@ namespace Engine {
Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring Scope<UniformBuffer> m_UniformBuffer; // per-slot UBO ring
Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO) Scope<UniformBuffer> m_ObjectBuffer; // GPUDriven 实例矩阵 ring(SSBO)
Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity) Scope<UniformBuffer> m_PrevObjectBuffer; // 上一帧实例矩阵 ring(TAA velocity)
Scope<UniformBuffer> m_SkinOffsetBuffer; // 实例→palette 基址 ring(SSBO,与实例序平行)
Scope<UniformBuffer> m_SkinPaletteBuffer; // 骨骼蒙皮矩阵 palette ring(SSBO,帧首 bump)
Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配 Scope<Passes::SceneGpu> m_SceneGpu; // 场景类 pass 共享装配
Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配 Scope<Passes::FullscreenGpu> m_FullscreenGpu; // 全屏 pass 共享装配
// 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造 // 环境预积分生成端(compute 链),先于 m_Environment/m_Passes 构造
@@ -150,6 +184,7 @@ namespace Engine {
// ---- RenderGraph 资源句柄 ---- // ---- RenderGraph 资源句柄 ----
RG::TextureHandle m_Backbuffer{}; RG::TextureHandle m_Backbuffer{};
RG::TextureHandle m_PresentTarget{}; // tonemap 输出(offscreen,ImGui 采样)
RG::TextureHandle m_Depth{}; RG::TextureHandle m_Depth{};
std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联 std::array<RG::TextureHandle, kMaxCascades> m_ShadowMaps{}; // CSM 级联
RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一 RG::TextureHandle m_HDRColor{}; // Forward 输出(线性 HDR),TAA 归一
@@ -178,6 +213,11 @@ namespace Engine {
vk::Extent2D m_Extent{}; vk::Extent2D m_Extent{};
u32 m_CurrentFrame = 0; u32 m_CurrentFrame = 0;
// ---- 帧首同步的意图缓冲(UI 段写入,下一帧帧首落定)----
int m_DesiredDisplayMode = kDisplayDirect; // 默认直接渲染, 视口渲染需要手动指定一下
vk::Extent2D m_DesiredViewport{}; // Editor 视口渲染分辨率(面板上报)
bool m_DesiredVSync = true;
}; };
} // namespace Engine } // namespace Engine
+42 -7
View File
@@ -21,8 +21,11 @@
#define HELLOVULKAN_SCENERENDERER_H #define HELLOVULKAN_SCENERENDERER_H
#include <Engine/Core/Types.h> #include <Engine/Core/Types.h>
#include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/SceneTypes.h> #include <Engine/Renderer/SceneTypes.h>
#include <glm/gtc/quaternion.hpp>
#include <vulkan/vulkan.hpp> #include <vulkan/vulkan.hpp>
#include <array>
#include <unordered_map> #include <unordered_map>
namespace Engine { namespace Engine {
@@ -34,8 +37,9 @@ namespace Engine {
public: public:
// 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。 // 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。
// targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。 // targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。
// dt 驱动骨骼动画采样推进(crossfade 计时)。
bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent); vk::Extent2D targetExtent, f32 dt);
FrameSceneData& Frame() { return m_Frame; } FrameSceneData& Frame() { return m_Frame; }
@@ -55,7 +59,9 @@ namespace Engine {
void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; } void SetShadowsEnabled(bool b) { m_ShadowsEnabled = b; }
void SetTAAEnabled(bool b) { m_TAAEnabled = b; } void SetTAAEnabled(bool b) { m_TAAEnabled = b; }
void SetBloomEnabled(bool b) { m_BloomEnabled = b; } void SetBloomEnabled(bool b) { m_BloomEnabled = b; }
void SetLodEnabled(bool b) { m_LodEnabled = b; }
void SetDebugView(int v) { m_DebugView = v; } void SetDebugView(int v) { m_DebugView = v; }
void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport
private: private:
// CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影 // CSM:把相机视锥按实用分割切 N 段,每段独立拟合光源正交投影
@@ -63,29 +69,45 @@ namespace Engine {
//(Forward UBO)共用同一组矩阵。 //(Forward UBO)共用同一组矩阵。
void ComputeCascades(const glm::vec3& shadowDir); void ComputeCascades(const glm::vec3& shadowDir);
// MDI 打包:batch 按组键排序(megabuffer + 可选材质)后压成
// IndirectDrawCmd 连续数组 + IndirectGroup 区间表。
// byMaterial=true(Forward):每材质一次 bind + 一次 MDI;
// byMaterial=false(Shadow):只按 megabuffer 分组(depth-only)
static void PackIndirect(CullResult& view, bool byMaterial);
FrameSceneData m_Frame; FrameSceneData m_Frame;
// ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)---- // ---- 剔除中间量(每帧 clear 复用,拼接进 m_Frame 后保留容量)----
struct ViewScratch { struct ViewScratch {
// cur + prev 合并写:两次随机堆访问 → 一次(cache miss 减半)
struct InstanceMatrices {
glm::mat4 cur;
glm::mat4 prev;
u32 skinBase = 0; // palette 区段基址(0 = identity 槽)
};
CullResult result; CullResult result;
// 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 = // 每 batch 自己的实例列表(按 mesh 分组)。GPU 端 batch 语义 =
// firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh // firstInstance + 连续区间,实体序收集不满足(多实体共享 mesh
// 时实例分散各处),扁平化成连续数组推迟到 appendView // 时实例分散各处),扁平化成连续数组推迟到 appendView
std::vector<std::vector<glm::mat4>> batchWorlds; std::vector<std::vector<InstanceMatrices>> batchMats;
std::vector<std::vector<glm::mat4>> batchPrevWorlds; // 每 batch 实例距离累加(透明排序用:平均距离降序 = 远→近)
std::unordered_map<Mesh*, u32> lookup; std::vector<f32> batchDistSum;
// batch 查找:数组寻址(下标 = Mesh::GetSlot(),值 = 每 LOD 的
// batchIdx+1,0 = 未用)——逐实例 hash 在大场景是毫秒级开销
std::vector<std::array<u32, kMeshLodCount>> lookup;
void Clear() void Clear()
{ {
result.batches.clear(); result.batches.clear();
result.instanceCount = 0; result.instanceCount = 0;
batchWorlds.clear(); batchMats.clear();
batchPrevWorlds.clear(); batchDistSum.clear();
lookup.clear(); for (auto& e : lookup) e.fill(0); // 容量保留,置零复用
} }
}; };
ViewScratch m_MainScratch; ViewScratch m_MainScratch;
ViewScratch m_CascScratch[kMaxCascades]; ViewScratch m_CascScratch[kMaxCascades];
ViewScratch m_TranslucentScratch; // 透明材质(仅主视图,不投阴影)
// ---- TAA 时域状态 ---- // ---- TAA 时域状态 ----
u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样) u64 m_FrameIndex = 0; // 抖动序列游标(Halton 2,3 ×8 采样)
@@ -96,13 +118,26 @@ namespace Engine {
std::unordered_map<u32, glm::mat4> m_PrevWorlds; std::unordered_map<u32, glm::mat4> m_PrevWorlds;
std::unordered_map<u32, glm::mat4> m_CurWorlds; std::unordered_map<u32, glm::mat4> m_CurWorlds;
// ---- 骨骼动画 scratch(帧首重建/容量复用)----
// entKey → palette 区段基址(第 2.5 节预采样填,收集段查)
std::unordered_map<u32, u32> m_EntitySkinBase;
// 采样层 A(当前 clip)/ B(fade 来源 clip)与 global 累乘
std::vector<glm::vec3> m_PosePosA, m_PosePosB;
std::vector<glm::quat> m_PoseRotA, m_PoseRotB;
std::vector<glm::vec3> m_PoseSclA, m_PoseSclB;
std::vector<u8> m_TouchedA, m_TouchedB;
std::vector<glm::mat4> m_Globals;
bool m_SkinTruncWarned = false; // palette 预算溢出只告警一次
bool m_TruncationWarned = false; // 实例缓冲打满只告警一次 bool m_TruncationWarned = false; // 实例缓冲打满只告警一次
// ---- 运行时开关/调试视图状态(本类是唯一写入者)---- // ---- 运行时开关/调试视图状态(本类是唯一写入者)----
bool m_ShadowsEnabled = true; bool m_ShadowsEnabled = true;
bool m_TAAEnabled = true; bool m_TAAEnabled = true;
bool m_BloomEnabled = true; bool m_BloomEnabled = true;
bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过)
int m_DebugView = 0; int m_DebugView = 0;
int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态
}; };
} // namespace Engine } // namespace Engine
+64 -2
View File
@@ -18,18 +18,49 @@
namespace Engine { namespace Engine {
class Mesh; class Mesh;
class Material;
// GPUDriven:按 mesh 聚合的绘制批 // GPUDriven:按 (mesh, LOD) 聚合的绘制批——同 mesh 不同 LOD 的实例
// 索引区间不同,不能并成一次 draw
struct InstanceBatch { struct InstanceBatch {
Mesh* mesh; Mesh* mesh;
u32 first; // 在实例矩阵数组中的起始下标 u32 first; // 在实例矩阵数组中的起始下标
u32 count; // 实例数 u32 count; // 实例数
u32 lod = 0; // 绘制用索引区间(Mesh::Draw)
}; };
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份) // MDI:一次 vkCmdDrawIndexedIndirectCount 消费的命令(CPU 侧镜像,
// 内存布局 = VkDrawIndexedIndirectCommand,20B)
struct IndirectDrawCmd {
u32 indexCount;
u32 instanceCount;
u32 firstIndex; // megabuffer 里的全局索引下标
u32 vertexOffset; // megabuffer 里的顶点元素偏移
u32 firstInstance; // 实例矩阵数组下标
};
static_assert(sizeof(IndirectDrawCmd) == 20, "布局必须镜像 VkDrawIndexedIndirectCommand");
// MDI:共享同一 megabuffer(同 Model)的一组命令——
// 一次 bindVertex/IndexBuffer +(Forward)一次材质 Set1 绑定 +
// 一次 drawIndexedIndirectCount。组内命令在 indirect buffer 中连续
struct IndirectGroup {
const Mesh* mesh = nullptr; // 取共享 VB/IB 句柄(组内同一份)
Material* material = nullptr; // shadow 组为 null(depth-only 无材质)
u32 firstCommand = 0; // 命令区起始下标(count 同下标)
u32 commandCount = 0;
};
// indirect buffer 的视图分区:[main | cascade0 | cascade1 | cascade2],
// 每区 kMaxInstances 条命令预算(每实例至多 1 条,必不越界)
constexpr u32 kIndirectViewMain = 0;
// 一个渲染视图的剔除结果(相机视图 / 光源视图各一份)。
// batches = 逐实例批次(透明队列仍逐批 draw);indirect = MDI 打包产物
struct CullResult { struct CullResult {
std::vector<InstanceBatch> batches; std::vector<InstanceBatch> batches;
u32 instanceCount = 0; u32 instanceCount = 0;
std::vector<IndirectDrawCmd> indirectCommands;
std::vector<IndirectGroup> indirectGroups;
}; };
constexpr u32 kMaxLights = 4; constexpr u32 kMaxLights = 4;
@@ -41,6 +72,13 @@ namespace Engine {
constexpr u32 kMaxCascades = 3; // CSM 级联数 constexpr u32 kMaxCascades = 3; // CSM 级联数
constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长 constexpr u32 kCascadeRes = 1024; // 每级联阴影图边长
// 蒙皮矩阵预算(palette 总槽位数/帧):每动画实体一段(按实体分配,
// 不按模型去重——同模型两实体可不同播放状态)。4096×64B = 256KB/槽位
constexpr u32 kMaxSkinMatrices = 4096;
// palette slot 0 固定 identity:无骨骼顶点(weights=(1,0,0,0), idx=0)
// 直通它 → 静态模型蒙皮零影响
constexpr u32 kIdentityPaletteSlot = 0;
// 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。 // 镜像 shader 侧 UBO 布局的灯光项(std140,每灯 64B)。
// 字段未用部分置零——shader 按类型分支读取 // 字段未用部分置零——shader 按类型分支读取
struct LightData { struct LightData {
@@ -68,6 +106,14 @@ namespace Engine {
glm::vec4 cameraPos; // 相机世界位置(高光 V 向量) glm::vec4 cameraPos; // 相机世界位置(高光 V 向量)
}; };
// 显示模式(帧契约 displayMode 字段;ImGui 经 Renderer 门面写入):
// Direct = 全屏直出:Blit pass 把 PresentTarget 拷进 backbuffer,
// ImGui 叠加其上(游戏形态)
// Viewport = Editor 视口:backbuffer 只画 ImGui(清屏),场景经
// ImGui::Image 呈现;渲染分辨率跟随 ImGui 面板尺寸
inline constexpr int kDisplayDirect = 0;
inline constexpr int kDisplayViewport = 1;
// 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。 // 每帧从场景收集的渲染数据(SceneRenderer 生产,pass 消费)。
// 复用成员容量:clear() 不缩容,稳态零每帧堆分配。 // 复用成员容量:clear() 不缩容,稳态零每帧堆分配。
struct FrameSceneData { struct FrameSceneData {
@@ -85,21 +131,37 @@ namespace Engine {
// 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一 // 上一帧世界矩阵,与 instanceModels 平行对齐(同一下标 = 同一
// 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵 // 实例的上一帧姿态,TAA velocity 用);首帧/新实体 = 当前矩阵
std::vector<glm::mat4> prevInstanceModels; std::vector<glm::mat4> prevInstanceModels;
// ---- 蒙皮(与 instanceModels 平行的 per-instance 间接索引)----
// skinOffsets[i] = 第 i 个实例的 palette 区段起点(kMaxSkinMatrices
// 全局数组内下标);无动画实体指向 slot0 identity
std::vector<u32> skinOffsets;
// palette 区段池:帧首 bump allocator 按动画实体分配
// [skinBase .. skinBase+boneCount) = 该实体全部骨骼的蒙皮矩阵
std::vector<glm::mat4> skinPalettes;
CullResult mainView; // 相机视锥 → Forward pass CullResult mainView; // 相机视锥 → Forward pass
// 每级联独立剔除结果(Shadow pass 按 region 各画各的—— // 每级联独立剔除结果(Shadow pass 按 region 各画各的——
// 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉) // 级联盒沿光方向加了 caster pad,影子不会被剔除裁掉)
CullResult shadowCascades[kMaxCascades]; CullResult shadowCascades[kMaxCascades];
// 透明批次(预乘 alpha 混合,深度只测不写,远→近排序);
// 不参与阴影投射
CullResult translucentView;
// ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入, // ---- 运行时开关/调试视图(ImGui 经 Renderer 门面写入,
// SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)---- // SceneRenderer 唯一落帧,各 pass 只读;每帧确定性)----
bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路 bool shadowsEnabled = true; // ShadowPass 空转 + frag ShadowFactor 短路
bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动 bool taaEnabled = true; // TAAPass 直通(alpha=0) + 停投影抖动
bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零 bool bloomEnabled = true; // BloomPass 子 pass 空转 + tonemap 强度归零
bool lodEnabled = true; // 关 = 实例收集恒选 LOD0(绘制原网格)
int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏) int debugView = 0; // Passes::DebugView 枚举(tonemap 路由上屏)
int displayMode = kDisplayViewport; // 显示模式(Blit 路由 + ImGui loadOp)
// 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃—— // 实例预算溢出(kMaxInstances):本轮收集有实例被静默丢弃——
// 必须遥测可见,"只告警一次"的大场景截断等于没有告警 // 必须遥测可见,"只告警一次"的大场景截断等于没有告警
bool instancesTruncated = false; bool instancesTruncated = false;
// palette 预算溢出(kMaxSkinMatrices):动画实体 palette 被截断
bool skinPaletteTruncated = false;
f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测) f32 collectMs = 0.0f; // 收集+剔除 CPU 耗时(遥测)
}; };
+20 -2
View File
@@ -16,6 +16,14 @@ namespace Engine {
glm::vec2 uv; glm::vec2 uv;
glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace) glm::vec3 tangent; // 切线(法线贴图 TBN 用;assimp CalcTangentSpace)
// ---- 骨骼蒙皮属性 ----
// 无骨骼资产保持默认值(weights=(1,0,0,0) + palette[0]=identity)
// → skin 矩阵 = identity,蒙皮路径对静态模型零影响。
// weights 必须带默认初始化器:聚合初始化 Vertex{pos,normal,uv,tangent}
// 会把未列出的成员零初始化 → skin=mat4(0) → 顶点全部塌缩到原点
glm::uvec4 boneIds{0, 0, 0, 0};
glm::vec4 weights{1.0f, 0.0f, 0.0f, 0.0f};
static vk::VertexInputBindingDescription2EXT GetBindingDescription() { static vk::VertexInputBindingDescription2EXT GetBindingDescription() {
vk::VertexInputBindingDescription2EXT binding; vk::VertexInputBindingDescription2EXT binding;
binding.setBinding(0) binding.setBinding(0)
@@ -25,8 +33,8 @@ namespace Engine {
return binding; return binding;
} }
static std::array<vk::VertexInputAttributeDescription2EXT, 4> GetAttributeDescriptions() { static std::array<vk::VertexInputAttributeDescription2EXT, 6> GetAttributeDescriptions() {
std::array<vk::VertexInputAttributeDescription2EXT, 4> attrs; std::array<vk::VertexInputAttributeDescription2EXT, 6> attrs;
attrs[0].setLocation(0) attrs[0].setLocation(0)
.setBinding(0) .setBinding(0)
@@ -48,6 +56,16 @@ namespace Engine {
.setFormat(vk::Format::eR32G32B32Sfloat) .setFormat(vk::Format::eR32G32B32Sfloat)
.setOffset(offsetof(Vertex, tangent)); .setOffset(offsetof(Vertex, tangent));
attrs[4].setLocation(4)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Uint)
.setOffset(offsetof(Vertex, boneIds));
attrs[5].setLocation(5)
.setBinding(0)
.setFormat(vk::Format::eR32G32B32A32Sfloat)
.setOffset(offsetof(Vertex, weights));
return attrs; return attrs;
} }
}; };
@@ -35,6 +35,8 @@ namespace Engine {
Scope<Model> LoadModel(const std::string& path, Scope<Model> LoadModel(const std::string& path,
ParsedModel&& parsed) override; ParsedModel&& parsed) override;
Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override; Scope<Model> CreatePlane(f32 halfSize, u8 r, u8 g, u8 b) override;
Scope<Model> CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a) override;
Scope<Cubemap> LoadEnvironment(const std::string& path) override; Scope<Cubemap> LoadEnvironment(const std::string& path) override;
Scope<Cubemap> LoadEnvironment(const std::string& path, Scope<Cubemap> LoadEnvironment(const std::string& path,
DecodedEquirect&& decoded) override; DecodedEquirect&& decoded) override;
+3 -1
View File
@@ -26,7 +26,9 @@ namespace Engine {
Buffer& operator=(const Buffer&) = delete; Buffer& operator=(const Buffer&) = delete;
vk::Buffer Get() const { return m_Buffer; } vk::Buffer Get() const { return m_Buffer; }
vk::DeviceSize GetSize() const { return m_Size; } vk::DeviceSize Size() const { return m_Size; }
// 持久映射指针(非 host visible 为 nullptr;诊断读回用)
void* MappedPtr() const { return m_MappedPtr; }
// 只有 HOST_VISIBLE 的 Buffer 能调 // 只有 HOST_VISIBLE 的 Buffer 能调
void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const; void Upload(const void* data, vk::DeviceSize size, vk::DeviceSize offset = 0) const;
+4
View File
@@ -43,6 +43,8 @@ public:
bool IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; } bool IsDynamicRenderingSupported() const { return m_DynamicRenderingSupported; }
bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; } bool IsShaderObjectSupported() const { return m_ShaderObjectSupported; }
bool IsSynchronization2Supported() const { return m_Synchronization2Supported; } bool IsSynchronization2Supported() const { return m_Synchronization2Supported; }
// MDI:drawCount>1 的 drawIndexedIndirect(Count) 必需(core 特性位)
bool IsMultiDrawIndirectSupported() const { return m_MultiDrawIndirectSupported; }
private: private:
void CreateInstance(); void CreateInstance();
@@ -72,6 +74,8 @@ private:
bool m_DynamicRenderingSupported = false; bool m_DynamicRenderingSupported = false;
bool m_ShaderObjectSupported = false; bool m_ShaderObjectSupported = false;
bool m_Synchronization2Supported = false; bool m_Synchronization2Supported = false;
bool m_MultiDrawIndirectSupported = false;
bool m_DrawIndirectCountSupported = false;
std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" }; std::vector<const char*> m_ValidationLayers = { "VK_LAYER_KHRONOS_validation" };
std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; std::vector<const char*> m_DeviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };
+3
View File
@@ -31,6 +31,9 @@ namespace Engine {
struct ProceduralTag {}; struct ProceduralTag {};
static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator, static Scope<Texture> CreateSolidColor(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, bool srgb = true); u8 r, u8 g, u8 b, bool srgb = true);
// 带 alpha 的纯色纹理(透明管线验证等)
static Scope<Texture> CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb = true);
~Texture(); ~Texture();
+12
View File
@@ -41,6 +41,18 @@ namespace Engine::Assets {
[&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); }); [&](const std::string&) { return m_Uploader.CreatePlane(halfSize, r, g, b); });
} }
ModelRef AssetManager::CreateTransparentPlane(f32 halfSize,
u8 r, u8 g, u8 b, u8 a)
{
std::string key = "procedural://transparent-plane/" +
std::to_string(halfSize) + "/" +
std::to_string(r) + "," + std::to_string(g) + "," +
std::to_string(b) + "," + std::to_string(a);
return m_Models.Load(key, [&](const std::string&) {
return m_Uploader.CreateTransparentPlane(halfSize, r, g, b, a);
});
}
EnvRef AssetManager::LoadEnvironment(const std::string& path) EnvRef AssetManager::LoadEnvironment(const std::string& path)
{ {
// GPU 生成(compute 预积分)经接口发生在渲染层实现里 // GPU 生成(compute 预积分)经接口发生在渲染层实现里
+14 -4
View File
@@ -61,7 +61,10 @@ namespace Engine {
void Application::EnableImGui() void Application::EnableImGui()
{ {
if (m_ImGuiLayer) return; if (m_ImGuiLayer) {
ENGINE_WARN("ImGui layer already enabled, do not enable twice!");
return;
}
m_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer); m_ImGuiLayer = CreateScope<ImGuiLayer>(GetWindow(), *m_Renderer);
m_Renderer->SetImGuiLayer(m_ImGuiLayer.get()); m_Renderer->SetImGuiLayer(m_ImGuiLayer.get());
@@ -108,24 +111,31 @@ namespace Engine {
m_Scene->FlushDestroyQueue(); m_Scene->FlushDestroyQueue();
m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜 m_Scene->UpdateTransforms(); // UE transform-update 阶段:渲染前保证 world 新鲜
// 帧首同步:窗口 resize / 渲染尺寸(面板跟随)/ 显示模式 / vsync
// 统一在 UI 构建"之前"落定——此处可能重建 swapchain 与 RG 目标
// 并重注册 ImGui 场景纹理;若挪到 UI 构建之后,本帧 draw data
// 会引用刚被销毁的描述符(validation: Invalid VkDescriptorSet)
m_Renderer->BeginFrameSync();
// ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ---- // ---- UI 构建段:OnRenderOverlay 只"声明"控件,不直接碰 GPU ----
if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled()) if (m_ImGuiLayer && m_ImGuiLayer->IsEnabled())
{ {
const auto uiStart = std::chrono::steady_clock::now(); const auto uiStart = std::chrono::steady_clock::now();
m_ImGuiLayer->NewFrame(); m_ImGuiLayer->NewFrame();
OnRenderOverlay(); OnImGuiRender();
m_ImGuiLayer->BuildDrawData(); m_ImGuiLayer->BuildDrawData();
const auto uiEnd = std::chrono::steady_clock::now(); const auto uiEnd = std::chrono::steady_clock::now();
} }
// ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) → // ---- 主渲染(框架行为):RG 顺序 Forward → ImGui(loadOp=Load) →
// Present,draw data 在 ImGui pass 里才真正提交 GPU ---- // Present,draw data 在 ImGui pass 里才真正提交 GPU ----
m_Renderer->Render(*m_Scene, *m_Assets); m_Renderer->Render(*m_Scene, *m_Assets, m_DeltaTime);
// ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ---- // ---- 帧率控制:vsync 开启时 present 自带节奏;否则按目标限帧 ----
// 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU // 策略:先 sleep 到剩 ~2ms,再自旋——精度高且不烧 CPU
if (!m_VSync) { // TargetFps <= 0 表示不限帧(跳过 sleep,避免 1e6/0 除零 UB)
if (!m_VSync && m_TargetFps > 0) {
const auto deadline = frameStart + const auto deadline = frameStart +
std::chrono::microseconds(static_cast<long long>( std::chrono::microseconds(static_cast<long long>(
1e6 / m_TargetFps)); 1e6 / m_TargetFps));
+41 -17
View File
@@ -81,12 +81,19 @@ namespace Engine
void Window::WindowFocusCallback(GLFWwindow* window, int focused) void Window::WindowFocusCallback(GLFWwindow* window, int focused)
{ {
const auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window)); auto* self = static_cast<Window*>(glfwGetWindowUserPointer(window));
if (!self->m_EventCallback) return; if (!self->m_EventCallback) return;
Event e; Event e;
e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus; e.type = focused ? EventType::WindowFocus : EventType::WindowLostFocus;
self->m_EventCallback(e); self->m_EventCallback(e);
// 失焦时释放光标捕获(切窗口不会留下隐形被捕获的光标;
// 相机下一帧按右键状态自行决定是否重新捕获)
if (!focused && self->m_CursorCaptured)
{
self->SetCursorCaptured(false);
}
} }
void Window::WindowCloseCallback(GLFWwindow* window) void Window::WindowCloseCallback(GLFWwindow* window)
@@ -120,7 +127,7 @@ namespace Engine
glfwTerminate(); glfwTerminate();
return; return;
} }
glfwSetWindowOpacity(m_Window, 0.5f); glfwSetWindowOpacity(m_Window, 0.0f);
ENGINE_INFO("Window created: {} ({}x{})", title, width, height); ENGINE_INFO("Window created: {} ({}x{})", title, width, height);
@@ -191,32 +198,49 @@ namespace Engine
return glfwGetKey(m_Window, key) == GLFW_PRESS; return glfwGetKey(m_Window, key) == GLFW_PRESS;
} }
bool Window::IsMouseButtonPressed(const int button) const
{
// UI 捕获鼠标时(ImGui 悬停)不占用:面板上的点击归 ImGui。
// 已捕获光标(自由视角中)时绕过——虚拟光标位置无意义
if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture()) return false;
return glfwGetMouseButton(m_Window, button) == GLFW_PRESS;
}
void Window::SetCursorCaptured(const bool captured)
{
if (m_CursorCaptured == captured) return;
m_CursorCaptured = captured;
glfwSetInputMode(m_Window, GLFW_CURSOR,
captured ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
// DISABLED 模式光标是虚拟无界坐标,与上次普通坐标不连续——
// 重置参考点,下一帧只建立基准不出增量(防视角跳变)
m_FirstMouse = true;
}
void Window::GetMouseDelta(float& dx, float& dy) const void Window::GetMouseDelta(float& dx, float& dy) const
{ {
double mx, my; double mx, my;
glfwGetCursorPos(m_Window, &mx, &my); glfwGetCursorPos(m_Window, &mx, &my);
static double lastX = mx; // 首帧(或捕获切换后):只建立参考点
static double lastY = my; if (m_FirstMouse)
static bool first = true;
if (first)
{ {
lastX = mx; m_LastMouseX = mx;
lastY = my; m_LastMouseY = my;
first = false; m_FirstMouse = false;
dx = dy = 0.0f; dx = dy = 0.0f;
return; return;
} }
dx = static_cast<float>(mx - lastX); dx = static_cast<float>(mx - m_LastMouseX);
dy = static_cast<float>(my - lastY); dy = static_cast<float>(my - m_LastMouseY);
m_LastMouseX = mx;
m_LastMouseY = my;
lastX = mx; // UI 捕获鼠标(ImGui 悬停)时 delta 归零;参考点照常更新,避免
lastY = my; // 释放后跳变。光标被相机捕获时绕过——虚拟光标可能悬在 ImGui
// 面板上方,若被归零视角会卡死
// UI 捕获鼠标时 delta 归零;lastX/Y 照常更新,避免释放后跳变 if (!m_CursorCaptured && m_MouseCapture && m_MouseCapture())
if (m_MouseCapture && m_MouseCapture())
{ {
dx = dy = 0.0f; dx = dy = 0.0f;
} }
-2
View File
@@ -16,9 +16,7 @@ namespace Engine::ECS {
// 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义) // 出生即有:UUID(稳定身份)+ Transform(空间存在性,Unity 语义)
m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()}); m_Registry.emplace<IdComponent>(e, IdComponent{UUID::New()});
m_Registry.emplace<TransformComponent>(e); m_Registry.emplace<TransformComponent>(e);
if (!name.empty()) {
m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)}); m_Registry.emplace<NameComponent>(e, NameComponent{std::move(name)});
}
return EntityHandle{this, e}; return EntityHandle{this, e};
} }
+25
View File
@@ -95,6 +95,11 @@ namespace Engine {
// 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构) // 后端销毁自己的管线/字体资源前先等 GPU 空闲(层先于 Renderer 析构)
m_Device.GetDevice().waitIdle(); m_Device.GetDevice().waitIdle();
// 场景纹理描述符先归还(后端 shutdown 前必须完成)
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
UpdateSceneTexture(slot, vk::ImageView{});
}
ImGui_ImplVulkan_Shutdown(); ImGui_ImplVulkan_Shutdown();
ImGui_ImplGlfw_Shutdown(); ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(); ImGui::DestroyContext();
@@ -104,6 +109,26 @@ namespace Engine {
ENGINE_INFO("ImGuiLayer shut down"); ENGINE_INFO("ImGuiLayer shut down");
} }
void ImGuiLayer::UpdateSceneTexture(u32 slot, vk::ImageView view)
{
if (slot >= kMaxFramesInFlight) return;
// 旧描述符先归还(池带 eFreeDescriptorSet;resize 重建时 GPU 已
// waitIdle,无在飞引用)
if (m_SceneTextures[slot]) {
ImGui_ImplVulkan_RemoveTexture(
(VkDescriptorSet)(intptr_t)m_SceneTextures[slot]);
m_SceneTextures[slot] = ImTextureID_Invalid;
}
if (view) {
// 后端自有采样器(linear):只需提供 view 与采样期 layout——
// RG 已保证 ImGuiPass 执行期处于 eShaderReadOnlyOptimal
m_SceneTextures[slot] = (ImTextureID)(intptr_t)
ImGui_ImplVulkan_AddTexture(
(VkImageView)view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}
}
void ImGuiLayer::NewFrame() void ImGuiLayer::NewFrame()
{ {
ImGui_ImplVulkan_NewFrame(); ImGui_ImplVulkan_NewFrame();
+20 -3
View File
@@ -7,6 +7,7 @@
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <algorithm> #include <algorithm>
#include <ranges>
#include <set> #include <set>
#include <stdexcept> #include <stdexcept>
@@ -41,8 +42,8 @@ namespace Engine::RG {
ReleasePhysical(); ReleasePhysical();
const auto& vkDevice = m_Device.GetDevice(); const auto& vkDevice = m_Device.GetDevice();
for (auto& [key, images] : m_Pool) { for (auto &images: m_Pool | std::views::values) {
for (auto& img : images) { for (const auto& img : images) {
if (!img->valid) continue; if (!img->valid) continue;
vkDevice.destroyImageView(img->view); vkDevice.destroyImageView(img->view);
m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放 m_Allocator.Destroy(img->allocation); // image 资源与显存成对释放
@@ -57,7 +58,7 @@ namespace Engine::RG {
{ {
CheckMutable(); CheckMutable();
TextureHandle h{static_cast<u32>(m_Textures.size()), 1}; const TextureHandle h{static_cast<u32>(m_Textures.size()), 1};
TextureEntry entry; TextureEntry entry;
entry.name = std::move(name); entry.name = std::move(name);
@@ -101,6 +102,15 @@ namespace Engine::RG {
entry.importedView = view; entry.importedView = view;
} }
void RenderGraph::SetImportedExtent(TextureHandle h, vk::Extent2D extent)
{
auto& entry = GetEntry(h);
if (!entry.imported) {
ENGINE_FATAL("RenderGraph: SetImportedExtent on non-imported resource '{}'", entry.name);
}
entry.importedExtent = extent;
}
// ---------- Pass 声明 ---------- // ---------- Pass 声明 ----------
RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec) RenderGraph::PassBuilder RenderGraph::AddPass(std::string name, ExecuteFn exec)
@@ -832,6 +842,13 @@ namespace Engine::RG {
return entry.physical[frame.frameSlot]->image; return entry.physical[frame.frameSlot]->image;
} }
vk::Extent2D RenderGraphContext::Extent(TextureHandle h) const
{
const auto& entry = graph->GetEntry(h);
if (entry.imported) return entry.importedExtent;
return entry.physical[frame.frameSlot]->extent;
}
u32 RenderGraphContext::HistorySlot(u32 frameSlot) const u32 RenderGraphContext::HistorySlot(u32 frameSlot) const
{ {
return graph->HistorySlot(frameSlot); return graph->HistorySlot(frameSlot);
+18 -3
View File
@@ -14,21 +14,36 @@ namespace Engine {
void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime) void Camera::Update(Window& window, ECS::TransformComponent& t, f32 deltaTime)
{ {
// 鼠标旋转:直接写实体旋转(yaw → rotation.y,pitch → rotation.x) // 鼠标视角(编辑器习惯):按住右键捕获光标进入自由视角,
if (m_MouseLookEnabled) // 松开释放光标(ImGui 面板可正常交互)。m_MouseLookEnabled 总开关
const bool wantLook = m_MouseLookEnabled &&
window.IsMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT);
if (wantLook)
{ {
if (!window.IsCursorCaptured())
{
window.SetCursorCaptured(true);
}
f32 mouseDX = 0, mouseDY = 0; f32 mouseDX = 0, mouseDY = 0;
window.GetMouseDelta(mouseDX, mouseDY); window.GetMouseDelta(mouseDX, mouseDY);
if (mouseDX != 0 || mouseDY != 0) if (mouseDX != 0 || mouseDY != 0)
{ {
glm::vec3 rot = t.GetRotation(); glm::vec3 rot = t.GetRotation();
rot.y += mouseDX * m_MouseSensitivity; // 右手系绕 Y 正角度 = 向左转,鼠标右移(DX>0)应看向右
// → yaw 取负。pitch 正角度 = 抬头,鼠标下移(DY>0)低头
// → rot.x 直接用负号,两者符号已核对一致
rot.y -= mouseDX * m_MouseSensitivity;
rot.x = glm::clamp( rot.x = glm::clamp(
rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f); rot.x - mouseDY * m_MouseSensitivity, -89.0f, 89.0f);
t.SetRotation(rot); t.SetRotation(rot);
} }
} }
else if (window.IsCursorCaptured())
{
window.SetCursorCaptured(false);
}
// 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X, // 基向量由实体旋转决定(本地 -Z 为视线方向;应用顺序 Y→X,
// 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机) // 与 TransformComponent::Compose 一致;roll 不参与 FPS 相机)
+6 -2
View File
@@ -17,8 +17,10 @@ namespace Engine
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout layout, vk::DescriptorSetLayout layout,
const std::array<Texture*, kSlotCount>& textures, const std::array<Texture*, kSlotCount>& textures,
const MaterialParams& params) const MaterialParams& params,
bool transparent)
: m_Textures(textures) : m_Textures(textures)
, m_Transparent(transparent)
{ {
m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout); m_DescriptorSet = descriptorManager.AllocateDescriptorSet(layout);
@@ -44,7 +46,8 @@ namespace Engine
Material::~Material() = default; Material::~Material() = default;
std::string Material::MakeCacheKey(const std::string paths[kSlotCount], std::string Material::MakeCacheKey(const std::string paths[kSlotCount],
const MaterialParams& params) const MaterialParams& params,
bool transparent)
{ {
std::string key; std::string key;
for (u32 slot = 0; slot < kSlotCount; ++slot) { for (u32 slot = 0; slot < kSlotCount; ++slot) {
@@ -53,6 +56,7 @@ namespace Engine
} }
key += "f:" + std::to_string(params.factors.x) + key += "f:" + std::to_string(params.factors.x) +
"," + std::to_string(params.factors.y); "," + std::to_string(params.factors.y);
key += transparent ? ":t" : ":o"; // 透明语义入键(描述符相同但队列不同)
return key; return key;
} }
} // namespace Engine } // namespace Engine
+40 -10
View File
@@ -10,20 +10,28 @@
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <limits> #include <limits>
#include <atomic>
#include "Engine/Core/Stats.h" #include "Engine/Core/Stats.h"
namespace Engine namespace Engine
{ {
namespace {
// 全局连续 mesh 槽位:SceneRenderer 的 batch 查找用数组寻址
//(每实例每视图一次 hash 的代价在大场景下是 5ms 级)
std::atomic<u32> s_nextMeshSlot{0};
}
Mesh::Mesh(Device& device, MemoryAllocator& allocator, Mesh::Mesh(Device& device, MemoryAllocator& allocator,
const std::vector<Vertex>& vertices, const std::vector<Vertex>& vertices,
const std::vector<u32>& indices, const std::vector<std::vector<u32>>& lods,
Material* material) Material* material)
: m_Material(material) : m_Material(material)
, m_Slot(s_nextMeshSlot.fetch_add(1, std::memory_order_relaxed))
{ {
if (vertices.empty() || indices.empty()) if (vertices.empty() || lods.empty() || lods[0].empty())
{ {
ENGINE_ERROR("Mesh: empty vertices or indices"); ENGINE_ERROR("Mesh: empty vertices or LOD0 indices");
return; return;
} }
@@ -35,15 +43,31 @@ namespace Engine
vk::MemoryPropertyFlagBits::eHostCoherent); vk::MemoryPropertyFlagBits::eHostCoherent);
m_VertexBuffer->Upload(vertices.data(), vbSize); m_VertexBuffer->Upload(vertices.data(), vbSize);
u64 ibSize = sizeof(u32) * indices.size(); // 各级索引连续拼进同一 index buffer(共享顶点,只换索引区间)
std::vector<u32> allIndices;
for (const auto& idx : lods) {
allIndices.insert(allIndices.end(), idx.begin(), idx.end());
}
u64 ibSize = sizeof(u32) * allIndices.size();
m_IndexBuffer = CreateScope<Buffer>( m_IndexBuffer = CreateScope<Buffer>(
device, allocator, ibSize, device, allocator, ibSize,
vk::BufferUsageFlagBits::eIndexBuffer, vk::BufferUsageFlagBits::eIndexBuffer,
vk::MemoryPropertyFlagBits::eHostVisible | vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent); vk::MemoryPropertyFlagBits::eHostCoherent);
m_IndexBuffer->Upload(indices.data(), ibSize); m_IndexBuffer->Upload(allIndices.data(), ibSize);
m_IndexCount = static_cast<u32>(indices.size()); // 记录各级区间。LOD 不足的级复用最后一级的"已有区间"
//(first 不得继续累加——buffer 里没有重复数据,累加必越界)
u32 first = 0;
for (u32 lod = 0; lod < kMeshLodCount; ++lod) {
if (lod < lods.size()) {
m_Lods[lod] = {first, static_cast<u32>(lods[lod].size())};
first += m_Lods[lod].indexCount;
} else {
m_Lods[lod] = m_Lods[lods.size() - 1];
}
}
// 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用) // 局部空间包围球:顶点包围盒 → 中心 + 最大半径(视锥剔除用)
{ {
@@ -61,10 +85,12 @@ namespace Engine
} }
} }
ENGINE_INFO("Mesh created: {} vertices, {} indices", vertices.size(), indices.size()); ENGINE_INFO("Mesh created: {} vertices, LOD0 {} indices", vertices.size(), lods[0].size());
m_VertexBytes = sizeof(Vertex) * vertices.size(); m_VertexBytes = sizeof(Vertex) * vertices.size();
m_IndexBytes = sizeof(u32) * indices.size(); for (const auto& idx : lods) {
m_IndexBytes += sizeof(u32) * idx.size();
}
STATS.AddMesh(m_VertexBytes, m_IndexBytes); STATS.AddMesh(m_VertexBytes, m_IndexBytes);
} }
@@ -73,10 +99,14 @@ namespace Engine
STATS.RemoveMesh(m_VertexBytes, m_IndexBytes); STATS.RemoveMesh(m_VertexBytes, m_IndexBytes);
} }
void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance) const void Mesh::Draw(vk::CommandBuffer cmd, u32 instanceCount, u32 firstInstance,
u32 lod) const
{ {
const LodRange& range =
m_Lods[lod < kMeshLodCount ? lod : kMeshLodCount - 1];
cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0}); cmd.bindVertexBuffers(0, m_VertexBuffer->Get(), {0});
cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32); cmd.bindIndexBuffer(m_IndexBuffer->Get(), 0, vk::IndexType::eUint32);
cmd.drawIndexed(m_IndexCount, instanceCount, 0, 0, firstInstance); cmd.drawIndexed(range.indexCount, instanceCount, range.firstIndex,
0, firstInstance);
} }
} // namespace Engine } // namespace Engine
+372 -40
View File
@@ -13,12 +13,28 @@
#include <assimp/Importer.hpp> #include <assimp/Importer.hpp>
#include <assimp/scene.h> #include <assimp/scene.h>
#include <assimp/postprocess.h> #include <assimp/postprocess.h>
#include <assimp/GltfMaterial.h> // AI_MATKEY_GLTF_ALPHAMODE
#include <meshoptimizer.h>
#include <filesystem> #include <filesystem>
#include <algorithm> #include <algorithm>
namespace Engine { namespace Engine {
namespace {
// assimp 行主矩阵 → glm 列主(转置搬运;骨骼 node TRS / invBind 共用)
glm::mat4 ToGlm(const aiMatrix4x4& m)
{
glm::mat4 out;
out[0][0] = m.a1; out[1][0] = m.a2; out[2][0] = m.a3; out[3][0] = m.a4;
out[0][1] = m.b1; out[1][1] = m.b2; out[2][1] = m.b3; out[3][1] = m.b4;
out[0][2] = m.c1; out[1][2] = m.c2; out[2][2] = m.c3; out[3][2] = m.c4;
out[0][3] = m.d1; out[1][3] = m.d2; out[2][3] = m.d3; out[3][3] = m.d4;
return out;
}
}
Model::Model(Device& device, MemoryAllocator& allocator, Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -43,6 +59,23 @@ Model::Model(Device& device, MemoryAllocator& allocator,
Model::~Model() = default; Model::~Model() = default;
void Model::RebuildUnionBounds()
{
if (m_Meshes.empty()) {
m_Bounds = {};
return;
}
// 各 mesh 包围球 → AABB 并集 → 外接球(保守,宁多画不漏画)
glm::vec3 mn(1e9f), mx(-1e9f);
for (const auto& mesh : m_Meshes) {
const auto& b = mesh->GetBounds();
mn = glm::min(mn, b.center - glm::vec3(b.radius));
mx = glm::max(mx, b.center + glm::vec3(b.radius));
}
m_Bounds.center = (mn + mx) * 0.5f;
m_Bounds.radius = 0.5f * glm::length(mx - mn);
}
void Model::InitDefaultTextures() void Model::InitDefaultTextures()
{ {
// 各材质槽位的兜底纹理(1x1,开销可忽略): // 各材质槽位的兜底纹理(1x1,开销可忽略):
@@ -56,21 +89,34 @@ void Model::InitDefaultTextures()
m_DefaultTextures[Material::Emissive] = m_DefaultTextures[Material::Emissive] =
Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0); Texture::CreateSolidColor(m_Device, m_Allocator, 0, 0, 0);
m_DefaultTextures[Material::Ao] = m_DefaultTextures[Material::Ao] =
Texture::CreateSolidColor(m_Device, m_Allocator, 255, 255, 255, false); Texture::CreateSolidColor(m_Device, m_Allocator, 255, 0, 0, false);
// 独立粗糙度槽兜底:0.5 灰(无图资产 = 中性粗糙度)
m_DefaultTextures[Material::Roughness] =
Texture::CreateSolidColor(m_Device, m_Allocator, 128, 128, 128, false);
} }
Model::Model(Device& device, MemoryAllocator& allocator, Model::Model(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
ProceduralTag, f32 halfSize, const std::array<u8, 3>& color) ProceduralTag, f32 halfSize, const std::array<u8, 4>& rgba,
bool transparent)
: m_Device(device) : m_Device(device)
, m_Allocator(allocator) , m_Allocator(allocator)
, m_DescriptorManager(descriptorManager) , m_DescriptorManager(descriptorManager)
, m_TextureLayout(textureLayout) , m_TextureLayout(textureLayout)
{ {
// 纯色 1x1 贴图(albedo 兜底) // 纯色贴图直供 albedo 槽(alpha 非全 1 时为半透明材质);
// 生命周期挂进纹理缓存(键含 rgba 值,同色共享、异色互不冲突)
InitDefaultTextures(); InitDefaultTextures();
auto tex = Texture::CreateSolidColor(device, allocator, color[0], color[1], color[2]); Scope<Texture> tex = (rgba[3] == 255)
? Texture::CreateSolidColor(device, allocator, rgba[0], rgba[1], rgba[2])
: Texture::CreateSolidColorRGBA(device, allocator,
rgba[0], rgba[1], rgba[2], rgba[3]);
Texture* albedoOverride = tex.get();
const std::string texKey = "proc:" + std::to_string(rgba[0]) + ',' +
std::to_string(rgba[1]) + ',' + std::to_string(rgba[2]) + ',' +
std::to_string(rgba[3]);
m_TextureCache.emplace(texKey, std::move(tex));
// XZ 平面,法线 +Y,从上往下看 CCW 绕序 // XZ 平面,法线 +Y,从上往下看 CCW 绕序
const f32 h = halfSize; const f32 h = halfSize;
@@ -81,13 +127,19 @@ Model::Model(Device& device, MemoryAllocator& allocator,
{{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}}, {{ h, 0.0f, -h}, {0, 1, 0}, {0.0f, 0.0f}},
}; };
const std::vector<u32> indices = {0, 1, 2, 0, 2, 3}; const std::vector<u32> indices = {0, 1, 2, 0, 2, 3};
const std::vector<std::vector<u32>> lodIndices = {indices};
// 材质/纹理生命周期随模型(缓存持有) // 材质/纹理生命周期随模型(缓存持有);颜色纹理必须显式覆盖 albedo
// 槽——缺了会折到默认白纹理(白板叠白地 → 平面“消失”的教训)
std::string paths[Material::kSlotCount] = {"", "", "", ""}; std::string paths[Material::kSlotCount] = {"", "", "", ""};
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}); // 全默认材质 std::array<Texture*, Material::kSlotCount> overrides{};
overrides[Material::Albedo] = albedoOverride;
Material* mat = GetOrCreateMaterial(paths, MaterialParams{}, transparent,
overrides);
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, vertices, indices, mat)); m_Device, m_Allocator, vertices, lodIndices, mat));
RebuildUnionBounds();
} }
Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator, Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
@@ -97,7 +149,20 @@ Scope<Model> Model::CreatePlane(Device& device, MemoryAllocator& allocator,
{ {
return Scope<Model>(new Model(device, allocator, descriptorManager, return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{}, textureLayout, ProceduralTag{},
halfSize, std::array<u8, 3>{r, g, b})); halfSize, std::array<u8, 4>{r, g, b, 255},
false));
}
Scope<Model> Model::CreateTransparentPlane(
Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout,
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Scope<Model>(new Model(device, allocator, descriptorManager,
textureLayout, ProceduralTag{},
halfSize, std::array<u8, 4>{r, g, b, a},
true));
} }
ParsedModel Model::ParseFromDisk(const std::string& path) ParsedModel Model::ParseFromDisk(const std::string& path)
@@ -125,20 +190,177 @@ ParsedModel Model::ParseFromDisk(const std::string& path)
(out.isGltf ? 0 : aiProcess_FlipUVs) | (out.isGltf ? 0 : aiProcess_FlipUVs) |
aiProcess_GenSmoothNormals | aiProcess_GenSmoothNormals |
aiProcess_CalcTangentSpace | // 法线贴图 TBN aiProcess_CalcTangentSpace | // 法线贴图 TBN
aiProcess_JoinIdenticalVertices); aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights); // 每顶点限 4 权重(= 顶点格式 boneIds 容量)
if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) {
if (!scene->mNumMeshes) {
ENGINE_WARN("Assimp warn: model dose not have mesh assets\nmodel has following asset:");
ENGINE_WARN("\tMeshes Count: {}", scene->mNumMeshes);
ENGINE_WARN("\tAnimations Count: {}", scene->mNumAnimations);
ENGINE_WARN("\tCameras Count: {}", scene->mNumCameras);
ENGINE_WARN("\tLights Count: {}", scene->mNumLights);
ENGINE_WARN("\tMaterials Count: {}", scene->mNumMaterials);
ENGINE_WARN("\tSkeletons Count: {}", scene->mNumSkeletons);
ENGINE_WARN("\tTextures Count: {}", scene->mNumTextures);
}
ENGINE_ERROR("Assimp error: {}", importer.GetErrorString()); ENGINE_ERROR("Assimp error: {}", importer.GetErrorString());
return out; // meshes 空 = 无效 return out; // meshes 空 = 无效
} }
// 骨骼动画三件套:node 表(global 累乘骨架)→ 动画轨道 → 骨骼注册
//(DFS 序;ProcessNode 的 mesh 遍历序与其独立,nodeIdx 靠 name 查表)
FlattenNodes(scene->mRootNode, -1, out.nodes);
ParseAnimations(scene, out.nodes, out.clips);
const std::filesystem::path modelDir = const std::filesystem::path modelDir =
std::filesystem::path(path).parent_path(); std::filesystem::path(path).parent_path();
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf); // 骨骼注册表(boneName → palette slot):多 mesh 共享骨架去重
ENGINE_INFO("Model parsed: {} ({} meshes)", path, out.meshes.size()); std::unordered_map<std::string, u32> boneSlots;
ProcessNode(scene->mRootNode, scene, out.meshes, modelDir, out.isGltf,
out.bones, boneSlots);
// 骨骼 nodeIdx 回填(注册发生在各 mesh 的 ProcessMesh 内,
// 这里按 node 表查 name 定位累乘路径)
if (!out.bones.empty()) {
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(out.nodes.size());
for (u32 i = 0; i < out.nodes.size(); ++i) {
nodeByName.emplace(out.nodes[i].name, i);
}
for (auto& bone : out.bones) {
if (auto it = nodeByName.find(bone.name); it != nodeByName.end()) {
bone.nodeIdx = it->second;
} else {
ENGINE_WARN("Bone '{}' has no matching node, palette entry static",
bone.name);
}
}
}
ENGINE_INFO("Model parsed: {} ({} meshes, {} bones, {} anims)",
path, out.meshes.size(), out.bones.size(), out.clips.size());
return out; return out;
} }
u32 Model::FlattenNodes(const aiNode* node, i32 parent,
std::vector<ModelNode>& out)
{
const u32 idx = static_cast<u32>(out.size());
ModelNode n;
n.name = node->mName.C_Str();
n.parent = parent;
// 完整 mTransformation:FBX 的 PreRotation/单位换算都折在这里,
// 骨骼 global 累乘必须用全量矩阵,不能拆 TRS 重建
n.localTransform = ToGlm(node->mTransformation);
out.push_back(std::move(n));
for (u32 i = 0; i < node->mNumChildren; ++i) {
FlattenNodes(node->mChildren[i], static_cast<i32>(idx), out);
}
return idx;
}
void Model::ParseAnimations(const aiScene* scene,
const std::vector<ModelNode>& nodes,
std::vector<AnimationClip>& out)
{
if (scene->mNumAnimations == 0) return;
// nodeName → node 表下标(channel 的目标必须能在层级里累乘)
std::unordered_map<std::string, u32> nodeByName;
nodeByName.reserve(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
nodeByName.emplace(nodes[i].name, i);
}
for (u32 a = 0; a < scene->mNumAnimations; ++a) {
const aiAnimation* anim = scene->mAnimations[a];
AnimationClip clip;
clip.name = anim->mName.length > 0 ? anim->mName.C_Str()
: ("clip_" + std::to_string(a));
// tps=0 按 1(assimp 对部分导出器的惯例缺省)
clip.tps = anim->mTicksPerSecond > 0.0
? static_cast<f32>(anim->mTicksPerSecond) : 1.0f;
clip.duration = static_cast<f32>(anim->mDuration) / clip.tps;
for (u32 c = 0; c < anim->mNumChannels; ++c) {
const aiNodeAnim* ch = anim->mChannels[c];
auto it = nodeByName.find(ch->mNodeName.C_Str());
if (it == nodeByName.end()) {
ENGINE_WARN("Anim '{}': channel node '{}' not in hierarchy, skipped",
clip.name, ch->mNodeName.C_Str());
continue;
}
AnimChannel chan;
chan.nodeIdx = it->second;
// 关键帧 time 统一换算成秒(运行时采样免除法)
chan.positions.reserve(ch->mNumPositionKeys);
for (u32 k = 0; k < ch->mNumPositionKeys; ++k) {
const auto& key = ch->mPositionKeys[k];
chan.positions.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
chan.rotations.reserve(ch->mNumRotationKeys);
for (u32 k = 0; k < ch->mNumRotationKeys; ++k) {
const auto& key = ch->mRotationKeys[k];
// aiQuaternion 存储序 (w,x,y,z) → glm 构造序一致
chan.rotations.push_back({
static_cast<f32>(key.mTime) / clip.tps,
glm::quat(key.mValue.w, key.mValue.x,
key.mValue.y, key.mValue.z)});
}
chan.scales.reserve(ch->mNumScalingKeys);
for (u32 k = 0; k < ch->mNumScalingKeys; ++k) {
const auto& key = ch->mScalingKeys[k];
chan.scales.push_back({
static_cast<f32>(key.mTime) / clip.tps,
{key.mValue.x, key.mValue.y, key.mValue.z}});
}
clip.channels.push_back(std::move(chan));
}
out.push_back(std::move(clip));
}
}
void Model::CollectMeshBones(const aiMesh* mesh,
std::vector<Vertex>& vertices,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots)
{
for (u32 b = 0; b < mesh->mNumBones; ++b) {
const aiBone* bone = mesh->mBones[b];
const char* name = bone->mName.C_Str();
u32 slot;
if (auto it = boneSlots.find(name); it != boneSlots.end()) {
slot = it->second; // 多 mesh 共享同一骨架(身体/附件分区)
} else {
slot = static_cast<u32>(bones.size());
boneSlots.emplace(name, slot);
// nodeIdx 由 ParseFromDisk 统一回填(node 表此时已就绪)
bones.push_back({name, 0, ToGlm(bone->mOffsetMatrix)});
}
// 顶点权重累加:LimitBoneWeights 保证每顶点 ≤4 根骨,逐槽找空位;
// 4 槽满的理论残留权重直接忽略(量化误差级)
for (u32 w = 0; w < bone->mNumWeights; ++w) {
const aiVertexWeight& vw = bone->mWeights[w];
if (vw.mVertexId >= vertices.size() || vw.mWeight <= 0.0f) continue;
Vertex& v = vertices[vw.mVertexId];
for (u32 k = 0; k < 4; ++k) {
if (v.weights[k] == 0.0f) {
v.boneIds[k] = slot;
v.weights[k] = vw.mWeight;
break;
}
}
}
}
}
Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator, Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptorManager, DescriptorManager& descriptorManager,
vk::DescriptorSetLayout textureLayout, vk::DescriptorSetLayout textureLayout,
@@ -153,15 +375,19 @@ Scope<Model> Model::FromParsed(Device& device, MemoryAllocator& allocator,
descriptorManager, textureLayout)); descriptorManager, textureLayout));
model->m_ModelPath = parsed.path; model->m_ModelPath = parsed.path;
model->m_IsGltf = parsed.isGltf; model->m_IsGltf = parsed.isGltf;
model->m_Nodes = std::move(parsed.nodes);
model->m_Bones = std::move(parsed.bones);
model->m_Clips = std::move(parsed.clips);
// 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程) // 材质缓存去重 + 纹理上传 + mesh 缓冲(全部 GPU,主线程)
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = model->GetOrCreateMaterial(pm.texPaths.data(), pm.params);
model->m_Meshes.push_back(CreateScope<Mesh>( model->m_Meshes.push_back(CreateScope<Mesh>(
device, allocator, std::move(pm.vertices), std::move(pm.indices), mat)); device, allocator, pm.vertices, pm.lods, mat));
} }
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
parsed.path, model->m_Meshes.size(), model->m_TextureCache.size()); parsed.path, model->m_Meshes.size(), model->m_TextureCache.size());
model->RebuildUnionBounds();
return model; return model;
} }
@@ -174,25 +400,39 @@ void Model::LoadModel(const std::string& path)
} }
m_ModelPath = parsed.path; m_ModelPath = parsed.path;
m_IsGltf = parsed.isGltf; m_IsGltf = parsed.isGltf;
m_Nodes = std::move(parsed.nodes);
m_Bones = std::move(parsed.bones);
m_Clips = std::move(parsed.clips);
for (auto& pm : parsed.meshes) { for (auto& pm : parsed.meshes) {
Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params); Material* mat = GetOrCreateMaterial(pm.texPaths.data(), pm.params);
m_Meshes.push_back(CreateScope<Mesh>( m_Meshes.push_back(CreateScope<Mesh>(
m_Device, m_Allocator, std::move(pm.vertices), std::move(pm.indices), mat)); m_Device, m_Allocator, pm.vertices, pm.lods, mat));
} }
ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)", ENGINE_INFO("Model loaded: {} ({} meshes, {} textures)",
path, m_Meshes.size(), m_TextureCache.size()); path, m_Meshes.size(), m_TextureCache.size());
RebuildUnionBounds();
} }
void Model::ProcessNode(const aiNode* node, const aiScene* scene, void Model::ProcessNode(const aiNode* node, const aiScene* scene,
std::vector<ParsedMesh>& out, std::vector<ParsedMesh>& out,
const std::filesystem::path& modelDir, bool isGltf) const std::filesystem::path& modelDir, bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& parentGlobal)
{ {
// 累乘节点链全局变换:Blender/Sketchfab 导出的 FBX(未勾 Apply
// Transform)把物体旋转(含 Z-up→Y-up)与 cm→m 单位换算全折在
// 节点变换上,忽略会让每个零件按原始局部空间乱画(朝向丢失、
// 尺度差百倍,车都不成车)
const aiMatrix4x4 global = parentGlobal * node->mTransformation;
for (u32 i = 0; i < node->mNumMeshes; ++i) { for (u32 i = 0; i < node->mNumMeshes; ++i) {
aiMesh* mesh = scene->mMeshes[node->mMeshes[i]]; aiMesh* mesh = scene->mMeshes[node->mMeshes[i]];
out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf)); out.push_back(ProcessMesh(mesh, scene, modelDir, isGltf,
bones, boneSlots, global));
} }
for (u32 i = 0; i < node->mNumChildren; ++i) { for (u32 i = 0; i < node->mNumChildren; ++i) {
ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf); ProcessNode(node->mChildren[i], scene, out, modelDir, isGltf,
bones, boneSlots, global);
} }
} }
@@ -222,21 +462,38 @@ Texture* Model::GetOrLoadTexture(const std::string& rawPath, bool srgb)
} }
Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount], Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCount],
const MaterialParams& params) const MaterialParams& params,
bool transparent,
const std::array<Texture*, Material::kSlotCount>&
slotOverrides)
{ {
const std::string key = Material::MakeCacheKey(paths, params); // 程序化覆盖纹理以指针地址入键:同模型内不同颜色的覆盖组合
//(如两块玻璃板)各得一份材质,不与纯路径键的材质串缓存
std::string key = Material::MakeCacheKey(paths, params, transparent);
bool hasOverride = false;
for (const Texture* t : slotOverrides) {
if (t) { hasOverride = true; break; }
}
if (hasOverride) {
key += "@ovr";
for (const Texture* t : slotOverrides) {
key += t ? ":" + std::to_string(reinterpret_cast<uintptr_t>(t))
: ":0";
}
}
if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) { if (auto it = m_MaterialCache.find(key); it != m_MaterialCache.end()) {
return it->second.get(); return it->second.get();
} }
// 每槽位解析纹理:空/缺失折到该槽位默认纹理 // 每槽位解析纹理:程序化覆盖优先,路径次之,空/缺失折到默认纹理
std::array<Texture*, Material::kSlotCount> textures{}; std::array<Texture*, Material::kSlotCount> textures{};
// 槽位 sRGB 语义:颜色图 true,数据图 false // 槽位 sRGB 语义:颜色图 true,数据图 false
const bool slotSrgb[Material::kSlotCount] = {true, false, false, true}; const bool slotSrgb[Material::kSlotCount] = {true, false, false, true};
for (u32 slot = 0; slot < Material::kSlotCount; ++slot) { for (u32 slot = 0; slot < Material::kSlotCount; ++slot) {
Texture* tex = nullptr; Texture* tex = slotOverrides[slot];
if (!paths[slot].empty()) { if (!tex && !paths[slot].empty()) {
tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]); tex = GetOrLoadTexture(paths[slot], slotSrgb[slot]);
} }
textures[slot] = tex ? tex : m_DefaultTextures[slot].get(); textures[slot] = tex ? tex : m_DefaultTextures[slot].get();
@@ -244,7 +501,7 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
auto mat = CreateScope<Material>( auto mat = CreateScope<Material>(
m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout, m_Device, m_Allocator, m_DescriptorManager, m_TextureLayout,
textures, params); textures, params, transparent);
Material* ptr = mat.get(); Material* ptr = mat.get();
m_MaterialCache.emplace(key, std::move(mat)); m_MaterialCache.emplace(key, std::move(mat));
return ptr; return ptr;
@@ -252,29 +509,47 @@ Material* Model::GetOrCreateMaterial(const std::string paths[Material::kSlotCoun
ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene, ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
const std::filesystem::path& modelDir, const std::filesystem::path& modelDir,
bool isGltf) bool isGltf,
std::vector<ModelBone>& bones,
std::unordered_map<std::string, u32>& boneSlots,
const aiMatrix4x4& globalTransform)
{ {
ParsedMesh out; ParsedMesh out;
std::vector<Vertex>& vertices = out.vertices; std::vector<Vertex>& vertices = out.vertices;
std::vector<u32>& indices = out.indices; std::vector<u32> indices;
vertices.reserve(mesh->mNumVertices); vertices.reserve(mesh->mNumVertices);
// 有骨骼的 mesh:顶点权重先清零再由 CollectMeshBones 灌入——
// 默认 w=(1,0,0,0) 的槽 0 若不清,palette[0] 的 identity 会以
// 权重 1 混进蒙皮矩阵(顶点永远钉在原点)
const bool hasBones = mesh->mNumBones > 0;
// 非蒙皮 mesh:烘焙节点链全局变换(旋转/单位换算折进顶点);
// 蒙皮 mesh 保持 mesh 空间——palette × offsetMatrix 的蒙皮
// 约定依赖原空间,烘焙会与动画路径打架
const bool bakeTransform = !hasBones;
const aiMatrix3x3 normalMat(globalTransform);
for (u32 i = 0; i < mesh->mNumVertices; ++i) { for (u32 i = 0; i < mesh->mNumVertices; ++i) {
Vertex vertex{}; Vertex vertex{};
if (bakeTransform) {
const aiVector3D p = globalTransform * mesh->mVertices[i];
vertex.position = {p.x, p.y, p.z};
} else {
vertex.position = { vertex.position = {
mesh->mVertices[i].x, mesh->mVertices[i].x,
mesh->mVertices[i].y, mesh->mVertices[i].y,
mesh->mVertices[i].z mesh->mVertices[i].z
}; };
}
if (mesh->HasNormals()) { if (mesh->HasNormals()) {
vertex.normal = { const aiVector3D n = bakeTransform
mesh->mNormals[i].x, ? normalMat * mesh->mNormals[i]
mesh->mNormals[i].y, : mesh->mNormals[i];
mesh->mNormals[i].z const glm::vec3 nn{n.x, n.y, n.z};
}; vertex.normal = bakeTransform ? glm::normalize(nn) : nn;
} }
if (mesh->mTextureCoords[0]) { if (mesh->mTextureCoords[0]) {
@@ -286,16 +561,23 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线) // 切线(法线贴图 TBN;无切线数据置零,shader 侧退回几何法线)
if (mesh->mTangents) { if (mesh->mTangents) {
vertex.tangent = { const aiVector3D t = bakeTransform
mesh->mTangents[i].x, ? normalMat * mesh->mTangents[i]
mesh->mTangents[i].y, : mesh->mTangents[i];
mesh->mTangents[i].z const glm::vec3 tt{t.x, t.y, t.z};
}; vertex.tangent = bakeTransform ? glm::normalize(tt) : tt;
}
if (hasBones) {
vertex.weights = glm::vec4(0.0f);
} }
vertices.push_back(vertex); vertices.push_back(vertex);
} }
// 骨骼权重灌顶点(无骨骼 mesh 顶点保持默认 idx0/w1 → identity 直通)
CollectMeshBones(mesh, vertices, bones, boneSlots);
for (u32 i = 0; i < mesh->mNumFaces; ++i) { for (u32 i = 0; i < mesh->mNumFaces; ++i) {
aiFace face = mesh->mFaces[i]; aiFace face = mesh->mFaces[i];
for (u32 j = 0; j < face.mNumIndices; ++j) { for (u32 j = 0; j < face.mNumIndices; ++j) {
@@ -313,16 +595,19 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/ // glTF2 贴图在 assimp 5.x 的落点:MR 合图 → METALNESS(15)/
// DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。 // DIFFUSE_ROUGHNESS(16)(同图双槽);baseColor → BASE_COLOR(12)。
// OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。 // OBJ:map_Ns → SHININESS(独立 roughness 图)、map_Ka 常绑 AO。
// 先到先得,数组顺序即优先级 // 先到先得,数组顺序即优先级。
// 注意:FBX 独立 metalness/roughness 是两张图——metal 落 MR 槽
//(灰度 R=G=B,shader 读 .b 当金属度恰好正确),roughness 落
// 独立 Roughness 槽(factors.z=1 启用),不再挤占 MR 槽的 .g
const std::pair<aiTextureType, u32> slots[] = { const std::pair<aiTextureType, u32> slots[] = {
{aiTextureType_DIFFUSE, Material::Albedo}, {aiTextureType_DIFFUSE, Material::Albedo},
{aiTextureType_BASE_COLOR, Material::Albedo}, {aiTextureType_BASE_COLOR, Material::Albedo},
{aiTextureType_NORMALS, Material::Normal}, {aiTextureType_NORMALS, Material::Normal},
{aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点 {aiTextureType_HEIGHT, Material::Normal}, // OBJ map_Bump 落点
{aiTextureType_METALNESS, Material::MetallicRoughness}, {aiTextureType_METALNESS, Material::MetallicRoughness},
{aiTextureType_DIFFUSE_ROUGHNESS, Material::MetallicRoughness}, {aiTextureType_DIFFUSE_ROUGHNESS, Material::Roughness},
{aiTextureType_SHININESS, Material::Roughness}, // OBJ map_Ns
{aiTextureType_UNKNOWN, Material::MetallicRoughness}, {aiTextureType_UNKNOWN, Material::MetallicRoughness},
{aiTextureType_SHININESS, Material::MetallicRoughness},
{aiTextureType_AMBIENT_OCCLUSION, Material::Ao}, {aiTextureType_AMBIENT_OCCLUSION, Material::Ao},
{aiTextureType_LIGHTMAP, Material::Ao}, {aiTextureType_LIGHTMAP, Material::Ao},
{aiTextureType_AMBIENT, Material::Ao}, {aiTextureType_AMBIENT, Material::Ao},
@@ -343,7 +628,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
// 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认 // 材质加载可见性:贴图槽缺失/路径解析错误会静默退化成默认
// 纹理(金属全没 → 塑料感),这里留下诊断锚点 // 纹理(金属全没 → 塑料感),这里留下诊断锚点
ENGINE_INFO("Material '{}': albedo={} normal={} MR={} emissive={} ao={}", ENGINE_INFO("Material '{}': albedo={} normal={} MR={} roughness={} "
"emissive={} ao={}",
material->GetName().C_Str(), material->GetName().C_Str(),
texPaths[Material::Albedo].empty() ? "<none>" texPaths[Material::Albedo].empty() ? "<none>"
: texPaths[Material::Albedo], : texPaths[Material::Albedo],
@@ -351,6 +637,8 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
: texPaths[Material::Normal], : texPaths[Material::Normal],
texPaths[Material::MetallicRoughness].empty() ? "<none>" texPaths[Material::MetallicRoughness].empty() ? "<none>"
: texPaths[Material::MetallicRoughness], : texPaths[Material::MetallicRoughness],
texPaths[Material::Roughness].empty() ? "<none>"
: texPaths[Material::Roughness],
texPaths[Material::Emissive].empty() ? "<none>" texPaths[Material::Emissive].empty() ? "<none>"
: texPaths[Material::Emissive], : texPaths[Material::Emissive],
texPaths[Material::Ao].empty() ? "<none>" texPaths[Material::Ao].empty() ? "<none>"
@@ -368,16 +656,33 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness); material->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
params.factors = glm::vec4(static_cast<f32>(metallic), params.factors = glm::vec4(static_cast<f32>(metallic),
static_cast<f32>(roughness), 0.0f, 0.0f); static_cast<f32>(roughness), 0.0f, 0.0f);
// glTF alphaMode:BLEND = 半透明(透明绘制队列 + 深度只测不写)。
// MASK(alpha cutout)暂按不透明——需要 alpha test,后续演进。
// OBJ/FBX 无此键 → 恒不透明(资产格式定义写死)
aiString alphaMode;
if (material->Get(AI_MATKEY_GLTF_ALPHAMODE, alphaMode) == AI_SUCCESS) {
out.transparent = (alphaMode == aiString("BLEND"));
}
} else { } else {
// FBX/OBJ 独立贴图工作流
const bool hasSepRough = !texPaths[Material::Roughness].empty();
f32 roughnessFactor = 1.0f; f32 roughnessFactor = 1.0f;
if (texPaths[Material::MetallicRoughness].empty()) { if (!hasSepRough && texPaths[Material::MetallicRoughness].empty()) {
// 无粗糙度贴图 → Blinn-Phong Ns 换算 R=sqrt(2/(Ns+2)),
// 补偿默认 MR 贴图 G=0.5 → 因子 = 2R
ai_real shininess = 10.0f; ai_real shininess = 10.0f;
material->Get(AI_MATKEY_SHININESS, shininess); material->Get(AI_MATKEY_SHININESS, shininess);
const f32 rough = const f32 rough =
std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f)); std::sqrt(2.0f / (static_cast<f32>(shininess) + 2.0f));
roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f); roughnessFactor = 2.0f * glm::clamp(rough, 0.05f, 1.0f);
} }
params.factors = glm::vec4(0.0f, roughnessFactor, 0.0f, 0.0f); // 金属度图落 MR 槽(灰度 .b 即金属度语义):
// 有图 metallic 因子 = 1,无图 = 0(OBJ specular 工作流无金属概念)
const f32 metallicFactor =
texPaths[Material::MetallicRoughness].empty() ? 0.0f : 1.0f;
params.factors = glm::vec4(metallicFactor, roughnessFactor,
hasSepRough ? 1.0f : 0.0f, 0.0f);
} }
} }
@@ -386,6 +691,33 @@ ParsedMesh Model::ProcessMesh(const aiMesh* mesh, const aiScene* scene,
out.texPaths[slot] = std::move(texPaths[slot]); out.texPaths[slot] = std::move(texPaths[slot]);
} }
out.params = params; out.params = params;
// out.transparent 已在 glTF 分支读取
// ---- LOD 链(解析期生成,worker 线程免费时间):----
// LOD0 = 原始;每级目标索引数减半,相对误差 1e-2。顶点共享,
// 简化只重排索引(meshopt_simplify 从 LOD0 单次起点逐级向下)
out.lods.push_back(std::move(indices));
for (u32 lod = 1; lod < kMeshLodCount; ++lod) {
const size_t target = out.lods[0].size() >> lod;
// 太小的网格不简化:简化收益低于误差风险,直接复用上一级
if (target < 96 || out.lods[0].size() < 192) {
out.lods.push_back(out.lods.back());
continue;
}
std::vector<u32> dest(out.lods[0].size());
float error = 0.0f;
const size_t count = meshopt_simplify(
dest.data(),
out.lods[0].data(), out.lods[0].size(),
&out.vertices[0].position.x, out.vertices.size(), sizeof(Vertex),
target, 1e-2f, 0, &error);
if (count == 0) {
out.lods.push_back(out.lods.back()); // 简化失败兜底
continue;
}
dest.resize(count);
out.lods.push_back(std::move(dest));
}
return out; return out;
} }
+211 -66
View File
@@ -1,8 +1,28 @@
// //
// Created by atdunbg on 2026/9/23. // Created by atdunbg on 2026/9/23.
// //
// ForwardPass:相机视角渲染到 HDR 颜色图(线性光照值)。
// 显示变换不在本 pass——HDRColor 经 Tonemap 归一后才写 Backbuffer
// (对齐 UE/Unity:SceneColor(HDR) → tonemapper → UI)。
//
// 透明管线对齐 UE/Unity 标准语义:
// - 不透明("Forward"):深度测试 + 写,无混合直写
// - 透明("ForwardTrans"):独立渲染实例,深度测试(对不透明)+
// 深度写入关闭,HDR/velocity/depth 全部 Load 承接不透明段结果,
// 预乘 alpha 混合(线性空间 over),远 → 近排序(SceneRenderer
// 侧按 batch 平均距离排)
//
// NVIDIA shader-object 实测注意(见 .trae/documents 性能档案):
// 早期实验称 depthWriteEnable=FALSE 有灾难病态(~5fps),但该实验
// 是在 frag 仍写 gl_FragDepth 的时期做的(写 gl_FragDepth 会禁用
// early-Z,归因很可能在它而非 FALSE 本身)。gl_FragDepth 已删除,
// 透明段现为 depthWrite=FALSE;若病态复现,先排查二者再下结论。
// 另:勿用 gl_FragDepth=far 模拟"只测不写"(eLess 下片元自身
// 永远测试失败,透明队列全灭)。
//
#include <Engine/Renderer/Passes/ForwardPass.h> #include <Engine/Renderer/Passes/ForwardPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Vertex.h> #include <Engine/Renderer/Vertex.h>
#include <Engine/Renderer/Mesh.h> #include <Engine/Renderer/Mesh.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
@@ -12,57 +32,43 @@
namespace Engine::Passes namespace Engine::Passes
{ {
void ForwardPass::Register(RG::RenderGraph& rg) namespace
{ {
rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) { // 直写方程:(ONE, ZERO) → dst = src,数学上等价于无混合
const vk::CommandBuffer cmd = ctx.cmd; vk::ColorBlendEquationEXT MakeDirectEquation()
{
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}
vk::ClearValue clearValue; constexpr vk::ColorComponentFlags kColorWriteMask =
clearValue.color = vk::ClearColorValue( vk::ColorComponentFlagBits::eR |
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f}); vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::RenderingAttachmentInfo colorAttachments[2]; vk::ColorComponentFlagBits::eA;
// HDR 附件由 Skybox pass 清屏并画背景,这里 Load 在其上合成几何 } // namespace
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样)
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
colorAttachments[1].setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
colorAttachments[0], colorAttachments[1]};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorArray)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
// 两段渲染共享的命令录制:shader/描述符绑定 + 几何/光栅/视口状态。
// translucent=true 时绑定真实深度写入的透明 frag 变体。
// 深度写与混合状态由各渲染实例作为初始态设置(见文件头注释)。
void ForwardPass::RecordSharedState(const RG::RenderGraphContext& ctx,
vk::CommandBuffer cmd,
bool translucent) const
{
// 绑定 shader object // 绑定 shader object
std::array<vk::ShaderStageFlagBits, 2> stages = { std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eVertex,
vk::ShaderStageFlagBits::eFragment vk::ShaderStageFlagBits::eFragment
}; };
std::array<vk::ShaderEXT, 2> shaders = {m_Gpu.forwardVert, m_Gpu.forwardFrag}; std::array<vk::ShaderEXT, 2> shaders = {
m_Gpu.forwardVert,
translucent ? m_Gpu.translucentFrag : m_Gpu.forwardFrag};
cmd.bindShadersEXT(stages, shaders); cmd.bindShadersEXT(stages, shaders);
// Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图 // Set 0:本帧槽位的 UBO;Set 2:本帧槽位的阴影图
@@ -73,11 +79,9 @@ namespace Engine::Passes
m_Gpu.forwardLayout, 2, m_Gpu.forwardLayout, 2,
m_Gpu.shadowSets[ctx.frame.frameSlot], {}); m_Gpu.shadowSets[ctx.frame.frameSlot], {});
// ---- 动态状态 ----
cmd.setRasterizerDiscardEnable(VK_FALSE); cmd.setRasterizerDiscardEnable(VK_FALSE);
cmd.setDepthTestEnable(VK_TRUE); cmd.setDepthTestEnable(VK_TRUE);
cmd.setDepthWriteEnable(VK_TRUE);
cmd.setDepthCompareOp(vk::CompareOp::eLess); cmd.setDepthCompareOp(vk::CompareOp::eLess);
cmd.setDepthBoundsTestEnable(VK_FALSE); cmd.setDepthBoundsTestEnable(VK_FALSE);
@@ -107,39 +111,75 @@ namespace Engine::Passes
vk::Rect2D scissor({0, 0}, ctx.extent); vk::Rect2D scissor({0, 0}, ctx.extent);
cmd.setScissorWithCount(scissor); cmd.setScissorWithCount(scissor);
}
vk::Bool32 blendEnable = VK_FALSE; void ForwardPass::Register(RG::RenderGraph& rg)
vk::ColorBlendEquationEXT blendEquation; {
blendEquation.setSrcColorBlendFactor(vk::BlendFactor::eOne) // ---- 不透明段:深度测试 + 写,无混合直写 ----
.setDstColorBlendFactor(vk::BlendFactor::eZero) rg.AddPass("Forward", [this](RG::RenderGraphContext& ctx) {
.setColorBlendOp(vk::BlendOp::eAdd) const vk::CommandBuffer cmd = ctx.cmd;
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
vk::ColorComponentFlags colorWriteMask = vk::ClearValue clearValue;
vk::ColorComponentFlagBits::eR | clearValue.color = vk::ClearColorValue(
vk::ColorComponentFlagBits::eG | std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA; vk::RenderingAttachmentInfo colorAttachments[2];
// HDR 附件由 Skybox pass 清屏并画背景,这里 Load 在其上合成几何
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity 附件:清 0 = 静止(背景无几何 → 历史原位采样)
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
colorAttachments[1].setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eClear)
// 透明段要 Load 本段写出的深度做测试,必须 store
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
colorAttachments[0], colorAttachments[1]};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorArray)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
RecordSharedState(ctx, cmd, false);
cmd.setDepthWriteEnable(VK_TRUE);
// 两个颜色附件(HDR + velocity)都是无混合直写 // 两个颜色附件(HDR + velocity)都是无混合直写
cmd.setColorBlendEnableEXT(0, cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{blendEnable, blendEnable}); std::array<vk::Bool32, 2>{VK_FALSE, VK_FALSE});
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEquationEXT(0, cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{blendEquation, blendEquation}); std::array<vk::ColorBlendEquationEXT, 2>{direct, direct});
cmd.setColorWriteMaskEXT(0, cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{colorWriteMask, colorWriteMask}); std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
// ---- 绘制:GPUDriven 按 mesh 分 batch(相机视锥剔除后)---- // 调试视图 Transparent:只看透明队列——跳过不透明绘制,
// 实例矩阵已由 Renderer 写入 ObjectBuffer(per-slot SSBO); // HDR 颜色 = 清屏色 + 透明段内容(Skybox 同步跳过背景)
// 同 mesh 的 N 个可见实体合并为一次 drawIndexed(instanceCount=N), if (m_Frame.debugView != Passes::kDebugTransparent) {
// draw 数 = 可见唯一 mesh 数,与实体数量解耦
for (const auto& batch : m_Frame.mainView.batches) { for (const auto& batch : m_Frame.mainView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1, m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {}); batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first); batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
} }
cmd.endRendering(); cmd.endRendering();
@@ -153,6 +193,111 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eFragment) vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead, .Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment); vk::ShaderStageFlagBits::eFragment);
// ---- 透明段:独立渲染实例,UE/Unity 标准语义 ----
// 深度测试对不透明深度(eLess)+ 深度写入关闭——玻璃之间
// 互相不遮挡(谁先画都留在画面上,先后只影响 over 混合的
// 颜色数学),交叉半透明不再整块消失。
// 对不透明深度的 RAW 由 depth 的 Write use 链保证排在
// 不透明段之后(WAW 边)。
// alpha 语义:frag 输出未预乘线性色 + 覆盖率 alpha,
// 预乘在 tonemap 段(rgb * a)完成 —— hard constraint。
rg.AddPass("ForwardTrans", [this](RG::RenderGraphContext& ctx) {
if (m_Frame.translucentView.batches.empty()) return;
const vk::CommandBuffer cmd = ctx.cmd;
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.1f, 0.1f, 0.1f, 0.0f});
vk::RenderingAttachmentInfo colorAttachments[2];
colorAttachments[0].setImageView(ctx.View(m_HDRColor))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
// velocity:Load 不透明段写出的运动向量,玻璃只覆盖自己的像素
vk::ClearValue velocityClear;
velocityClear.color = vk::ClearColorValue(std::array<float, 4>{0.f, 0.f, 0.f, 0.f});
colorAttachments[1].setImageView(ctx.View(m_Velocity))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(velocityClear);
vk::RenderingAttachmentInfo depthAttachment;
depthAttachment.setImageView(ctx.View(m_Depth))
// 深度只读测试源:Load 承接不透明段深度,
// 透明段 depthWrite=FALSE 不产生写入,
// storeOp DontCare(下帧 entry 重新 Clear)
.setImageLayout(vk::ImageLayout::eDepthAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad)
.setStoreOp(vk::AttachmentStoreOp::eDontCare)
.setClearValue(vk::ClearValue(
vk::ClearDepthStencilValue(1.0f, 0)));
std::array<vk::RenderingAttachmentInfo, 2> colorArray = {
colorAttachments[0], colorAttachments[1]};
vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent})
.setLayerCount(1)
.setColorAttachments(colorArray)
.setPDepthAttachment(&depthAttachment);
cmd.beginRendering(renderingInfo);
RecordSharedState(ctx, cmd, true);
// UE/Unity 语义:透明段只测不写深度。写深度会让"先画的玻璃"
// 剔掉后画但实际在它前面的玻璃(交叉半透明整块消失)。
// 早期 depthWrite=FALSE 引发 NVIDIA ~5fps 病态的实验是在
// frag 仍写 gl_FragDepth 的时期做的(禁 early-Z),归因存疑;
// gl_FragDepth 已删除,FALSE 若复现病态再另行隔离。
cmd.setDepthWriteEnable(VK_FALSE);
vk::ColorBlendEquationEXT premul;
premul.setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha)
.setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha)
.setColorBlendOp(vk::BlendOp::eAdd)
// alpha 穿透:玻璃只做场景内 rgb over(透出背景色),
// 不修改像素覆盖率——HDR 的 a 是"窗口透过率"(tonemap
// rgb *= a 预乘 + 透明窗口桌面合成),归背景专属。若让
// 玻璃 over 混入 a(旧写法),a<1 背景上的玻璃会被拖进
// 桌面混色链路 → 预乘拉黑
.setSrcAlphaBlendFactor(vk::BlendFactor::eZero)
.setDstAlphaBlendFactor(vk::BlendFactor::eOne)
.setAlphaBlendOp(vk::BlendOp::eAdd);
const vk::ColorBlendEquationEXT direct = MakeDirectEquation();
cmd.setColorBlendEnableEXT(0,
std::array<vk::Bool32, 2>{VK_TRUE, VK_FALSE});
cmd.setColorBlendEquationEXT(0,
std::array<vk::ColorBlendEquationEXT, 2>{premul, direct});
cmd.setColorWriteMaskEXT(0,
std::array<vk::ColorComponentFlags, 2>{kColorWriteMask, kColorWriteMask});
for (const auto& batch : m_Frame.translucentView.batches) {
cmd.bindDescriptorSets(vk::PipelineBindPoint::eGraphics,
m_Gpu.forwardLayout, 1,
batch.mesh->GetMaterial()->GetDescriptorSet(), {});
batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
}
cmd.endRendering();
})
.Write(m_HDRColor, RG::ResourceUse::ColorAttachment)
.Write(m_Velocity, RG::ResourceUse::ColorAttachment)
// depth 走写 use(WAW 边):保证透明段排在不透明段之后,
// barrier 覆盖透明段对不透明深度的测试读(Read-Write hazard
// 由 src=Write 的内存域保证可见性)
.Write(m_Depth, RG::ResourceUse::DepthAttachment)
.Read(m_ShadowMaps[0], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[1], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment)
.Read(m_ShadowMaps[2], RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
} }
} // namespace Engine::Passes } // namespace Engine::Passes
@@ -174,6 +174,32 @@ namespace Engine::Passes
cmd.setAlphaToCoverageEnableEXT(VK_FALSE); cmd.setAlphaToCoverageEnableEXT(VK_FALSE);
cmd.setAlphaToOneEnableEXT(VK_FALSE); cmd.setAlphaToOneEnableEXT(VK_FALSE);
// ---- 颜色混合必须显式直写 ----
// shader-object 路径混合是动态状态,会从上一个 pass(ForwardTrans
// 的 premul 混合)残留:tonemap 输出被二次混合 → rgb = color×a²
//(拖低 alpha 平方级变黑),且 alpha 被残留方程与 clear(a=1) 混成
// 恒 1 → 透明窗口永远不生效
constexpr vk::Bool32 kBlendOff = VK_FALSE;
constexpr vk::ColorComponentFlags kWriteAll =
vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB |
vk::ColorComponentFlagBits::eA;
// 直写方程:(ONE, ZERO) → dst = src(无混合),alpha 同样直写
static const vk::ColorBlendEquationEXT kDirect = [] {
vk::ColorBlendEquationEXT eq;
eq.setSrcColorBlendFactor(vk::BlendFactor::eOne)
.setDstColorBlendFactor(vk::BlendFactor::eZero)
.setColorBlendOp(vk::BlendOp::eAdd)
.setSrcAlphaBlendFactor(vk::BlendFactor::eOne)
.setDstAlphaBlendFactor(vk::BlendFactor::eZero)
.setAlphaBlendOp(vk::BlendOp::eAdd);
return eq;
}();
cmd.setColorBlendEnableEXT(0, kBlendOff);
cmd.setColorBlendEquationEXT(0, kDirect);
cmd.setColorWriteMaskEXT(0, kWriteAll);
vk::Viewport viewport(0.0f, 0.0f, vk::Viewport viewport(0.0f, 0.0f,
static_cast<float>(ctx.extent.width), static_cast<float>(ctx.extent.width),
static_cast<float>(ctx.extent.height), static_cast<float>(ctx.extent.height),
+22 -4
View File
@@ -12,14 +12,28 @@ namespace Engine::Passes
rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) { rg.AddPass("ImGui", [this](RG::RenderGraphContext& ctx) {
const vk::CommandBuffer cmd = ctx.cmd; const vk::CommandBuffer cmd = ctx.cmd;
// loadOp 按显示模式分支:Direct = blit 已铺满场景(Load 叠加);
// Viewport = 本 pass 是唯一写入者(Clear 为"桌面底色")
const bool direct = (m_Frame.displayMode == kDisplayDirect);
vk::ClearValue clearValue;
clearValue.color = vk::ClearColorValue(
std::array<float, 4>{0.04f, 0.045f, 0.06f, 1.0f});
vk::RenderingAttachmentInfo colorAttachment; vk::RenderingAttachmentInfo colorAttachment;
colorAttachment.setImageView(ctx.View(m_Backbuffer)) colorAttachment.setImageView(ctx.View(m_Backbuffer))
.setImageLayout(vk::ImageLayout::eColorAttachmentOptimal) .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
.setLoadOp(vk::AttachmentLoadOp::eLoad) // 关键:叠加 .setLoadOp(direct ? vk::AttachmentLoadOp::eLoad
.setStoreOp(vk::AttachmentStoreOp::eStore); : vk::AttachmentLoadOp::eClear)
.setStoreOp(vk::AttachmentStoreOp::eStore)
.setClearValue(clearValue);
vk::RenderingInfo renderingInfo; vk::RenderingInfo renderingInfo;
renderingInfo.setRenderArea({{0, 0}, ctx.extent}) // 渲染区域 = backbuffer 物理尺寸(窗口大小),不是 ctx.extent
//(= RG 目标尺寸 = 场景渲染分辨率)。Editor 视口模式下场景链
// 按面板尺寸渲染,而 backbuffer 是全窗口——若跟随目标尺寸,
// 整个 ImGui 会被裁剪到面板大小
renderingInfo.setRenderArea({{0, 0}, ctx.Extent(m_Backbuffer)})
.setLayerCount(1) .setLayerCount(1)
.setColorAttachments(colorAttachment); .setColorAttachments(colorAttachment);
@@ -30,7 +44,11 @@ namespace Engine::Passes
} }
cmd.endRendering(); cmd.endRendering();
}).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment); }).Write(m_Backbuffer, RG::ResourceUse::ColorAttachment)
// 场景纹理在本 pass 内被 ImGui::Image 的描述符采样:声明读让 RG
// 补 ColorAttachment→ShaderRead barrier,并推导 eSampled usage
.Read(m_PresentTarget, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment);
} }
} // namespace Engine::Passes } // namespace Engine::Passes
+92 -4
View File
@@ -4,8 +4,10 @@
#include <Engine/Renderer/Passes/SceneGpu.h> #include <Engine/Renderer/Passes/SceneGpu.h>
#include <Engine/Renderer/Material.h> #include <Engine/Renderer/Material.h>
#include <Engine/Renderer/Mesh.h> // IndirectGroup 取数(inline 经 Mesh)
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/Buffer.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
#include <Engine/Vulkan/ShaderManager.h> #include <Engine/Vulkan/ShaderManager.h>
#include <Engine/Vulkan/UniformBuffer.h> #include <Engine/Vulkan/UniformBuffer.h>
@@ -15,14 +17,18 @@
namespace Engine::Passes namespace Engine::Passes
{ {
SceneGpu::SceneGpu(Device& device, DescriptorManager& descriptors, SceneGpu::SceneGpu(Device& device, MemoryAllocator& allocator,
DescriptorManager& descriptors,
ShaderManager& shaders, ShaderManager& shaders,
UniformBuffer& ubo, UniformBuffer& objectBuffer, UniformBuffer& ubo, UniformBuffer& objectBuffer,
UniformBuffer& prevObjectBuffer) UniformBuffer& prevObjectBuffer,
: m_Device(device), m_Descriptors(descriptors) UniformBuffer& skinOffsetBuffer,
UniformBuffer& skinPaletteBuffer)
: m_Device(device), m_Allocator(allocator), m_Descriptors(descriptors)
{ {
// Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取) // Set 0: UBO + GPUDriven 实例数据(SSBO,vertex 读取)
// + 上一帧实例矩阵(TAA velocity,vertex 读取) // + 上一帧实例矩阵(TAA velocity,vertex 读取)
// + 骨骼蒙皮:SkinOffsets(实例→palette 基址)+ SkinPalettes(骨骼矩阵)
std::vector<DescriptorBinding> uboBindings = { std::vector<DescriptorBinding> uboBindings = {
{0, vk::DescriptorType::eUniformBuffer, {0, vk::DescriptorType::eUniformBuffer,
vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment}, vk::ShaderStageFlagBits::eVertex | vk::ShaderStageFlagBits::eFragment},
@@ -30,6 +36,10 @@ namespace Engine::Passes
vk::ShaderStageFlagBits::eVertex}, vk::ShaderStageFlagBits::eVertex},
{2, vk::DescriptorType::eStorageBuffer, {2, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex}, vk::ShaderStageFlagBits::eVertex},
{3, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
{4, vk::DescriptorType::eStorageBuffer,
vk::ShaderStageFlagBits::eVertex},
}; };
uboSetLayout = descriptors.CreateLayout(uboBindings); uboSetLayout = descriptors.CreateLayout(uboBindings);
@@ -81,6 +91,11 @@ namespace Engine::Passes
shaderDir + "simple_shader.frag.spv", shaderDir + "simple_shader.frag.spv",
vk::ShaderStageFlagBits::eFragment, vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0); {uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// 透明变体:同一套布局,frag 末尾写 gl_FragDepth=1.0
auto transFrag = shaders.CreateFromFile(
shaderDir + "simple_shader_translucent.frag.spv",
vk::ShaderStageFlagBits::eFragment,
{uboSetLayout, textureSetLayout, shadowSetLayout}, 0);
// shadow shader:depth-only,push = lightVP // shadow shader:depth-only,push = lightVP
auto shadowVertShader = shaders.CreateFromFile( auto shadowVertShader = shaders.CreateFromFile(
@@ -96,10 +111,12 @@ namespace Engine::Passes
forwardVert = vert.Handle; forwardVert = vert.Handle;
forwardFrag = frag.Handle; forwardFrag = frag.Handle;
translucentFrag = transFrag.Handle;
shadowVert = shadowVertShader.Handle; shadowVert = shadowVertShader.Handle;
shadowFrag = shadowFragShader.Handle; shadowFrag = shadowFragShader.Handle;
if (!forwardVert || !forwardFrag || !shadowVert || !shadowFrag) { if (!forwardVert || !forwardFrag || !translucentFrag ||
!shadowVert || !shadowFrag) {
ENGINE_FATAL("SceneGpu: failed to create shader objects"); ENGINE_FATAL("SceneGpu: failed to create shader objects");
} }
@@ -132,10 +149,34 @@ namespace Engine::Passes
descriptors.UpdateStorageBuffer( descriptors.UpdateStorageBuffer(
uboSets[slot], 2, prevObjectBuffer.Get(slot), uboSets[slot], 2, prevObjectBuffer.Get(slot),
prevObjectBuffer.GetSize()); prevObjectBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 3, skinOffsetBuffer.Get(slot),
skinOffsetBuffer.GetSize());
descriptors.UpdateStorageBuffer(
uboSets[slot], 4, skinPaletteBuffer.Get(slot),
skinPaletteBuffer.GetSize());
shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout); shadowSets[slot] = descriptors.AllocateDescriptorSet(shadowSetLayout);
} }
// shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成 // shadowSets 的图片绑定在 Compile 后由 BindShadowMap 完成
// ---- MDI per-slot ring:命令 + count(CPU 直写,host visible)----
// 布局 [main | casc0 | casc1 | casc2] × kMaxInstances 命令预算;
// 两者 usage 都要 eIndirectBuffer(count buffer 的 spec 要求)
const vk::DeviceSize cmdBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(IndirectDrawCmd);
const vk::DeviceSize cntBytes = vk::DeviceSize(kIndirectViewMain + 1 + kMaxCascades) *
kMaxInstances * sizeof(u32);
constexpr auto memProps = vk::MemoryPropertyFlagBits::eHostVisible |
vk::MemoryPropertyFlagBits::eHostCoherent;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
indirectCmdBuffers[slot] = CreateScope<Buffer>(
device, allocator, cmdBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
indirectCountBuffers[slot] = CreateScope<Buffer>(
device, allocator, cntBytes,
vk::BufferUsageFlagBits::eIndirectBuffer, memProps);
}
} }
SceneGpu::~SceneGpu() SceneGpu::~SceneGpu()
@@ -144,6 +185,7 @@ namespace Engine::Passes
if (forwardVert) vkDevice.destroyShaderEXT(forwardVert); if (forwardVert) vkDevice.destroyShaderEXT(forwardVert);
if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag); if (forwardFrag) vkDevice.destroyShaderEXT(forwardFrag);
if (translucentFrag) vkDevice.destroyShaderEXT(translucentFrag);
if (shadowVert) vkDevice.destroyShaderEXT(shadowVert); if (shadowVert) vkDevice.destroyShaderEXT(shadowVert);
if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag); if (shadowFrag) vkDevice.destroyShaderEXT(shadowFrag);
@@ -168,6 +210,52 @@ namespace Engine::Passes
} }
} }
// ---- MDI:per-slot 上传 + 组绘制 ----
void SceneGpu::UploadIndirect(u32 frameSlot, const FrameSceneData& frame)
{
auto uploadView = [&](u32 viewIdx, const CullResult& v) {
if (v.indirectCommands.empty()) return;
const vk::DeviceSize base =
vk::DeviceSize(viewIdx) * kMaxInstances;
indirectCmdBuffers[frameSlot]->Upload(
v.indirectCommands.data(),
v.indirectCommands.size() * sizeof(IndirectDrawCmd),
base * sizeof(IndirectDrawCmd));
// 每组一个 count,下标 = 组首命令下标(drawIndexedIndirectCount
// 从此读取实际执行条数——GPU 剔除切片接管时只换写入者)
for (const auto& g : v.indirectGroups) {
indirectCountBuffers[frameSlot]->Upload(
&g.commandCount, sizeof(u32),
(base + g.firstCommand) * sizeof(u32));
}
};
uploadView(kIndirectViewMain, frame.mainView);
for (u32 c = 0; c < kMaxCascades; ++c) {
uploadView(kIndirectViewMain + 1 + c, frame.shadowCascades[c]);
}
}
void SceneGpu::DrawIndirectGroup(vk::CommandBuffer cmd, u32 frameSlot,
u32 viewIndex, const IndirectGroup& g)
{
// 组内全部命令共享同一 megabuffer:bind 一次,逐 mesh 定位由
// 命令里的 firstIndex/vertexOffset 承担
cmd.bindVertexBuffers(0, g.mesh->GetVertexBuffer(), {vk::DeviceSize(0)});
cmd.bindIndexBuffer(g.mesh->GetIndexBuffer(), vk::DeviceSize(0),
vk::IndexType::eUint32);
const vk::DeviceSize base =
vk::DeviceSize(viewIndex) * kMaxInstances;
cmd.drawIndexedIndirectCount(
indirectCmdBuffers[frameSlot]->Get(),
base * sizeof(IndirectDrawCmd),
indirectCountBuffers[frameSlot]->Get(),
(base + g.firstCommand) * sizeof(u32),
g.commandCount, // maxDrawCount = CPU 打包的组大小
sizeof(IndirectDrawCmd));
}
void SceneGpu::BindEnvironment(const Cubemap& env) void SceneGpu::BindEnvironment(const Cubemap& env)
{ {
// IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。 // IBL 资源 view 全程稳定(不随 RG 重编译变化),绑一次即可。
+1 -1
View File
@@ -101,7 +101,7 @@ namespace Engine::Passes
&m_Frame.ubo.lightSpace[c]); &m_Frame.ubo.lightSpace[c]);
for (const auto& batch : m_Frame.shadowCascades[c].batches) { for (const auto& batch : m_Frame.shadowCascades[c].batches) {
batch.mesh->Draw(cmd, batch.count, batch.first); batch.mesh->Draw(cmd, batch.count, batch.first, batch.lod);
} }
cmd.endRendering(); cmd.endRendering();
+5 -1
View File
@@ -3,6 +3,7 @@
// //
#include <Engine/Renderer/Passes/SkyboxPass.h> #include <Engine/Renderer/Passes/SkyboxPass.h>
#include <Engine/Renderer/Passes/FullscreenGpu.h>
#include <Engine/Renderer/Cubemap.h> #include <Engine/Renderer/Cubemap.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
@@ -96,6 +97,8 @@ namespace Engine::Passes
const glm::vec4& cc = m_Frame.ClearColor; const glm::vec4& cc = m_Frame.ClearColor;
vk::ClearValue clear; vk::ClearValue clear;
// alpha 语义 = 窗口透过率(透明窗口与桌面合成;tonemap 段
// rgb *= a 预乘)。滑条由用户控制:a<1 = 背景透出桌面
clear.color = vk::ClearColorValue( clear.color = vk::ClearColorValue(
std::array<float, 4>{cc.r, cc.g, cc.b, cc.a}); std::array<float, 4>{cc.r, cc.g, cc.b, cc.a});
@@ -113,7 +116,8 @@ namespace Engine::Passes
cmd.beginRendering(renderingInfo); cmd.beginRendering(renderingInfo);
if (m_Env && m_Frame.DrawSkybox) { if (m_Env && m_Frame.DrawSkybox &&
m_Frame.debugView != Passes::kDebugTransparent) {
// 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写) // 无环境贴图:只清屏(Forward loadOp=Load 依赖这里先写)
const std::array<vk::ShaderStageFlagBits, 2> stages = { const std::array<vk::ShaderStageFlagBits, 2> stages = {
vk::ShaderStageFlagBits::eVertex, vk::ShaderStageFlagBits::eVertex,
+4 -4
View File
@@ -14,7 +14,7 @@ namespace Engine::Passes
FrameSceneData& frame, FrameSceneData& frame,
RG::TextureHandle hdrColor, RG::TextureHandle bloomA, RG::TextureHandle hdrColor, RG::TextureHandle bloomA,
RG::TextureHandle temporalDiff, RG::TextureHandle temporalDiff,
RG::TextureHandle backbuffer, RG::TextureHandle presentTarget,
RG::TextureHandle rawHdr, RG::TextureHandle rawHdr,
RG::TextureHandle velocity, RG::TextureHandle velocity,
RG::TextureHandle shadowMap0, RG::TextureHandle shadowMap0,
@@ -22,7 +22,7 @@ namespace Engine::Passes
RG::TextureHandle shadowMap2) RG::TextureHandle shadowMap2)
: m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame), : m_Gpu(gpu), m_Descriptors(descriptors), m_Params(params), m_Frame(frame),
m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff), m_HDR(hdrColor), m_A(bloomA), m_TemporalDiff(temporalDiff),
m_Backbuffer(backbuffer), m_Target(presentTarget),
m_RawHDR(rawHdr), m_Velocity(velocity), m_RawHDR(rawHdr), m_Velocity(velocity),
m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2} m_ShadowMaps{shadowMap0, shadowMap1, shadowMap2}
{ {
@@ -38,7 +38,7 @@ namespace Engine::Passes
{ {
rg.AddPass("Tonemap", rg.AddPass("Tonemap",
[this](RG::RenderGraphContext& ctx) { [this](RG::RenderGraphContext& ctx) {
m_Gpu.RecordFullscreen(ctx, ctx.View(m_Backbuffer), m_Gpu.RecordFullscreen(ctx, ctx.View(m_Target),
m_Gpu.tonemapFrag, m_Gpu.tonemapFrag,
m_Sets[ctx.frame.frameSlot], m_Sets[ctx.frame.frameSlot],
FullscreenPush{m_Params.exposure, FullscreenPush{m_Params.exposure,
@@ -49,7 +49,7 @@ namespace Engine::Passes
static_cast<f32>( static_cast<f32>(
m_Frame.debugView)}); m_Frame.debugView)});
}) })
.Write(m_Backbuffer, RG::ResourceUse::ColorAttachment) .Write(m_Target, RG::ResourceUse::ColorAttachment)
.Read(m_HDR, RG::ResourceUse::ShaderRead, .Read(m_HDR, RG::ResourceUse::ShaderRead,
vk::ShaderStageFlagBits::eFragment) vk::ShaderStageFlagBits::eFragment)
.Read(m_A, RG::ResourceUse::ShaderRead, .Read(m_A, RG::ResourceUse::ShaderRead,
+132 -24
View File
@@ -24,6 +24,7 @@
#include <Engine/Renderer/Passes/TemporalDiffPass.h> #include <Engine/Renderer/Passes/TemporalDiffPass.h>
#include <Engine/Renderer/Passes/ImGuiPass.h> #include <Engine/Renderer/Passes/ImGuiPass.h>
#include <Engine/Renderer/Passes/PresentPass.h> #include <Engine/Renderer/Passes/PresentPass.h>
#include <Engine/Renderer/Passes/BlitPass.h>
#include <Engine/Vulkan/Device.h> #include <Engine/Vulkan/Device.h>
#include <Engine/Vulkan/MemoryAllocator.h> #include <Engine/Vulkan/MemoryAllocator.h>
#include <Engine/Vulkan/DescriptorManager.h> #include <Engine/Vulkan/DescriptorManager.h>
@@ -34,6 +35,7 @@
#include <Engine/Vulkan/UniformBuffer.h> #include <Engine/Vulkan/UniformBuffer.h>
#include <Engine/Core/Window.h> #include <Engine/Core/Window.h>
#include <Engine/Core/Log.h> #include <Engine/Core/Log.h>
#include <Engine/ImGui/ImGuiLayer.h>
#include <Engine/ECS/Scene.h> #include <Engine/ECS/Scene.h>
#include <Engine/Assets/AssetManager.h> #include <Engine/Assets/AssetManager.h>
@@ -43,6 +45,7 @@
#include <array> #include <array>
#include <limits> #include <limits>
#include <string>
namespace Engine namespace Engine
{ {
@@ -67,11 +70,22 @@ namespace Engine
*m_Device, *m_Allocator, *m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight, sizeof(glm::mat4) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer); vk::BufferUsageFlagBits::eStorageBuffer);
// 骨骼蒙皮:offsets 与实例序平行(每实例一个 palette 基址);
// palette 每实体一个 slot(帧首 bump 分配),容量 kMaxSkinMatrices
m_SkinOffsetBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(u32) * kMaxInstances, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
m_SkinPaletteBuffer = CreateScope<UniformBuffer>(
*m_Device, *m_Allocator,
sizeof(glm::mat4) * kMaxSkinMatrices, kMaxFramesInFlight,
vk::BufferUsageFlagBits::eStorageBuffer);
// 3. pass 装配体:布局/shader/采样器/per-slot 描述符 // 3. pass 装配体:布局/shader/采样器/per-slot 描述符
m_SceneGpu = CreateScope<Passes::SceneGpu>( m_SceneGpu = CreateScope<Passes::SceneGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager, *m_Device, *m_Allocator, *m_DescriptorManager, *m_ShaderManager,
*m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer); *m_UniformBuffer, *m_ObjectBuffer, *m_PrevObjectBuffer,
*m_SkinOffsetBuffer, *m_SkinPaletteBuffer);
m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>( m_FullscreenGpu = CreateScope<Passes::FullscreenGpu>(
*m_Device, *m_DescriptorManager, *m_ShaderManager); *m_Device, *m_DescriptorManager, *m_ShaderManager);
@@ -186,9 +200,17 @@ namespace Engine
return m_SceneRenderer->Frame().bloomEnabled; return m_SceneRenderer->Frame().bloomEnabled;
} }
void Renderer::SetBloomEnabled(bool b) { m_SceneRenderer->SetBloomEnabled(b); } void Renderer::SetBloomEnabled(bool b) { m_SceneRenderer->SetBloomEnabled(b); }
bool Renderer::GetLodEnabled() const { return m_SceneRenderer->Frame().lodEnabled; }
void Renderer::SetLodEnabled(bool b) { m_SceneRenderer->SetLodEnabled(b); }
int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; } int Renderer::GetDebugView() const { return m_SceneRenderer->Frame().debugView; }
void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); } void Renderer::SetDebugView(int v) { m_SceneRenderer->SetDebugView(v); }
void Renderer::SetDisplayMode(int mode)
{
m_DesiredDisplayMode = mode;
m_SceneRenderer->SetDisplayMode(mode);
}
void Renderer::SetEnvironment(const Cubemap* env) void Renderer::SetEnvironment(const Cubemap* env)
{ {
if (!env) env = m_Environment.get(); if (!env) env = m_Environment.get();
@@ -208,9 +230,9 @@ namespace Engine
static const std::vector<const char*> kNames = { static const std::vector<const char*> kNames = {
"Final", "TemporalDiff", "HDR", "TAAColor", "Bloom", "Final", "TemporalDiff", "HDR", "TAAColor", "Bloom",
"Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2", "Velocity", "ShadowMap0", "ShadowMap1", "ShadowMap2",
"EnvCube", "Irradiance", "Prefilter", "BrdfLut", "EnvCube", "Irradiance", "Prefilter", "BrdfLut", "Transparent",
}; };
static_assert(int(Passes::kDebugViewCount) == 13, static_assert(int(Passes::kDebugViewCount) == 14,
"DebugView 枚举与名表同步"); "DebugView 枚举与名表同步");
return kNames; return kNames;
} }
@@ -225,6 +247,11 @@ namespace Engine
return m_SceneRenderer->Frame().instancesTruncated; return m_SceneRenderer->Frame().instancesTruncated;
} }
f32 Renderer::GetCollectMs() const
{
return m_SceneRenderer->GetCollectMs();
}
u32 Renderer::GetShadowInstanceCount() const u32 Renderer::GetShadowInstanceCount() const
{ {
return m_SceneRenderer->GetShadowInstanceCount(); return m_SceneRenderer->GetShadowInstanceCount();
@@ -237,9 +264,17 @@ namespace Engine
// ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)---- // ---- 资源声明(命名资源;pass 经句柄引用,数据流由 uses 决定)----
// 导入 swapchain 颜色附件 // 导入 swapchain 颜附件(物理尺寸 = 窗口,与场景渲染分辨率解耦)
m_Backbuffer = m_RenderGraph->ImportSwapchain( m_Backbuffer = m_RenderGraph->ImportSwapchain(
"Backbuffer", m_Swapchain->GetFormat()); "Backbuffer", m_Swapchain->GetFormat());
m_RenderGraph->SetImportedExtent(m_Backbuffer, m_Swapchain->GetExtent());
// 显示目标:tonemap 写这里(offscreen),ImGui::Image 采样后自由
// 摆放;backbuffer 只承载 ImGui。格式随 swapchain(Unorm:tonemap
// 自带 gamma 编码,采样 → ImGui 写回全程无二次编解码,零色偏)
m_PresentTarget = m_RenderGraph->CreateTexture(
"PresentTarget",
RG::TextureDesc{m_Swapchain->GetFormat()});
// 深度图:目标尺寸,格式由 Device 能力决定 // 深度图:目标尺寸,格式由 Device 能力决定
const vk::Format depthFormat = m_Device->FindSupportedFormat( const vk::Format depthFormat = m_Device->FindSupportedFormat(
@@ -310,10 +345,15 @@ namespace Engine
m_BloomB)); m_BloomB));
m_Passes.push_back(CreateScope<Passes::TonemapPass>( m_Passes.push_back(CreateScope<Passes::TonemapPass>(
*m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame, *m_FullscreenGpu, *m_DescriptorManager, *m_PostParams, frame,
m_TAAColor, m_BloomA, m_TemporalDiff, m_Backbuffer, m_HDRColor, m_TAAColor, m_BloomA, m_TemporalDiff, m_PresentTarget, m_HDRColor,
m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2])); m_Velocity, m_ShadowMaps[0], m_ShadowMaps[1], m_ShadowMaps[2]));
// Direct 模式适配器:PresentTarget → backbuffer(Viewport 模式空转)。
// 必须注册在 ImGui 之前:两者都写 backbuffer,WAW 边按声明序
// 连接(前 writer → 后 writer),反序会让 blit 盖掉 UI
m_Passes.push_back(CreateScope<Passes::BlitPass>(
frame, m_PresentTarget, m_Backbuffer));
m_Passes.push_back(CreateScope<Passes::ImGuiPass>( m_Passes.push_back(CreateScope<Passes::ImGuiPass>(
&m_ImGuiLayer, m_Backbuffer)); &m_ImGuiLayer, frame, m_Backbuffer, m_PresentTarget));
m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer)); m_Passes.push_back(CreateScope<Passes::PresentPass>(m_Backbuffer));
for (auto& pass : m_Passes) { for (auto& pass : m_Passes) {
@@ -323,29 +363,88 @@ namespace Engine
m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile m_RenderGraph->SetTargetExtent(m_Extent); // 触发首次 Compile
} }
void Renderer::SetVSync(bool vsync) // ---- 帧首同步:所有"会换物理资源/描述符"的意图统一在这里落定 ----
vk::Extent2D Renderer::DesiredRenderExtent() const
{ {
if (m_Swapchain->GetVSync() == vsync) return; // Editor 视口模式:渲染分辨率跟随 ImGui 面板(面板未上报/关闭时
m_Swapchain->SetVSync(vsync); // 回退 swapchain 尺寸);Direct 模式:全窗口
HandleResize(); // present mode 变化需要重建 swapchain return (m_DesiredDisplayMode == kDisplayViewport && m_DesiredViewport.width)
? m_DesiredViewport
: m_Swapchain->GetExtent();
} }
void Renderer::HandleResize() void Renderer::BeginFrameSync()
{
const bool vsyncChanged = (m_DesiredVSync != m_Swapchain->GetVSync());
const bool windowResized = m_Window.WasResized();
const vk::Extent2D want = DesiredRenderExtent();
const bool extentChanged =
(want.width != m_Extent.width || want.height != m_Extent.height);
if (!vsyncChanged && !windowResized && !extentChanged) return;
if (windowResized) m_Window.ResetResizedFlag();
// swapchain 重建仅由窗口 resize / vsync 切换触发;
// 面板拖拽只重编译 RG(swapchain 与渲染分辨率解耦)
HandleResize(windowResized || vsyncChanged);
}
// GPU 空闲 → (可选)重建 swapchain → RG 重编译到期望渲染分辨率 →
// 各 pass 重绑输入描述符 → 场景纹理重新注册到 ImGui。
// 调用点:BeginFrameSync(UI 构建前)与帧中止路径(acquire/present
// 失败)——都不存在"已构建 draw data 引用旧描述符"的窗口
void Renderer::HandleResize(bool recreateSwapchain)
{ {
m_Device->GetDevice().waitIdle(); m_Device->GetDevice().waitIdle();
if (recreateSwapchain) {
// 先把 vsync 意图落进 swapchain(Create 按 m_VSync 选 present
// mode 且从不自行修改它)——否则 vsyncChanged 每帧恒真,
// swapchain 被无限重建,present mode 也永远切不过去
m_Swapchain->SetVSync(m_DesiredVSync);
m_Swapchain->Recreate(m_Window); m_Swapchain->Recreate(m_Window);
m_Extent = m_Swapchain->GetExtent();
// image 数量可能变化 // image 数量可能变化
m_SyncManager = CreateScope<SyncManager>( m_SyncManager = CreateScope<SyncManager>(
*m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount()); *m_Device, kMaxFramesInFlight, m_Swapchain->GetImageCount());
// backbuffer 物理尺寸跟新窗口走(ImGui 渲染区域 = 全窗口)
m_RenderGraph->SetImportedExtent(m_Backbuffer,
m_Swapchain->GetExtent());
}
m_Extent = DesiredRenderExtent(); // 重建后取(swapchain 尺寸已新鲜)
// RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符 // RG 重编译:物理镜像按新尺寸轮换,各 pass 重绑输入描述符
m_RenderGraph->SetTargetExtent(m_Extent); m_RenderGraph->SetTargetExtent(m_Extent);
for (auto& pass : m_Passes) { for (auto& pass : m_Passes) {
pass->OnTargetsRebuilt(*m_RenderGraph); pass->OnTargetsRebuilt(*m_RenderGraph);
} }
RefreshSceneTextures();
}
// ---- 场景显示纹理(PresentTarget → ImGui::Image)----
void* Renderer::GetSceneTexture() const
{
// UI 构建段时 m_CurrentFrame 已指向本帧 Render 将使用的槽位
//(上一帧 Render 尾部推进),UI 与渲染同槽位
void* tex = nullptr;
if (m_ImGuiLayer) {
tex = (void*)(intptr_t)
m_ImGuiLayer->GetSceneTexture(m_CurrentFrame);
}
return tex;
}
// RG 物理镜像按帧槽位各一份:每槽位注册一个 ImGui 描述符。
// 首次编译后(SetImGuiLayer)与每次重编译后(HandleResize)调用
void Renderer::RefreshSceneTextures()
{
if (!m_ImGuiLayer || !m_PresentTarget.Valid()) return;
for (u32 slot = 0; slot < kMaxFramesInFlight; ++slot) {
m_ImGuiLayer->UpdateSceneTexture(
slot, m_RenderGraph->GetPhysicalView(m_PresentTarget, slot));
}
} }
void Renderer::ReadGpuTimes(u32 frameSlot) void Renderer::ReadGpuTimes(u32 frameSlot)
@@ -379,19 +478,16 @@ namespace Engine
} }
} }
void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets) void Renderer::Render(ECS::Scene& scene, Assets::AssetManager& assets, f32 dt)
{ {
ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp) ZoneScoped; // CPU 侧:UBO/SSBO 上传 + 录制 + 提交(GPU 耗时见 RG timestamp)
if (m_Window.WasResized()) { // resize 已在 BeginFrameSync(UI 构建前)处理,此处无需再查
m_Window.ResetResizedFlag();
HandleResize();
}
const u32 frameSlot = m_CurrentFrame; const u32 frameSlot = m_CurrentFrame;
const auto& vkDevice = m_Device->GetDevice(); const auto& vkDevice = m_Device->GetDevice();
// 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU) // 1. 场景 → 当帧渲染数据(相机/灯光/双视图剔除;纯 CPU,不碰 GPU)
if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent)) { if (!m_SceneRenderer->BuildFrame(scene, assets, m_Extent, dt)) {
return; // 无 primary 相机:跳帧 return; // 无 primary 相机:跳帧
} }
const FrameSceneData& frame = m_SceneRenderer->Frame(); const FrameSceneData& frame = m_SceneRenderer->Frame();
@@ -410,6 +506,18 @@ namespace Engine
frame.prevInstanceModels.size() * frame.prevInstanceModels.size() *
sizeof(glm::mat4)); sizeof(glm::mat4));
} }
// 蒙皮数据与实例矩阵同窗口落槽位 ring(fence 已等,写-读竞争关闭)
if (!frame.skinOffsets.empty()) {
m_SkinOffsetBuffer->Update(frameSlot, frame.skinOffsets.data(),
frame.skinOffsets.size() * sizeof(u32));
}
if (!frame.skinPalettes.empty()) {
m_SkinPaletteBuffer->Update(frameSlot, frame.skinPalettes.data(),
frame.skinPalettes.size() *
sizeof(glm::mat4));
}
// MDI 命令/count 同窗口落槽位 ring(fence 已等,写-读竞争关闭)
m_SceneGpu->UploadIndirect(frameSlot, frame);
// 3. acquire swapchain image // 3. acquire swapchain image
u32 imageIndex = 0; u32 imageIndex = 0;
@@ -420,13 +528,13 @@ namespace Engine
m_SyncManager->GetImageAvailable(frameSlot)); m_SyncManager->GetImageAvailable(frameSlot));
if (result.result == vk::Result::eErrorOutOfDateKHR) { if (result.result == vk::Result::eErrorOutOfDateKHR) {
HandleResize(); HandleResize(true); // 帧中止:无 draw data 引用旧资源,安全
return; return;
} }
imageIndex = result.value; imageIndex = result.value;
} }
catch (const vk::OutOfDateKHRError&) { catch (const vk::OutOfDateKHRError&) {
HandleResize(); HandleResize(true);
return; return;
} }
@@ -479,11 +587,11 @@ namespace Engine
try { try {
const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo); const auto result = m_Device->GetPresentQueue().presentKHR(presentInfo);
if (result == vk::Result::eSuboptimalKHR) { if (result == vk::Result::eSuboptimalKHR) {
HandleResize(); HandleResize(true);
} }
} }
catch (const vk::OutOfDateKHRError&) { catch (const vk::OutOfDateKHRError&) {
HandleResize(); HandleResize(true);
} }
m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight; m_CurrentFrame = (m_CurrentFrame + 1) % kMaxFramesInFlight;
+395 -25
View File
@@ -14,6 +14,10 @@
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <algorithm>
#include <numeric>
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
@@ -43,6 +47,121 @@ namespace Engine
} }
return r; return r;
} }
// ---- 骨骼动画采样(BuildFrame 第 2.5 节专用)----
// 静止 local TRS 分解(列主序 SRT 矩阵;假设无负缩放/剪切)。
// 被动画轨道触碰的节点用关键帧重建,未触碰的保持原矩阵——
// FBX PreRotation 折在 localTransform 里,拆 TRS 重建会丢
void DecomposeTRS(const glm::mat4& m, glm::vec3& pos, glm::quat& rot,
glm::vec3& scl)
{
pos = glm::vec3(m[3]);
scl = {glm::length(glm::vec3(m[0])),
glm::length(glm::vec3(m[1])),
glm::length(glm::vec3(m[2]))};
const glm::mat3 r(glm::vec3(m[0]) / scl.x,
glm::vec3(m[1]) / scl.y,
glm::vec3(m[2]) / scl.z);
rot = glm::quat_cast(r);
}
// 关键帧采样:前后键插值(位置/缩放 lerp,旋转 slerp)。
// key 数通常几十个,线性扫描对 cache 更友好
glm::vec3 SampleKeys(const std::vector<PosKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::vec3 SampleKeys(const std::vector<SclKey>& keys, f32 t,
const glm::vec3& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::mix(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
glm::quat SampleKeys(const std::vector<RotKey>& keys, f32 t,
const glm::quat& rest)
{
if (keys.empty()) return rest;
if (t <= keys.front().time) return keys.front().value;
for (size_t i = 0; i + 1 < keys.size(); ++i) {
if (t < keys[i + 1].time) {
const f32 a = keys[i].time, b = keys[i + 1].time;
return glm::slerp(keys[i].value, keys[i + 1].value,
b > a ? (t - a) / (b - a) : 0.0f);
}
}
return keys.back().value;
}
// 单 clip 在 time 的全节点局部 TRS(无轨道分量 = 静止分解值)。
// touched = 被任意轨道触碰的节点标记(ComposeGlobals 决定重建或原样)
void SampleLocalPose(const Model& model, const AnimationClip& clip,
f32 time, std::vector<glm::vec3>& pos,
std::vector<glm::quat>& rot,
std::vector<glm::vec3>& scl,
std::vector<u8>& touched)
{
const auto& nodes = model.GetNodes();
pos.resize(nodes.size());
rot.resize(nodes.size());
scl.resize(nodes.size());
touched.assign(nodes.size(), 0);
for (u32 i = 0; i < nodes.size(); ++i) {
DecomposeTRS(nodes[i].localTransform, pos[i], rot[i], scl[i]);
}
if (clip.duration <= 0.0f) return;
const f32 t = glm::mod(time, clip.duration);
for (const auto& ch : clip.channels) {
if (ch.nodeIdx >= nodes.size()) continue;
touched[ch.nodeIdx] = 1;
if (!ch.positions.empty())
pos[ch.nodeIdx] = SampleKeys(ch.positions, t, pos[ch.nodeIdx]);
if (!ch.rotations.empty())
rot[ch.nodeIdx] = SampleKeys(ch.rotations, t, rot[ch.nodeIdx]);
if (!ch.scales.empty())
scl[ch.nodeIdx] = SampleKeys(ch.scales, t, scl[ch.nodeIdx]);
}
}
// 局部 TRS → 全节点 global 矩阵(层级累乘;父序 DFS 保证父先算)
void ComposeGlobals(const Model& model, const std::vector<glm::vec3>& pos,
const std::vector<glm::quat>& rot,
const std::vector<glm::vec3>& scl,
const std::vector<u8>& touched,
std::vector<glm::mat4>& globals)
{
const auto& nodes = model.GetNodes();
globals.resize(nodes.size());
for (u32 i = 0; i < nodes.size(); ++i) {
const glm::mat4 local = touched[i]
? glm::translate(glm::mat4(1.0f), pos[i]) *
glm::mat4_cast(rot[i]) * glm::scale(glm::mat4(1.0f), scl[i])
: nodes[i].localTransform;
globals[i] = nodes[i].parent >= 0
? globals[static_cast<u32>(nodes[i].parent)] * local
: local;
}
}
} }
void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir) void SceneRenderer::ComputeCascades(const glm::vec3& shadowDir)
@@ -129,7 +248,7 @@ namespace Engine
} }
bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, bool SceneRenderer::BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets,
vk::Extent2D targetExtent) vk::Extent2D targetExtent, f32 dt)
{ {
ZoneScopedN("BuildFrame"); ZoneScopedN("BuildFrame");
const auto collectStart = std::chrono::steady_clock::now(); const auto collectStart = std::chrono::steady_clock::now();
@@ -174,7 +293,9 @@ namespace Engine
m_Frame.shadowsEnabled = m_ShadowsEnabled; m_Frame.shadowsEnabled = m_ShadowsEnabled;
m_Frame.taaEnabled = m_TAAEnabled; m_Frame.taaEnabled = m_TAAEnabled;
m_Frame.bloomEnabled = m_BloomEnabled; m_Frame.bloomEnabled = m_BloomEnabled;
m_Frame.lodEnabled = m_LodEnabled;
m_Frame.debugView = m_DebugView; m_Frame.debugView = m_DebugView;
m_Frame.displayMode = m_DisplayMode;
ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f; ubo.cascadeSplits.w = m_ShadowsEnabled ? 1.0f : 0.0f;
// 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass; // 天空盒:视图去平移(只留旋转)× 投影 → 逆矩阵,push 给 Skybox pass;
@@ -246,6 +367,107 @@ namespace Engine
ComputeCascades(shadowDir); ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f); ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f);
// ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走,
// 否则被剔除的实体回屏时动画跳变)----
// palette 槽 0 固定 identity:静态模型/无动画实例的蒙皮直通
m_Frame.skinPalettes.clear();
m_Frame.skinPalettes.push_back(glm::mat4(1.0f));
m_Frame.skinOffsets.clear();
m_Frame.skinPaletteTruncated = false;
m_EntitySkinBase.clear();
scene.Each<MeshRendererComponent, AnimationComponent>(
[&](Entity entity, MeshRendererComponent& mr, AnimationComponent& anim) {
const Model* model = assets.GetModel(mr.model);
if (!model || !model->HasAnimations()) return;
const auto& clips = model->GetClips();
// clip 下标合法性(模型资产可能已换)
if (anim.clipIndex < 0 ||
anim.clipIndex >= static_cast<int>(clips.size())) {
anim.clipIndex = 0;
anim.fadeFrom = -1;
if (clips.empty()) return;
}
if (anim.fadeFrom >= static_cast<int>(clips.size())) {
anim.fadeFrom = -1;
}
// ---- 播放头推进(wrap/clamp)----
if (anim.playing && anim.speed != 0.0f) {
const f32 d = dt * anim.speed;
anim.time += d;
if (anim.fadeFrom >= 0) anim.fadeFromTime += d;
}
const auto wrapTime = [&](f32 t, const AnimationClip& c) {
if (c.duration <= 0.0f) return 0.0f;
return anim.loop ? glm::mod(t, c.duration)
: glm::clamp(t, 0.0f, c.duration - 1e-3f);
};
const AnimationClip& cur = clips[anim.clipIndex];
anim.time = wrapTime(anim.time, cur);
const AnimationClip* src = nullptr;
f32 srcTime = 0.0f;
f32 wCur = 1.0f; // 当前 clip 权重(crossfade 0→1)
if (anim.fadeFrom >= 0) {
// 淡出计时(暂停时冻结,与播放头一致)
if (anim.playing) anim.fadeElapsed += dt;
const f32 f = glm::min(
anim.fadeElapsed / glm::max(anim.fadeDuration, 1e-4f),
1.0f);
wCur = f * f * (3.0f - 2.0f * f); // smoothstep
if (f >= 1.0f) {
anim.fadeFrom = -1; // 过渡完成
} else {
src = &clips[anim.fadeFrom];
anim.fadeFromTime = wrapTime(anim.fadeFromTime, *src);
srcTime = anim.fadeFromTime;
}
}
// ---- 采样 + 局部 TRS 层混合 → global → palette ----
SampleLocalPose(*model, cur, anim.time,
m_PosePosA, m_PoseRotA, m_PoseSclA, m_TouchedA);
if (src) {
SampleLocalPose(*model, *src, srcTime,
m_PosePosB, m_PoseRotB, m_PoseSclB,
m_TouchedB);
const size_t n = m_PosePosA.size();
for (size_t i = 0; i < n; ++i) {
// touched = 两层轨道的并集(重建集合)
m_TouchedA[i] = static_cast<u8>(
m_TouchedA[i] | m_TouchedB[i]);
m_PosePosA[i] = glm::mix(m_PosePosB[i], m_PosePosA[i], wCur);
m_PoseSclA[i] = glm::mix(m_PoseSclB[i], m_PoseSclA[i], wCur);
// 旋转必须 slerp(lerp 单位四元数会走样)
m_PoseRotA[i] = glm::slerp(m_PoseRotB[i], m_PoseRotA[i], wCur);
}
}
ComposeGlobals(*model, m_PosePosA, m_PoseRotA, m_PoseSclA,
m_TouchedA, m_Globals);
// palette bump 分配(按实体,不按模型去重——同模型两实体
// 可不同播放状态)。预算打满 → 该实体退回绑定姿势
const auto& bones = model->GetBones();
const u32 base = static_cast<u32>(m_Frame.skinPalettes.size());
if (base + bones.size() > kMaxSkinMatrices) {
m_Frame.skinPaletteTruncated = true;
if (!m_SkinTruncWarned) {
m_SkinTruncWarned = true;
ENGINE_WARN("GPUDriven: skin palette full ({}), "
"animated entities fall back to bind pose",
kMaxSkinMatrices);
}
return;
}
for (const auto& b : bones) {
const u32 ni = b.nodeIdx < m_Globals.size()
? b.nodeIdx : 0;
m_Frame.skinPalettes.push_back(m_Globals[ni] * b.invBind);
}
const u32 entKey = static_cast<u32>(entt::to_integral(entity));
m_EntitySkinBase[entKey] = base;
});
// ---- 3. GPUDriven:单趟收集 + 多视图剔除 ---- // ---- 3. GPUDriven:单趟收集 + 多视图剔除 ----
// 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN // 同作用域第二个 zone:ZoneScopedN 变量名固定会重声明,用 ZoneNamedN
ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true ); ZoneNamedN( ___tracy_collect, "GPUDriven Collect", true );
@@ -258,8 +480,10 @@ namespace Engine
// batch.first 拼接时加上段基址,保证段内区间连续。 // batch.first 拼接时加上段基址,保证段内区间连续。
m_Frame.instanceModels.clear(); m_Frame.instanceModels.clear();
m_Frame.prevInstanceModels.clear(); m_Frame.prevInstanceModels.clear();
m_Frame.skinOffsets.clear();
m_MainScratch.Clear(); m_MainScratch.Clear();
for (auto& s : m_CascScratch) s.Clear(); for (auto& s : m_CascScratch) s.Clear();
m_TranslucentScratch.Clear(); // 透明队列每帧重排批次,必须整帧重建
u32 totalInstances = 0; u32 totalInstances = 0;
bool instanceTruncated = false; bool instanceTruncated = false;
@@ -285,19 +509,57 @@ namespace Engine
} }
m_CurWorlds.emplace(entKey, t.World()); m_CurWorlds.emplace(entKey, t.World());
// palette 基址(第 2.5 节预采样;无动画实体 = identity 槽)
u32 skinBase = kIdentityPaletteSlot;
if (auto it = m_EntitySkinBase.find(entKey);
it != m_EntitySkinBase.end()) {
skinBase = it->second;
}
// 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放) // 实例世界缩放(矩阵列向量最大长度,包围球半径随缩放)
const f32 worldScale = glm::max( const f32 worldScale = glm::max(
glm::length(glm::vec3(t.World()[0])), glm::length(glm::vec3(t.World()[0])),
glm::max(glm::length(glm::vec3(t.World()[1])), glm::max(glm::length(glm::vec3(t.World()[1])),
glm::length(glm::vec3(t.World()[2])))); glm::length(glm::vec3(t.World()[2]))));
// ---- 实体级粗剔除(联合包围球):四视图全部看不到 →
// 跳过整个 mesh 循环(多 mesh 模型的大头优化)----
const Mesh::Bounds& mb = model->GetBounds();
const glm::vec3 modelCenter =
glm::vec3(t.World() * glm::vec4(mb.center, 1.0f));
const f32 modelRadius = mb.radius * worldScale;
const bool mainVis =
mainFrustum.IntersectsSphere(modelCenter, modelRadius);
bool cascVis[kMaxCascades];
bool anyVis = mainVis;
for (u32 c = 0; c < kMaxCascades; ++c) {
cascVis[c] =
cascFrustum[c].IntersectsSphere(modelCenter, modelRadius);
anyVis |= cascVis[c];
}
if (!anyVis) return;
for (auto& mesh : model->GetMeshes()) { for (auto& mesh : model->GetMeshes()) {
const auto& bounds = mesh->GetBounds(); const auto& bounds = mesh->GetBounds();
const glm::vec3 worldCenter = const glm::vec3 worldCenter =
glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f)); glm::vec3(t.World() * glm::vec4(bounds.center, 1.0f));
const f32 worldRadius = bounds.radius * worldScale; const f32 worldRadius = bounds.radius * worldScale;
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum) { // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离;
// 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0
const f32 dist =
glm::length(worldCenter - camTransform->GetPosition());
u32 mainLod = 0;
if (m_LodEnabled) {
if (dist >= worldRadius * 20.0f) {
mainLod = 2;
} else if (dist >= worldRadius * 10.0f) {
mainLod = 1;
}
}
auto tryAppend = [&](ViewScratch& s, const Frustum& frustum,
u32 lod) {
if (!frustum.IntersectsSphere(worldCenter, worldRadius)) { if (!frustum.IntersectsSphere(worldCenter, worldRadius)) {
return; return;
} }
@@ -305,30 +567,47 @@ namespace Engine
instanceTruncated = true; instanceTruncated = true;
return; return;
} }
// lookup 值 = batches 索引(此前误存 worlds 索引: // batch 查找 = 数组寻址(Mesh::GetSlot 槽位 + LOD 槽)
// 单实体时两序号恰好恒等,多实体共享 mesh 后分叉 // batchIdx 存储 +1 偏移(0 = 该槽位尚无 batch)
// → batches[worldsIdx] 越界)。batch.first(GPU const u32 slot = mesh->GetSlot();
// firstInstance 基址)才是 worlds 索引,语义别混 if (slot >= s.lookup.size()) {
const auto [it, inserted] = s.lookup.emplace( s.lookup.resize(slot + 1);
mesh.get(), static_cast<u32>(s.result.batches.size())); }
if (inserted) { auto& lodSlots = s.lookup[slot];
s.result.batches.push_back({mesh.get(), 0, 0}); u32 batchIdx = lodSlots[lod];
s.batchWorlds.emplace_back(); if (batchIdx == 0) {
s.batchPrevWorlds.emplace_back(); batchIdx = static_cast<u32>(s.result.batches.size()) + 1;
lodSlots[lod] = batchIdx;
s.result.batches.push_back({mesh.get(), 0, 0, lod});
s.batchMats.emplace_back();
s.batchDistSum.push_back(0.0f);
} }
// 实例归入自己的 batch(mesh 分组),保证同 batch // 实例归入自己的 batch(mesh 分组),保证同 batch
// 实例在扁平化后连续——firstInstance 语义的前提 // 实例在扁平化后连续——firstInstance 语义的前提
const u32 batchIdx = it->second; const u32 bi = batchIdx - 1;
s.result.batches[batchIdx].count++; s.result.batches[bi].count++;
s.batchWorlds[batchIdx].push_back(t.World()); s.batchMats[bi].push_back({t.World(), prevWorld, skinBase});
s.batchPrevWorlds[batchIdx].push_back(prevWorld); s.batchDistSum[bi] += dist; // 透明排序用
s.result.instanceCount++; s.result.instanceCount++;
++totalInstances; ++totalInstances;
}; };
tryAppend(m_MainScratch, mainFrustum); // 透明材质 → 独立队列(主视图,混合绘制 + 排序;
// 不进阴影——alpha 阴影复杂度高收益低,注释后续演进)
const bool isTransparentMesh =
mesh->GetMaterial()->IsTransparent();
ViewScratch& mainTarget = isTransparentMesh
? m_TranslucentScratch : m_MainScratch;
tryAppend(mainTarget, mainFrustum, mainLod);
if (!isTransparentMesh) {
for (u32 c = 0; c < kMaxCascades; ++c) { for (u32 c = 0; c < kMaxCascades; ++c) {
tryAppend(m_CascScratch[c], cascFrustum[c]); if (!cascVis[c]) continue; // 粗剔除已判不可见
// 级联降级:越远的级联覆盖范围越大,用越低的 LOD
//(阴影本来就是低频信息,误差可接受);开关关闭 = 恒 LOD0
tryAppend(m_CascScratch[c], cascFrustum[c],
m_LodEnabled ? std::min(c, kMeshLodCount - 1) : 0u);
}
} }
} }
}); });
@@ -343,13 +622,12 @@ namespace Engine
b.first = base; // 扁平化后的连续区间基址 b.first = base; // 扁平化后的连续区间基址
base += b.count; base += b.count;
dst.batches.push_back(b); dst.batches.push_back(b);
m_Frame.instanceModels.insert(m_Frame.instanceModels.end(), for (const auto& m : s.batchMats[i]) {
s.batchWorlds[i].begin(), m_Frame.instanceModels.push_back(m.cur);
s.batchWorlds[i].end()); m_Frame.prevInstanceModels.push_back(m.prev);
m_Frame.prevInstanceModels.insert( // 与实例序平行对齐(shader 按 gl_InstanceIndex 取)
m_Frame.prevInstanceModels.end(), m_Frame.skinOffsets.push_back(m.skinBase);
s.batchPrevWorlds[i].begin(), }
s.batchPrevWorlds[i].end());
} }
dst.instanceCount = s.result.instanceCount; dst.instanceCount = s.result.instanceCount;
}; };
@@ -358,6 +636,50 @@ namespace Engine
appendView(m_Frame.shadowCascades[c], m_CascScratch[c]); appendView(m_Frame.shadowCascades[c], m_CascScratch[c]);
} }
// ---- 4.5 MDI 打包(已下线:移动视角下存在几何消失缺陷,待隔离修复)----
// PackIndirect(m_Frame.mainView, true);
// for (u32 c = 0; c < kMaxCascades; ++c) {
// PackIndirect(m_Frame.shadowCascades[c], false);
// }
// ---- 5. 透明批次排序:平均距离降序(远 → 近),连同实例列表同步重排
{
auto& tb = m_TranslucentScratch.result.batches;
if (tb.size() > 1) {
std::vector<u32> order(tb.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&](u32 a, u32 b) {
return m_TranslucentScratch.batchDistSum[a] / tb[a].count >
m_TranslucentScratch.batchDistSum[b] / tb[b].count;
});
CullResult sorted;
sorted.instanceCount = m_TranslucentScratch.result.instanceCount;
std::vector<ViewScratch::InstanceMatrices> sortedMats;
std::vector<f32> sortedDist;
for (const u32 i : order) {
sorted.batches.push_back(tb[i]);
sortedDist.push_back(m_TranslucentScratch.batchDistSum[i]);
for (auto& m : m_TranslucentScratch.batchMats[i]) {
sortedMats.push_back(std::move(m));
}
}
m_TranslucentScratch.result = std::move(sorted);
m_TranslucentScratch.batchDistSum = std::move(sortedDist);
// batchMats 按新分组的 count 顺序切分重装(保留容量)
m_TranslucentScratch.batchMats.assign(
m_TranslucentScratch.result.batches.size(), {});
u32 offset = 0;
for (size_t i = 0; i < m_TranslucentScratch.result.batches.size(); ++i) {
const u32 n = m_TranslucentScratch.result.batches[i].count;
m_TranslucentScratch.batchMats[i].assign(
std::make_move_iterator(sortedMats.begin() + offset),
std::make_move_iterator(sortedMats.begin() + offset + n));
offset += n;
}
}
}
appendView(m_Frame.translucentView, m_TranslucentScratch);
// 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出) // 帧末滚动时域状态:cur → prev(每帧重建,销毁的实体自动清出)
m_PrevWorlds.swap(m_CurWorlds); m_PrevWorlds.swap(m_CurWorlds);
m_CurWorlds.clear(); m_CurWorlds.clear();
@@ -379,4 +701,52 @@ namespace Engine
return true; return true;
} }
void SceneRenderer::PackIndirect(CullResult& view, bool byMaterial)
{
view.indirectCommands.clear();
view.indirectGroups.clear();
if (view.batches.empty()) return;
// 组键排序:megabuffer(VB 句柄)→ 材质(可选)→ mesh 槽位。
// 同 Model 的全部 mesh 共享同一 megabuffer,材质是主分组轴;
// 槽位兜底保证排序稳定(组内顺序无语义,防逐帧抖动)
std::sort(view.batches.begin(), view.batches.end(),
[byMaterial](const InstanceBatch& a, const InstanceBatch& b) {
const u64 va = reinterpret_cast<u64>(static_cast<VkBuffer>(
a.mesh->GetVertexBuffer()));
const u64 vb = reinterpret_cast<u64>(static_cast<VkBuffer>(
b.mesh->GetVertexBuffer()));
if (va != vb) return va < vb;
if (byMaterial && a.mesh->GetMaterial() != b.mesh->GetMaterial()) {
return reinterpret_cast<u64>(a.mesh->GetMaterial()) <
reinterpret_cast<u64>(b.mesh->GetMaterial());
}
return a.mesh->GetSlot() < b.mesh->GetSlot();
});
for (const auto& b : view.batches) {
const vk::Buffer vb = b.mesh->GetVertexBuffer();
Material* mat = byMaterial ? b.mesh->GetMaterial() : nullptr;
bool newGroup = view.indirectGroups.empty();
if (!newGroup) {
const IndirectGroup& g = view.indirectGroups.back();
newGroup = g.mesh->GetVertexBuffer() != vb ||
g.material != mat;
}
if (newGroup) {
view.indirectGroups.push_back({b.mesh, mat,
static_cast<u32>(view.indirectCommands.size()), 0});
}
view.indirectCommands.push_back({
b.mesh->GetLodIndexCount(b.lod),
b.count,
b.mesh->GetGlobalFirstIndex(b.lod),
b.mesh->GetVertexOffset(),
b.first});
++view.indirectGroups.back().commandCount;
}
}
} // namespace Engine } // namespace Engine
@@ -29,6 +29,14 @@ namespace Engine {
m_TextureLayout, halfSize, r, g, b); m_TextureLayout, halfSize, r, g, b);
} }
Scope<Model> VulkanAssetUploader::CreateTransparentPlane(
f32 halfSize, u8 r, u8 g, u8 b, u8 a)
{
return Model::CreateTransparentPlane(m_Device, m_Allocator,
m_Descriptors, m_TextureLayout,
halfSize, r, g, b, a);
}
Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path) Scope<Cubemap> VulkanAssetUploader::LoadEnvironment(const std::string& path)
{ {
// compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT // compute 预积分(毫秒级):HDR → cube + irradiance/prefilter/LUT
+18 -2
View File
@@ -166,6 +166,11 @@ void Device::PickPhysicalDevice()
m_DynamicRenderingSupported = features13.dynamicRendering; m_DynamicRenderingSupported = features13.dynamicRendering;
m_Synchronization2Supported = features13.synchronization2; m_Synchronization2Supported = features13.synchronization2;
// core 特性:MDI(drawCount>1)必需
m_MultiDrawIndirectSupported = features2.features.multiDrawIndirect == VK_TRUE;
// 1.2 特性:drawIndexedIndirectCount 必需(VUID-04445)
m_DrawIndirectCountSupported = features12.drawIndirectCount == VK_TRUE;
// 1.4 的新特性 // 1.4 的新特性
bool localReadSupported = features14.dynamicRenderingLocalRead; bool localReadSupported = features14.dynamicRenderingLocalRead;
bool pushDescriptorSupported = features14.pushDescriptor; bool pushDescriptorSupported = features14.pushDescriptor;
@@ -183,6 +188,10 @@ void Device::PickPhysicalDevice()
ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No"); ENGINE_INFO(" Dynamic Rendering Local Read (1.4): {}", localReadSupported ? "Yes" : "No");
ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No"); ENGINE_INFO(" Push Descriptor (1.4): {}", pushDescriptorSupported ? "Yes" : "No");
ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No"); ENGINE_INFO(" Shader Object (EXT): {}", m_ShaderObjectSupported ? "Yes" : "No");
ENGINE_INFO(" Multi Draw Indirect (1.0 core): {}",
m_MultiDrawIndirectSupported ? "Yes" : "No");
ENGINE_INFO(" Draw Indirect Count (1.2): {}",
m_DrawIndirectCountSupported ? "Yes" : "No");
} }
bool Device::IsDeviceSuitable(vk::PhysicalDevice device) bool Device::IsDeviceSuitable(vk::PhysicalDevice device)
@@ -244,6 +253,8 @@ void Device::CreateLogicalDevice()
} }
vk::PhysicalDeviceFeatures features; vk::PhysicalDeviceFeatures features;
// MDI:按材质分组的 drawIndexedIndirectCount,drawCount>1 必需
features.setMultiDrawIndirect(m_MultiDrawIndirectSupported ? VK_TRUE : VK_FALSE);
// ======================================================== // ========================================================
// 启用 1.4 特性链 // 启用 1.4 特性链
@@ -254,23 +265,28 @@ void Device::CreateLogicalDevice()
vk::PhysicalDeviceVulkan14Features features14{}; vk::PhysicalDeviceVulkan14Features features14{};
vk::PhysicalDeviceVulkan13Features features13{}; vk::PhysicalDeviceVulkan13Features features13{};
vk::PhysicalDeviceVulkan12Features features12{};
// 启用 1.3 的特性 // 启用 1.3 的特性
features13.dynamicRendering = VK_TRUE; features13.dynamicRendering = VK_TRUE;
features13.synchronization2 = VK_TRUE; features13.synchronization2 = VK_TRUE;
// 启用 1.2 的特性:MDI 的 drawIndexedIndirectCount(VUID-04445)
features12.drawIndirectCount = m_DrawIndirectCountSupported ? VK_TRUE : VK_FALSE;
// 启用 1.4 的新特性 // 启用 1.4 的新特性
features14.dynamicRenderingLocalRead = VK_TRUE; features14.dynamicRenderingLocalRead = VK_TRUE;
features14.pushDescriptor = VK_TRUE; features14.pushDescriptor = VK_TRUE;
// 链接:1.4 → 1.3 → (shader object) // 链接:1.4 → 1.3 → 1.2 → (shader object)
features14.pNext = &features13; features14.pNext = &features13;
features13.pNext = &features12;
// Shader Object 特性(如果支持) // Shader Object 特性(如果支持)
vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{}; vk::PhysicalDeviceShaderObjectFeaturesEXT shaderObjFeatures{};
if (m_ShaderObjectSupported) { if (m_ShaderObjectSupported) {
shaderObjFeatures.shaderObject = VK_TRUE; shaderObjFeatures.shaderObject = VK_TRUE;
features13.pNext = &shaderObjFeatures; features12.pNext = &shaderObjFeatures;
} }
// 扩展列表 // 扩展列表
+4 -1
View File
@@ -72,7 +72,10 @@ namespace Engine
.setImageColorSpace(surfaceFormat.colorSpace) .setImageColorSpace(surfaceFormat.colorSpace)
.setImageExtent(extent) .setImageExtent(extent)
.setImageArrayLayers(1) .setImageArrayLayers(1)
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) // COLOR_ATTACHMENT:ImGui 后端渲染;TRANSFER_DST:
// Direct 模式 BlitPass 直拷场景到 backbuffer
.setImageUsage(vk::ImageUsageFlagBits::eColorAttachment |
vk::ImageUsageFlagBits::eTransferDst)
.setImageSharingMode(vk::SharingMode::eExclusive) .setImageSharingMode(vk::SharingMode::eExclusive)
.setPreTransform(caps.currentTransform) .setPreTransform(caps.currentTransform)
.setCompositeAlpha(compositeAlpha) .setCompositeAlpha(compositeAlpha)
+7
View File
@@ -64,6 +64,13 @@ namespace Engine
{r, g, b, 255}, srgb)); {r, g, b, 255}, srgb));
} }
Scope<Texture> Texture::CreateSolidColorRGBA(Device& device, MemoryAllocator& allocator,
u8 r, u8 g, u8 b, u8 a, bool srgb)
{
return Scope<Texture>(new Texture(device, allocator, ProceduralTag{},
{r, g, b, a}, srgb));
}
void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb) void Texture::InitFromPixels(const void* pixels, u32 width, u32 height, bool srgb)
{ {
m_Width = width; m_Width = width;
+15
View File
@@ -0,0 +1,15 @@
Standard: Cpp03
UseTab: ForIndentation
TabWidth: 4
IndentWidth: 4
AccessModifierOffset: -4
BreakBeforeBraces: Allman
IndentCaseLabels: false
ColumnLimit: 0
PointerAlignment: Left
BreakConstructorInitializersBeforeComma: true
NamespaceIndentation: None
AlignEscapedNewlines: DontAlign
AlignAfterOpenBracket: DontAlign
IndentExternBlock: NoIndent
Macros: [MESHOPTIMIZER_ALLOC_CALLCONV=&_&]
+11
View File
@@ -0,0 +1,11 @@
# See https://editorconfig.org/ for more info
[*]
charset = utf-8
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
indent_style = space
+17
View File
@@ -0,0 +1,17 @@
# This file contains a list of Git commit hashes that should be hidden from the
# regular Git history. Typically, this includes commits involving mass auto-formatting
# or other normalizations. Commit hashes *must* use the full 40-character notation.
# To apply the ignore list in your local Git client, you must run:
#
# git config blame.ignoreRevsFile .git-blame-ignore-revs
#
# This file is automatically used by GitHub.com's blame view.
# Convert CRLF to LF everywhere
bb4aa0e1372751b74425e77c9a42f972971568bf
# js: Reformat all sources with Prettier
3dea31b5c248594a62f49a3e41fc88d7ceae2de3
# Reformat workflow YAML files with Prettier
52e8e1f61712928a36b5257a894bb098a4a98b22
@@ -0,0 +1,8 @@
---
name: Bug report
about: Create a report if you've found a bug in this project; please use GitHub Discussions instead if you are uncertain or the bug may be in your code.
title: ''
labels: bug
assignees: ''
---
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Questions and ideas
url: https://github.com/zeux/meshoptimizer/discussions
about: Please use GitHub Discussions if you have questions or ideas to discuss.
@@ -0,0 +1,8 @@
---
name: Feature request
about: Suggest a feature for this project; please use GitHub Discussions if your idea is not sufficiently concrete.
title: ''
labels: enhancement
assignees: ''
---
+11
View File
@@ -0,0 +1,11 @@
comment: false
coverage:
status:
project: off
patch: off
ignore:
- demo
- extern
- tools
+240
View File
@@ -0,0 +1,240 @@
name: build
on:
push:
branches:
- 'master'
paths-ignore:
- '*.md'
pull_request:
paths-ignore:
- '*.md'
jobs:
unix:
strategy:
matrix:
os:
[
{ name: ubuntu, version: ubuntu-latest },
{ name: ubuntu-arm, version: ubuntu-24.04-arm },
{ name: ubuntu-clang, version: ubuntu-latest },
{ name: macos, version: macos-latest },
]
name: ${{matrix.os.name}}
runs-on: ${{matrix.os.version}}
env:
werror: 1
steps:
- uses: actions/checkout@v6
- name: work around ASLR+ASAN compatibility
run: sudo sysctl -w vm.mmap_rnd_bits=28
if: matrix.os.name == 'ubuntu'
- name: enable clang
run: echo CXX=clang++ >Makefile.config
if: matrix.os.name == 'ubuntu-clang'
- name: make test
run: |
make -j2 config=sanitize test
make -j2 config=debug test
make -j2 config=release test
make -j2 config=coverage test
make -j2 config=coverage-scalar test
- name: make gltfpack
run: make -j2 config=release gltfpack
- name: upload coverage
run: |
find . -type f -name '*.gcno' -exec gcov -p {} +
sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov
bash <(curl -s https://codecov.io/bash) -f './src*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}}
if: matrix.os.name != 'ubuntu-clang'
windows:
strategy:
matrix:
arch:
[
{ name: windows, runner: windows-latest, arch: x64 },
{ name: windows-x86, runner: windows-latest, arch: Win32 },
{ name: windows-arm, runner: windows-11-arm, arch: ARM64 },
]
name: ${{matrix.arch.name}}
runs-on: ${{matrix.arch.runner}}
steps:
- uses: actions/checkout@v6
- name: cmake configure
run: cmake . -DMESHOPT_BUILD_DEMO=ON -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_WERROR=ON -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$<CONFIG:Debug>:Debug>" -A ${{matrix.arch.arch}}
- name: cmake test
shell: bash # necessary for fail-fast
run: |
cmake --build . -- -property:Configuration=Debug -verbosity:minimal
Debug/meshoptdemo.exe demo/pirate.obj
cmake --build . -- -property:Configuration=Release -verbosity:minimal
Release/meshoptdemo.exe demo/pirate.obj
nodejs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '16'
- name: test decoder
run: node js/meshopt_decoder.test.js
- name: test simd decoder
run: node --experimental-wasm-simd js/meshopt_decoder.test.js
- name: test encoder
run: node js/meshopt_encoder.test.js
- name: test simplifier
run: node js/meshopt_simplifier.test.js
- name: test clusterizer
run: node js/meshopt_clusterizer.test.js
- name: test tangents
run: node js/meshopt_tangents.test.js
- name: check typescript declarations
run: npx --yes -p typescript@6 tsc --noEmit --moduleResolution node16 --module node16 --strict js/index.d.ts
- name: check es5
run: |
npm install -g es-check@7.2.1
npx es-check --module es5 js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js
npx es-check --module es2020 gltf/library.js
gltfpack:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
repository: KhronosGroup/glTF-Sample-Assets
path: glTF-Sample-Assets
- name: work around ASLR+ASAN compatibility
run: sudo sysctl -w vm.mmap_rnd_bits=28
- name: make
run: make -j2 config=sanitize gltfpack
- name: test
run: find glTF-Sample-Assets -name '*.gltf' -or -name '*.glb' | xargs -P 2 -L 16 -d '\n' ./gltfpack -cc -test
- name: pack
run: find glTF-Sample-Assets -name '*.gltf' | grep -v '\-Draco/' | xargs -P 2 -L 16 -d '\n' -I '{}' ./gltfpack -i '{}' -o '{}pack.gltf'
- name: validate
run: |
curl -sL $VALIDATOR | tar xJ
find glTF-Sample-Assets -name '*.gltfpack.gltf' | xargs -P 2 -L 1 -d '\n' ./gltf_validator -r -a
env:
VALIDATOR: https://github.com/KhronosGroup/glTF-Validator/releases/download/2.0.0-dev.3.10/gltf_validator-2.0.0-dev.3.10-linux64.tar.xz
gltfpack-js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '18'
- name: install wasi
run: |
curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-x86_64-linux.deb > wasi-sdk.deb
sudo dpkg -i wasi-sdk.deb
env:
VERSION: 32
- name: build
run: |
make -j2 -B gltf/library.wasm js
git status
- name: test
run: |
node gltf/cli.js -i demo/pirate.obj -o pirate.glb -v
node gltf/cli.js -i `pwd`/pirate.glb -o pirate-repack.glb -cc -v
wc -c pirate.glb pirate-repack.glb
node js/meshopt_decoder.test.js
node js/meshopt_encoder.test.js
node js/meshopt_simplifier.test.js
node js/meshopt_clusterizer.test.js
node js/meshopt_tangents.test.js
gltfpack-full:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
repository: zeux/basis_universal
ref: gltfpack
path: basis_universal
- uses: actions/checkout@v6
with:
repository: webmproject/libwebp
ref: 1.6.0
path: libwebp
- name: cmake configure
run: cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -DMESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp
- name: cmake build
run: cmake --build . --target gltfpack -j 4
gltfpack-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
repository: zeux/basis_universal
ref: gltfpack
path: basis_universal
- uses: actions/checkout@v6
with:
repository: KhronosGroup/glTF-Sample-Assets
path: glTF-Sample-Assets
- name: build basisu
run: |
cd basis_universal
cmake . -D CMAKE_BUILD_TYPE=Debug
make -j4 basisu_encoder
- name: make
run: make -j2 config=coverage BASISU=basis_universal gltfpack
- name: test
run: |
find glTF-Sample-Assets -name '*.gltf' -or -name '*.glb' | xargs -P 2 -L 16 -d '\n' ./gltfpack -cc -test
./gltfpack -test demo/pirate.obj -si 0.5
./gltfpack -test demo/pirate.obj -si 0.5 -slb -se 0.02
./gltfpack -test demo/pirate.obj -si 0.1 -sa
./gltfpack -test demo/pirate.obj -noq
./gltfpack -test demo/pirate.obj -vp 16 -vt 16 -vn 16 -vc 16
./gltfpack -test demo/pirate.obj -vpf -vtf -vnf
./gltfpack -test demo/pirate.obj -vi -c
./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -mi -c
./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -kn -km -ke
./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -si 0.1 -sp
./gltfpack -test glTF-Sample-Assets/Models/ABeautifulGame/glTF/ABeautifulGame.gltf -si 0.1 -sv
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -vpf -vtf -c
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -vpf -vtf -cc
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc -tq color 10 -tu normal,attrib -ts attrib 0.5 -tl color 512
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tr
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc -ts 0.5 -tl 64
./gltfpack -test glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -tc color -tfy -tq 4 -tj 1
./gltfpack -test glTF-Sample-Assets/Models/CesiumMan/glTF/CesiumMan.gltf -tu -ts 0.6 -tp -ar 0
./gltfpack -test glTF-Sample-Assets/Models/CesiumMan/glTF/CesiumMan.gltf -gt -kv
./gltfpack -test glTF-Sample-Assets/Models/PrimitiveModeNormalsTest/glTF/PrimitiveModeNormalsTest.gltf -si 0.5
./gltfpack -test glTF-Sample-Assets/Models/VertexColorTest/glTF/VertexColorTest.gltf -si 0.5 -vc 12
./gltfpack -test glTF-Sample-Assets/Models/SimpleMeshes/glTF/SimpleMeshes.gltf -mm
./gltfpack -test glTF-Sample-Assets/Models/SimpleMeshes/glTF/SimpleMeshes.gltf -kn
echo newmtl Leather > demo/pirate.mtl
echo map_Kd leather.jpg >> demo/pirate.mtl
echo map_d leather.jpg >> demo/pirate.mtl
./gltfpack -test demo/pirate.obj
- name: test output
run: |
./gltfpack || true
./gltfpack -h || true
./gltfpack -i glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -o box.glb -vv -r box.json
./gltfpack -i glTF-Sample-Assets/Models/BoxTextured/glTF/BoxTextured.gltf -o box.gltf -cf
- name: upload coverage
run: |
find . -type f -name '*.gcno' -exec gcov -p {} +
sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov
bash <(curl -s https://codecov.io/bash) -f './gltf*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}}
macos-iphone:
runs-on: macos-14
steps:
- uses: actions/checkout@v6
- name: make
run: make -j2 config=iphone
+33
View File
@@ -0,0 +1,33 @@
name: CIFuzz
on:
push:
branches:
- 'master'
paths-ignore:
- '*.md'
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'meshoptimizer'
dry-run: false
language: c++
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'meshoptimizer'
fuzz-seconds: 30
dry-run: false
language: c++
- name: Upload Crash
uses: actions/upload-artifact@v7
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
+77
View File
@@ -0,0 +1,77 @@
name: release
on:
push:
branches:
- 'master'
paths-ignore:
- '*.md'
jobs:
gltfpack:
strategy:
matrix:
os: [{ name: windows, version: windows-latest }, { name: ubuntu, version: ubuntu-22.04 }, { name: macos, version: macos-14 }]
name: gltfpack-${{matrix.os.name}}
runs-on: ${{matrix.os.version}}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
repository: zeux/basis_universal
ref: gltfpack
path: basis_universal
- uses: actions/checkout@v6
with:
repository: webmproject/libwebp
ref: 1.6.0
path: libwebp
- name: cmake configure
run: cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -D MESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded" -DCMAKE_BUILD_TYPE=Release
- name: cmake configure x64
run: cmake . -DSSE=ON -DBASISU_SSE=ON
if: matrix.os.name == 'ubuntu'
- name: cmake build
run: cmake --build . --target gltfpack --config Release -j 3
- uses: actions/upload-artifact@v7
with:
name: gltfpack-windows
path: Release/gltfpack.exe
if: matrix.os.name == 'windows'
- uses: actions/upload-artifact@v7
with:
name: gltfpack-${{matrix.os.name}}
path: gltfpack
if: matrix.os.name != 'windows'
nodejs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '18'
- name: install wasi
run: |
curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-x86_64-linux.deb > wasi-sdk.deb
sudo dpkg -i wasi-sdk.deb
env:
VERSION: 32
- name: build
run: |
make -j2 -B gltf/library.wasm js
git status
- name: npm pack
run: |
cp LICENSE.md gltf/
cp LICENSE.md js/
cd gltf && npm pack && cd ..
cd js && npm pack && cd ..
- uses: actions/upload-artifact@v7
with:
name: gltfpack-npm
path: gltf/gltfpack-*.tgz
- uses: actions/upload-artifact@v7
with:
name: meshoptimizer-npm
path: js/meshoptimizer-*.tgz
+19
View File
@@ -0,0 +1,19 @@
# IDE integrations
/.cache/
/.idea/
/.vs/
/.vscode/
/.zed/
# Build files
/build/
/cmake*/
/out/
/*.dSYM/
/gltf/library.wasm
compile_commands.json
# Test files
/data/
/*.log
/perf.data*
+16
View File
@@ -0,0 +1,16 @@
{
"useTabs": true,
"tabWidth": 4,
"semi": true,
"singleQuote": true,
"printWidth": 150,
"trailingComma": "es5",
"overrides": [
{
"files": "*.yml",
"options": {
"tabWidth": 2
}
},
]
}
+227
View File
@@ -0,0 +1,227 @@
cmake_minimum_required(VERSION 3.5...3.30)
if(POLICY CMP0077)
cmake_policy(SET CMP0077 NEW) # Enables override of options from parent CMakeLists.txt
endif()
if(POLICY CMP0091)
cmake_policy(SET CMP0091 NEW) # Enables use of MSVC_RUNTIME_LIBRARY
endif()
if(POLICY CMP0092)
cmake_policy(SET CMP0092 NEW) # Enables clean /W4 override for MSVC
endif()
project(meshoptimizer VERSION 1.2 LANGUAGES CXX)
option(MESHOPT_BUILD_DEMO "Build demo" OFF)
option(MESHOPT_BUILD_GLTFPACK "Build gltfpack" OFF)
option(MESHOPT_BUILD_SHARED_LIBS "Build shared libraries" OFF)
option(MESHOPT_STABLE_EXPORTS "Only export stable APIs from shared library" OFF)
option(MESHOPT_WERROR "Treat warnings as errors" OFF)
option(MESHOPT_INSTALL "Install library" ON)
# Optional gltfpack components for texture compression support
set(MESHOPT_GLTFPACK_BASISU_PATH "" CACHE STRING "") # Basis Universal, https://github.com/BinomialLLC/basis_universal
set(MESHOPT_GLTFPACK_LIBWEBP_PATH "" CACHE STRING "") # libwebp, https://github.com/webmproject/libwebp
set(SOURCES
src/meshoptimizer.h
src/allocator.cpp
src/clusterizer.cpp
src/indexanalyzer.cpp
src/indexcodec.cpp
src/indexgenerator.cpp
src/meshletcodec.cpp
src/meshletutils.cpp
src/opacitymap.cpp
src/overdrawoptimizer.cpp
src/partition.cpp
src/quantization.cpp
src/rasterizer.cpp
src/remesher.cpp
src/simplifier.cpp
src/spatialorder.cpp
src/stripifier.cpp
src/tangentspace.cpp
src/vcacheoptimizer.cpp
src/vertexcodec.cpp
src/vertexfilter.cpp
src/vfetchoptimizer.cpp
)
set(GLTF_SOURCES
gltf/animation.cpp
gltf/encodebasis.cpp
gltf/encodewebp.cpp
gltf/fileio.cpp
gltf/gltfpack.cpp
gltf/image.cpp
gltf/json.cpp
gltf/material.cpp
gltf/mesh.cpp
gltf/node.cpp
gltf/parseobj.cpp
gltf/parselib.cpp
gltf/parsegltf.cpp
gltf/stream.cpp
gltf/write.cpp
)
if(WIN32)
list(APPEND GLTF_SOURCES gltf/gltfpack.manifest)
endif()
if(MSVC)
add_compile_options(/W4)
else()
add_compile_options(-Wall -Wextra -Wshadow -Wno-missing-field-initializers)
endif()
if(MESHOPT_WERROR)
if(MSVC)
add_compile_options(/WX)
else()
add_compile_options(-Werror)
endif()
endif()
if(MESHOPT_BUILD_SHARED_LIBS)
add_library(meshoptimizer SHARED ${SOURCES})
else()
add_library(meshoptimizer STATIC ${SOURCES})
endif()
target_include_directories(meshoptimizer INTERFACE "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>")
if(MESHOPT_BUILD_SHARED_LIBS)
set_target_properties(meshoptimizer PROPERTIES CXX_VISIBILITY_PRESET hidden)
set_target_properties(meshoptimizer PROPERTIES VISIBILITY_INLINES_HIDDEN ON)
# soversion may be requested via -DMESHOPT_SOVERSION=n; note that experimental APIs (marked with MESHOPTIMIZER_EXPERIMENTAL) are not ABI-stable
if(MESHOPT_SOVERSION)
set_target_properties(meshoptimizer PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${MESHOPT_SOVERSION})
endif()
if(WIN32)
target_compile_definitions(meshoptimizer INTERFACE "MESHOPTIMIZER_API=__declspec(dllimport)")
target_compile_definitions(meshoptimizer PRIVATE "MESHOPTIMIZER_API=__declspec(dllexport)")
else()
target_compile_definitions(meshoptimizer PUBLIC "MESHOPTIMIZER_API=__attribute__((visibility(\"default\")))")
endif()
target_compile_definitions(meshoptimizer PUBLIC MESHOPTIMIZER_ALLOC_EXPORT)
if(MESHOPT_STABLE_EXPORTS)
target_compile_definitions(meshoptimizer PUBLIC "MESHOPTIMIZER_EXPERIMENTAL=")
endif()
endif()
if(MESHOPT_BUILD_DEMO)
add_executable(demo demo/main.cpp demo/nanite.cpp demo/tests.cpp tools/objloader.cpp)
set_target_properties(demo PROPERTIES CXX_STANDARD 11)
set_target_properties(demo PROPERTIES OUTPUT_NAME meshoptdemo)
target_link_libraries(demo meshoptimizer)
endif()
if(MESHOPT_BUILD_GLTFPACK)
add_executable(gltfpack ${GLTF_SOURCES})
set_target_properties(gltfpack PROPERTIES CXX_STANDARD 11)
target_link_libraries(gltfpack meshoptimizer)
if(MESHOPT_BUILD_SHARED_LIBS AND MESHOPT_STABLE_EXPORTS)
target_compile_definitions(gltfpack PRIVATE GLTFPACK_NO_EXPERIMENTAL)
endif()
if(MESHOPT_BUILD_SHARED_LIBS)
string(CONCAT RPATH "$ORIGIN/../" ${CMAKE_INSTALL_LIBDIR})
set_target_properties(gltfpack PROPERTIES INSTALL_RPATH ${RPATH})
endif()
if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "")
get_filename_component(BASISU_PATH ${MESHOPT_GLTFPACK_BASISU_PATH} ABSOLUTE)
if (NOT EXISTS ${BASISU_PATH})
message(FATAL_ERROR "Basis Universal path ${BASISU_PATH} not found")
endif()
if (NOT SSE AND NOT BASISU_SSE AND NOT MSVC AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
message(WARNING "Building Basis Universal without SSE4.1 support; performance may be suboptimal")
endif()
add_subdirectory(${BASISU_PATH} ${CMAKE_CURRENT_BINARY_DIR}/basisu EXCLUDE_FROM_ALL)
target_compile_definitions(gltfpack PRIVATE WITH_BASISU)
target_link_libraries(gltfpack basisu_encoder)
set_source_files_properties(gltf/encodebasis.cpp PROPERTIES INCLUDE_DIRECTORIES ${BASISU_PATH}) # necessary because basisu_encoder doesn't export include directories
endif()
if(NOT MESHOPT_GLTFPACK_LIBWEBP_PATH STREQUAL "")
get_filename_component(LIBWEBP_PATH ${MESHOPT_GLTFPACK_LIBWEBP_PATH} ABSOLUTE)
if (NOT EXISTS ${LIBWEBP_PATH})
message(FATAL_ERROR "libwebp path ${LIBWEBP_PATH} not found")
endif()
add_subdirectory(${LIBWEBP_PATH} ${CMAKE_CURRENT_BINARY_DIR}/libwebp EXCLUDE_FROM_ALL)
target_compile_definitions(gltfpack PRIVATE WITH_LIBWEBP)
target_link_libraries(gltfpack webp)
# when gltfpack is built with Basis Universal, we use Basis image decoders for PNG/JPEG support for ease of distribution
if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "")
target_compile_definitions(gltfpack PRIVATE WITH_LIBWEBP_BASIS)
else()
target_link_libraries(gltfpack imagedec)
endif()
endif()
if(NOT MESHOPT_GLTFPACK_BASISU_PATH STREQUAL "" OR NOT MESHOPT_GLTFPACK_LIBWEBP_PATH STREQUAL "")
find_package(Threads REQUIRED)
target_link_libraries(gltfpack Threads::Threads)
endif()
endif()
if(MESHOPT_INSTALL)
include(GNUInstallDirs)
install(TARGETS meshoptimizer EXPORT meshoptimizerTargets
COMPONENT meshoptimizer
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
if(MESHOPT_BUILD_GLTFPACK)
install(TARGETS gltfpack RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
install(FILES src/meshoptimizer.h COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(EXPORT meshoptimizerTargets COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer NAMESPACE meshoptimizer::)
if(MSVC)
set(PDB_TARGETS meshoptimizer)
if(MESHOPT_BUILD_GLTFPACK)
list(APPEND PDB_TARGETS gltfpack)
endif()
foreach(TARGET ${PDB_TARGETS})
get_target_property(TARGET_TYPE ${TARGET} TYPE)
if(NOT ${TARGET_TYPE} STREQUAL "STATIC_LIBRARY")
install(FILES $<TARGET_PDB_FILE:${TARGET}> COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL)
endif()
endforeach(TARGET)
endif()
include(CMakePackageConfigHelpers)
# CMake 3.18+ supports file(CONFIGURE OUTPUT file CONTENT content @ONLY) but we only require 3.5+
file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake
"include(\"\${CMAKE_CURRENT_LIST_DIR}/meshoptimizerTargets.cmake\")\n")
write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake COMPATIBILITY ExactVersion)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake
${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake
COMPONENT meshoptimizer
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer)
endif()
+63
View File
@@ -0,0 +1,63 @@
Thanks for deciding to contribute to meshoptimizer! These guidelines will try to help make the process painless and efficient.
## Questions
If you have a question regarding the library usage, please [create a GitHub discussion](https://github.com/zeux/meshoptimizer/discussions/new/choose).
Some questions just need answers, but it's nice to keep them for future reference in case other people want to know the same thing.
Some questions help improve the library interface or documentation by inspiring future changes.
## Bugs
If the library doesn't compile on your system, compiles with warnings, doesn't seem to run correctly for your input data or if anything else is amiss, please [open a GitHub issue](https://github.com/zeux/meshoptimizer/issues/new).
It helps if you note the version of the library this issue happens in, the version of your compiler for compilation issues, and a reproduction case for runtime bugs.
Of course, feel free to [create a pull request](https://help.github.com/articles/about-pull-requests/) to fix the bug yourself.
## Features
New algorithms and improvements to existing algorithms are always welcome; please open an issue describing an improvement you'd like to see or make before opening a pull request.
This will give us a chance to discuss the idea before implementing it - some algorithms may not be easy to integrate into existing programs, may not be robust to arbitrary meshes or may be expensive to run or implement/maintain, so a discussion helps make sure these don't block the algorithm development.
## Code style
Contributions to this project are expected to follow the existing code style.
`.clang-format` file mostly defines syntactic styling rules (you can run `make format` to format the code accordingly).
As for naming conventions, this library uses `snake_case` for variables, `lowerCamelCase` for functions, `UpperCamelCase` for types, `kCamelCase` for global constants and `SCARY_CASE` for macros. All public functions/types must additionally have an extra `meshopt_` prefix to avoid symbol conflicts.
## Dependencies
Please note that this library uses C89 interface for all APIs and a C++98 implementation - C++11 features can not be used.
This choice is made to maximize compatibility to make sure that any toolchain, including legacy proprietary gaming console toolchains, can compile this code.
Additionally, the library code has zero external dependencies, does not depend on STL and does not use RTTI or exceptions.
This, again, maximizes compatibility and makes sure the library can be used in environments where STL use is discouraged or prohibited, as well as maximizing runtime performance and minimizing compilation times.
The demo program uses STL since it serves as an example of usage and as a test harness, not as production-ready code.
## Testing
All pull requests will run through a continuous integration pipeline using GitHub Actions that will run the built-in unit tests and integration tests on Windows, macOS and Linux with gcc, clang and msvc compilers.
You can run the tests yourself using `make test` or building the demo program with `cmake -DBUILD_DEMO=ON` and running it.
Unit tests can be found in `demo/tests.cpp` and functional tests - in `demo/main.cpp`; when making code changes please try to make sure they are covered by an existing test or add a new test accordingly.
## Documentation
Documentation for this library resides in the `meshoptimizer.h` header, with examples as part of a usage manual available in `README.md`.
Changes to documentation are always welcome and should use issues/pull requests as outlined above; please note that `README.md` only contains documentation for stable algorithms, as experimental algorithms may change the interface without concern for backwards compatibility.
## Sensitive communication
If you prefer to not disclose the issues or information relevant to the issue such as reproduction case to the public, you can always contact the author via e-mail (arseny.kapoulkine@gmail.com).
## Contributor agreement
Any code you submit will become part of the repository and be distributed under the [meshoptimizer license](https://github.com/zeux/meshoptimizer/blob/master/LICENSE.md). By submitting code to the project you agree that the code is your work and that you can give it to the project.
You also agree by submitting your code that you grant all transferrable rights to the code to the project maintainer, including for example re-licensing the code, modifying the code, and distributing it in source or binary forms. Specifically, this includes a requirement that you assign copyright to the project maintainer.
## AI
AI-generated issues or pull requests (including pull request descriptions), will be closed. Repeat submissions may result in a ban.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016-2026 Arseny Kapoulkine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+271
View File
@@ -0,0 +1,271 @@
MAKEFLAGS+=-r -j
config=debug
files=demo/pirate.obj
BUILD=build/$(config)
LIBRARY_SOURCES=$(wildcard src/*.cpp)
LIBRARY_OBJECTS=$(LIBRARY_SOURCES:%=$(BUILD)/%.o)
DEMO_SOURCES=$(wildcard demo/*.c demo/*.cpp) tools/objloader.cpp
DEMO_OBJECTS=$(DEMO_SOURCES:%=$(BUILD)/%.o)
GLTFPACK_SOURCES=$(wildcard gltf/*.cpp)
GLTFPACK_OBJECTS=$(GLTFPACK_SOURCES:%=$(BUILD)/%.o)
OBJECTS=$(LIBRARY_OBJECTS) $(DEMO_OBJECTS) $(GLTFPACK_OBJECTS)
LIBRARY=$(BUILD)/libmeshoptimizer.a
DEMO=$(BUILD)/meshoptdemo
CFLAGS=-g -Wall -Wextra -std=c89
CXXFLAGS=-g -Wall -Wextra -Wshadow -Wno-missing-field-initializers
LDFLAGS=
$(LIBRARY_OBJECTS): CXXFLAGS+=-std=gnu++98
$(DEMO_OBJECTS): CXXFLAGS+=-std=c++11
$(GLTFPACK_OBJECTS): CXXFLAGS+=-std=c++11
ifdef BASISU
$(GLTFPACK_OBJECTS): CXXFLAGS+=-DWITH_BASISU
$(BUILD)/gltf/encodebasis.cpp.o: CXXFLAGS+=-I$(BASISU)
gltfpack: LDFLAGS+=-lpthread $(BASISU)/libbasisu_encoder.a
endif
WASI_SDK?=/opt/wasi-sdk
WASMCC?=$(WASI_SDK)/bin/clang++
WASIROOT?=$(WASI_SDK)/share/wasi-sysroot
WASM_FLAGS=--target=wasm32-wasi -Wno-deprecated --sysroot=$(WASIROOT) --no-wasm-opt
WASM_FLAGS+=-Wall -Wextra
WASM_FLAGS+=-O3 -DNDEBUG -DMESHOPTIMIZER_VERTEXCODEC_ZEROCOPY -nostartfiles -nostdlib -Wl,--no-entry -Wl,-s
WASM_FLAGS+=-mcpu=mvp # make sure clang doesn't use post-MVP features like sign extension
WASM_FLAGS+=-fno-slp-vectorize -fno-vectorize -fno-unroll-loops
WASM_FLAGS+=-Wl,-z -Wl,stack-size=36864 -Wl,--initial-memory=65536
WASM_EXPORT_PREFIX=-Wl,--export
WASM_DECODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp tools/wasmstubs.cpp
WASM_DECODER_EXPORTS=meshopt_decodeVertexBuffer meshopt_decodeIndexBuffer meshopt_decodeIndexSequence meshopt_decodeFilterOct meshopt_decodeFilterQuat meshopt_decodeFilterExp meshopt_decodeFilterColor sbrk __wasm_call_ctors
WASM_ENCODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp src/vcacheoptimizer.cpp src/vfetchoptimizer.cpp src/spatialorder.cpp tools/wasmstubs.cpp
WASM_ENCODER_EXPORTS=meshopt_encodeVertexBuffer meshopt_encodeVertexBufferBound meshopt_encodeVertexBufferLevel meshopt_encodeIndexBuffer meshopt_encodeIndexBufferBound meshopt_encodeIndexSequence meshopt_encodeIndexSequenceBound meshopt_encodeVertexVersion meshopt_encodeIndexVersion meshopt_encodeFilterOct meshopt_encodeFilterQuat meshopt_encodeFilterExp meshopt_encodeFilterColor meshopt_optimizeVertexCache meshopt_optimizeVertexCacheStrip meshopt_optimizeVertexFetchRemap meshopt_spatialSortRemap sbrk __wasm_call_ctors
WASM_SIMPLIFIER_SOURCES=src/simplifier.cpp src/remesher.cpp src/vfetchoptimizer.cpp src/indexgenerator.cpp tools/wasmstubs.cpp
WASM_SIMPLIFIER_EXPORTS=meshopt_simplify meshopt_simplifyWithAttributes meshopt_simplifyWithUpdate meshopt_simplifyScale meshopt_simplifyPoints meshopt_simplifySloppy meshopt_simplifyPrune meshopt_remesh meshopt_optimizeVertexFetchRemap meshopt_generatePositionRemap sbrk __wasm_call_ctors
WASM_CLUSTERIZER_SOURCES=src/clusterizer.cpp src/meshletutils.cpp tools/wasmstubs.cpp
WASM_CLUSTERIZER_EXPORTS=meshopt_buildMeshletsBound meshopt_buildMeshletsFlex meshopt_buildMeshletsSpatial meshopt_computeClusterBounds meshopt_computeMeshletBounds meshopt_computeSphereBounds meshopt_optimizeMeshlet sbrk __wasm_call_ctors
WASM_TANGENTS_SOURCES=src/tangentspace.cpp tools/wasmstubs.cpp
WASM_TANGENTS_EXPORTS=meshopt_generateTangents meshopt_generateNormals sbrk __wasm_call_ctors
ifneq ($(werror),)
CFLAGS+=-Werror
CXXFLAGS+=-Werror
endif
ifeq ($(config),iphone)
IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk
CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK)
CXXFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -stdlib=libc++
LDFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -L $(IPHONESDK)/usr/lib -mios-version-min=7.0
endif
ifeq ($(config),trace)
CXXFLAGS+=-DTRACE=1
endif
ifeq ($(config),tracev)
CXXFLAGS+=-DTRACE=2
endif
ifeq ($(config),release)
CXXFLAGS+=-O3 -DNDEBUG
endif
ifeq ($(config),coverage)
CXXFLAGS+=-coverage
LDFLAGS+=-coverage
endif
ifeq ($(config),release-avx)
CXXFLAGS+=-O3 -DNDEBUG -mavx
endif
ifeq ($(config),release-avx512)
CXXFLAGS+=-O3 -DNDEBUG -mavx512vl -mavx512vbmi -mavx512vbmi2 -mbmi2
endif
ifeq ($(config),release-scalar)
CXXFLAGS+=-O3 -DNDEBUG -DMESHOPTIMIZER_NO_SIMD
endif
ifeq ($(config),coverage-scalar)
CXXFLAGS+=-coverage -DMESHOPTIMIZER_NO_SIMD
LDFLAGS+=-coverage
endif
ifeq ($(config),sanitize)
CXXFLAGS+=-fsanitize=address,undefined -fsanitize-undefined-trap-on-error
LDFLAGS+=-fsanitize=address,undefined
endif
ifeq ($(config),analyze)
CXXFLAGS+=--analyze
endif
ifeq ($(config),fuzz)
CXXFLAGS+=-O1 -fsanitize=address,fuzzer
LDFLAGS+=-fsanitize=address,fuzzer
$(GLTFPACK_OBJECTS): CXXFLAGS+=-DGLTFFUZZ
endif
-include Makefile.config
all: $(DEMO)
test: $(DEMO)
$(DEMO) $(files)
check: $(DEMO)
$(DEMO)
dev: $(DEMO)
$(DEMO) -d $(files)
nanite: $(DEMO)
$(DEMO) -n $(files)
format:
clang-format -i src/*.h src/*.cpp demo/*.cpp gltf/*.h gltf/*.cpp
formatjs:
prettier -w js/*.js gltf/*.js demo/*.html js/*.ts
js: js/meshopt_decoder.cjs js/meshopt_decoder.mjs js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js
symbols: $(BUILD)/amalgamated.so
nm $< -U -g
gltfpack: $(BUILD)/gltfpack
ln -fs $^ $@
ifeq ($(config),fuzz)
gltffuzz: $(BUILD)/gltfpack
cp $^ $@
mkdir -p /tmp/gltffuzz
cp gltf/fuzz.glb /tmp/gltffuzz/
./gltffuzz /tmp/gltffuzz -fork=16 -dict=gltf/fuzz.dict -ignore_crashes=1 -max_len=32768
endif
$(BUILD)/gltfpack: $(GLTFPACK_OBJECTS) $(LIBRARY)
$(CXX) $^ $(LDFLAGS) -o $@
gltfpack.wasm: gltf/library.wasm
gltf/library.wasm: $(LIBRARY_SOURCES) $(GLTFPACK_SOURCES)
$(WASMCC) $^ -o $@ -Wall -Os -DNDEBUG --target=wasm32-wasi -Wno-deprecated --sysroot=$(WASIROOT) -nostartfiles --no-wasm-opt -Wl,--no-entry -Wl,--export=pack -Wl,--export=malloc -Wl,--export=free -Wl,--export=__wasm_call_ctors -Wl,-s -Wl,--allow-undefined-file=gltf/wasistubs.txt
build/decoder_base.wasm: $(WASM_DECODER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@
build/decoder_simd.wasm: $(WASM_DECODER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@ -msimd128 -mbulk-memory -DMESHOPTIMIZER_VERTEXFILTER_SIMDNOTAIL
build/encoder.wasm: $(WASM_ENCODER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_ENCODER_EXPORTS)) -lc -o $@
build/simplifier.wasm: $(WASM_SIMPLIFIER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_SIMPLIFIER_EXPORTS)) -lc -o $@
build/clusterizer.wasm: $(WASM_CLUSTERIZER_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_CLUSTERIZER_EXPORTS)) -lc -o $@
build/tangents.wasm: $(WASM_TANGENTS_SOURCES)
@mkdir -p build
$(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_TANGENTS_EXPORTS)) -lc -o $@
js/meshopt_decoder.mjs: build/decoder_base.wasm build/decoder_simd.wasm tools/wasmpack.py
sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@
sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@
python3 tools/wasmpack.py patch $@ base <build/decoder_base.wasm
python3 tools/wasmpack.py patch $@ simd <build/decoder_simd.wasm
js/meshopt_encoder.js: build/encoder.wasm tools/wasmpack.py
js/meshopt_simplifier.js: build/simplifier.wasm tools/wasmpack.py
js/meshopt_clusterizer.js: build/clusterizer.wasm tools/wasmpack.py
js/meshopt_tangents.js: build/tangents.wasm tools/wasmpack.py
js/meshopt_encoder.js js/meshopt_simplifier.js js/meshopt_clusterizer.js js/meshopt_tangents.js:
sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@
sed -i "s#Built from meshoptimizer .*#Built from meshoptimizer $$(cat src/meshoptimizer.h | grep -Po '(?<=version )[0-9.]+')#" $@
python3 tools/wasmpack.py patch $@ wasm <$<
js/meshopt_decoder.cjs: js/meshopt_decoder.mjs
sed '/export {/d' <$< >$@
echo "if (typeof exports === 'object' && typeof module === 'object') module.exports = MeshoptDecoder;" >>$@
echo "else if (typeof define === 'function' && define['amd']) define([], function () { return MeshoptDecoder; });" >>$@
echo "else if (typeof exports === 'object') exports['MeshoptDecoder'] = MeshoptDecoder;" >>$@
echo "else (typeof self !== 'undefined' ? self : this).MeshoptDecoder = MeshoptDecoder;" >>$@
$(DEMO): $(DEMO_OBJECTS) $(LIBRARY)
$(CXX) $^ $(LDFLAGS) -o $@
vcachetuner: tools/vcachetuner.cpp tools/objloader.cpp $(LIBRARY)
$(CXX) $^ -fopenmp $(CXXFLAGS) -std=c++11 $(LDFLAGS) -o $@
codecbench: tools/codecbench.cpp $(LIBRARY)
$(CXX) $^ $(CXXFLAGS) $(LDFLAGS) -o $@
codecbench.js: tools/codecbench.cpp $(LIBRARY_SOURCES)
emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -o $@
codecbench-simd.js: tools/codecbench.cpp $(LIBRARY_SOURCES)
emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -msimd128 -o $@
codecbench.wasm: tools/codecbench.cpp $(LIBRARY_SOURCES)
$(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -o $@
codecbench-simd.wasm: tools/codecbench.cpp $(LIBRARY_SOURCES)
$(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -msimd128 -o $@
codectest: tools/codectest.cpp $(LIBRARY)
$(CXX) $^ $(CXXFLAGS) $(LDFLAGS) -o $@
codecfuzz: tools/codecfuzz.cpp src/vertexcodec.cpp src/indexcodec.cpp src/meshletcodec.cpp
$(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@
clusterfuzz: tools/clusterfuzz.cpp src/clusterizer.cpp src/partition.cpp
$(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@
simplifyfuzz: tools/simplifyfuzz.cpp src/simplifier.cpp
$(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@
$(LIBRARY): $(LIBRARY_OBJECTS)
ar rcs $@ $^
$(BUILD)/amalgamated.so: $(LIBRARY_SOURCES)
@mkdir -p $(dir $@)
cat $^ | $(CXX) $(CXXFLAGS) -x c++ - -I src/ -o $@ -shared -fPIC
$(BUILD)/%.cpp.o: %.cpp
@mkdir -p $(dir $@)
$(CXX) $< $(CXXFLAGS) -c -MMD -MP -o $@
$(BUILD)/%.c.o: %.c
@mkdir -p $(dir $@)
$(CC) $< $(CFLAGS) -c -MMD -MP -o $@
-include $(OBJECTS:.o=.d)
clean:
rm -rf $(BUILD)
.PHONY: all clean format js
+967
View File
@@ -0,0 +1,967 @@
# 🐇 meshoptimizer [![Actions Status](https://github.com/zeux/meshoptimizer/workflows/build/badge.svg)](https://github.com/zeux/meshoptimizer/actions) [![codecov.io](https://codecov.io/github/zeux/meshoptimizer/coverage.svg?branch=master)](https://codecov.io/github/zeux/meshoptimizer?branch=master) [![MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md) [![GitHub](https://img.shields.io/badge/repo-github-green.svg)](https://github.com/zeux/meshoptimizer)
## Purpose
When a GPU renders triangle meshes, various stages of the GPU pipeline have to process vertex and index data. The efficiency of these stages depends on the data you feed to them; this library provides algorithms to help optimize meshes for these stages, as well as algorithms to reduce the mesh complexity and storage overhead.
The library provides a C and C++ interface for all algorithms; you can use it from C/C++ or from other languages via FFI (such as P/Invoke). If you want to use this library from Rust, you should use [meshopt crate](https://crates.io/crates/meshopt). JavaScript interface for some algorithms is available through [meshoptimizer.js](https://www.npmjs.com/package/meshoptimizer).
Two companion projects are developed and distributed alongside the library: [gltfpack](./gltf/README.md), a command-line tool that automatically optimizes glTF files, and [clusterlod.h](./demo/clusterlod.h), a single-header C/C++ library for continuous level of detail using clustered simplification.
## Installing
meshoptimizer is hosted on GitHub; you can download the latest release using git:
```
git clone -b v1.2 https://github.com/zeux/meshoptimizer.git
```
Alternatively you can [download the .zip archive from GitHub](https://github.com/zeux/meshoptimizer/archive/v1.2.zip).
The library is also available as a Linux package in several distributions ([ArchLinux](https://aur.archlinux.org/packages/meshoptimizer/), [Debian](https://packages.debian.org/libmeshoptimizer), [FreeBSD](https://www.freshports.org/misc/meshoptimizer/), [Nix](https://mynixos.com/nixpkgs/package/meshoptimizer), [Ubuntu](https://packages.ubuntu.com/libmeshoptimizer)), as well as a [Vcpkg port](https://github.com/microsoft/vcpkg/tree/master/ports/meshoptimizer) (see [installation instructions](https://learn.microsoft.com/en-us/vcpkg/get_started/get-started)) and a [Conan package](https://conan.io/center/recipes/meshoptimizer).
[gltfpack](./gltf/README.md) is available as a pre-built binary on [Releases page](https://github.com/zeux/meshoptimizer/releases) or via [npm package](https://www.npmjs.com/package/gltfpack). Native binaries are recommended since they are more efficient and support texture compression.
## Building
meshoptimizer is distributed as a C/C++ header (`src/meshoptimizer.h`) and a set of C++ source files (`src/*.cpp`). To include it in your project, you can use one of two options:
* Use CMake to build the library (either as a standalone project or as part of your project)
* Add source files to your project's build system
The source files are organized in such a way that you don't need to change your build-system settings, and you only need to add the source files for the algorithms you use. They should build without warnings or special compilation options on all major compilers. If you prefer amalgamated builds, you can also concatenate the source files into a single `.cpp` file and build that instead.
To use meshoptimizer functions, simply `#include` the header `meshoptimizer.h`; the library source is C++, but the header is C-compatible.
## Core pipeline
When optimizing a mesh, to maximize rendering efficiency you should typically feed it through a set of optimizations (the order is important!):
1. Indexing
2. Vertex cache optimization
3. (optional) Overdraw optimization
4. Vertex fetch optimization
5. Vertex quantization
6. Index filtering
7. (optional) Shadow indexing
### Indexing
Most algorithms in this library assume that a mesh has a vertex buffer and an index buffer. For algorithms to work well and also for GPU to render your mesh efficiently, the vertex buffer has to have no redundant vertices; you can generate an index buffer from an unindexed vertex buffer or reindex an existing (potentially redundant) index buffer as follows:
> Note: meshoptimizer generally works with 32-bit (`unsigned int`) indices, however when using C++ APIs you can use any integer type for index data by using the provided template overloads. By convention, remap tables always use `unsigned int`.
First, generate a remap table from your existing vertex (and, optionally, index) data:
```c++
size_t index_count = face_count * 3;
size_t unindexed_vertex_count = face_count * 3;
std::vector<unsigned int> remap(unindexed_vertex_count); // temporary remap table
size_t vertex_count = meshopt_generateVertexRemap(&remap[0], NULL, index_count,
&unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex));
```
Note that in this case we only have an unindexed vertex buffer; when input mesh has an index buffer, it will need to be passed to `meshopt_generateVertexRemap` instead of `NULL`, along with the correct source vertex count. In either case, the remap table is generated based on binary equivalence of the input vertices, so the resulting mesh will render the same way. Binary equivalence considers all input bytes, including padding which should be zero-initialized if the vertex structure has gaps.
After generating the remap table, you can allocate space for the target vertex buffer (`vertex_count` elements) and index buffer (`index_count` elements) and generate them:
```c++
meshopt_remapIndexBuffer(indices, NULL, index_count, &remap[0]);
meshopt_remapVertexBuffer(vertices, &unindexed_vertices[0], unindexed_vertex_count, sizeof(Vertex), &remap[0]);
```
You can then further optimize the resulting buffers by calling the other functions on them in-place.
`meshopt_generateVertexRemap` uses binary equivalence of vertex data, which is generally a reasonable default; however, in some cases some attributes may have floating point drift causing extra vertices to be generated. For such cases, it may be necessary to quantize some attributes (most importantly, normals and tangents) before generating the remap, or use `meshopt_generateVertexRemapCustom` algorithm that allows comparing individual attributes with tolerance by providing a custom comparison function:
```c++
size_t vertex_count = meshopt_generateVertexRemapCustom(&remap[0], NULL, index_count,
&unindexed_vertices[0].px, unindexed_vertex_count, sizeof(Vertex),
[&](unsigned int lhs, unsigned int rhs) -> bool {
const Vertex& lv = unindexed_vertices[lhs];
const Vertex& rv = unindexed_vertices[rhs];
return fabsf(lv.tx - rv.tx) < 1e-3f && fabsf(lv.ty - rv.ty) < 1e-3f;
});
```
### Vertex cache optimization
When the GPU renders the mesh, it runs the vertex shader for each vertex. Historically, GPUs used a small fixed-size post-transform cache (16-32 vertices) with different replacement policies to store the shader output and avoid redundant shader invocations. Modern GPUs still perform vertex reuse, but with substantially different mechanics: vertex invocations are batched into thread groups based on the input indices, and effective reuse depends on factors like vertex shader outputs and rasterizer throughput. To maximize the locality of reused vertex references, you have to reorder your triangles like so:
```c++
meshopt_optimizeVertexCache(indices, indices, index_count, vertex_count);
```
The details of vertex reuse vary between different GPU architectures, so vertex cache optimization uses an adaptive algorithm that produces a triangle sequence with good locality that works well across different GPUs. Alternatively, you can use an algorithm that optimizes specifically for fixed-size FIFO caches: `meshopt_optimizeVertexCacheFifo` (with a recommended cache size of 16). While it generally produces less performant results on most GPUs, it runs ~2x faster, which may benefit rapid content iteration.
### Overdraw optimization
After transforming the vertices, GPU sends the triangles for rasterization which results in generating pixels that are usually first run through the depth test, and pixels that pass it get the pixel shader executed to generate the final color. As pixel shaders get more expensive, it becomes more and more important to reduce overdraw. While in general improving overdraw requires view-dependent operations, this library provides an algorithm to reorder triangles to minimize the overdraw from all directions, which you can run after vertex cache optimization like this:
```c++
meshopt_optimizeOverdraw(indices, indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex), 1.05f);
```
The overdraw optimizer needs to read vertex positions as a float3 from the vertex; the code snippet above assumes that the vertex stores position as `float x, y, z`.
When performing the overdraw optimization you have to specify a floating-point threshold parameter. The algorithm tries to maintain a balance between vertex cache efficiency and overdraw; the threshold determines how much the algorithm can compromise the vertex cache hit ratio, with 1.05 meaning that the resulting ratio should be at most 5% worse than before the optimization.
Note that depending on the renderer structure and target hardware, the optimization may or may not be beneficial; for example, mobile GPUs with tiled deferred rendering (PowerVR, Apple) would not benefit from this optimization. For vertex heavy scenes it's recommended to measure the performance impact to ensure that the reduced vertex cache efficiency is outweighed by the reduced overdraw.
### Vertex fetch optimization
After the final triangle order has been established, we still can optimize the vertex buffer for memory efficiency. Before running the vertex shader GPU has to fetch the vertex attributes from the vertex buffer; the fetch is usually backed by a memory cache, and as such optimizing the data for the locality of memory access is important. You can do this by running this code:
```c++
meshopt_optimizeVertexFetch(vertices, indices, index_count, vertices, vertex_count, sizeof(Vertex));
```
This will reorder the vertices in the vertex buffer to try to improve the locality of reference, and rewrite the indices in place to match; if the vertex data is stored using multiple streams, you should use `meshopt_optimizeVertexFetchRemap` instead. This optimization has to be performed on the final index buffer since the optimal vertex order depends on the triangle order.
Note that the algorithm does not try to model cache replacement precisely and instead just orders vertices in the order of use, which generally produces results that are close to optimal.
### Vertex quantization
To optimize memory bandwidth when fetching the vertex data even further, and to reduce the amount of memory required to store the mesh, it is often beneficial to quantize the vertex attributes to smaller types. While this optimization can technically run at any part of the pipeline (and sometimes doing quantization as the first step can improve indexing by merging almost identical vertices), it generally is easier to run this after all other optimizations since some of them require access to float3 positions.
Quantization is usually domain specific; it's common to quantize normals using 3 8-bit integers but you can use higher-precision quantization (for example using 10 bits per component in a 10_10_10_2 format), or a different encoding to use just 2 components. For positions and texture coordinate data the two most common storage formats are half precision floats, and 16-bit normalized integers that encode the position relative to the AABB of the mesh or the UV bounding rectangle.
The number of possible combinations here is very large but this library does provide the building blocks, specifically functions to quantize floating point values to normalized integers, as well as half-precision floats. For example, here's how you can quantize a normal using 10-10-10 SNORM encoding:
```c++
unsigned int normal =
((meshopt_quantizeSnorm(v.nx, 10) & 1023) << 20) |
((meshopt_quantizeSnorm(v.ny, 10) & 1023) << 10) |
(meshopt_quantizeSnorm(v.nz, 10) & 1023);
```
and here's how you can quantize a position using half precision floats:
```c++
unsigned short px = meshopt_quantizeHalf(v.x);
unsigned short py = meshopt_quantizeHalf(v.y);
unsigned short pz = meshopt_quantizeHalf(v.z);
```
Since quantized vertex attributes often need to remain in their compact representations for efficient transfer and storage, they are usually dequantized during vertex processing by configuring the GPU vertex input correctly to expect normalized integers or half precision floats, which often needs no or minimal changes to the shader code. When CPU dequantization is required instead, `meshopt_dequantizeHalf` can be used to convert half precision values back to single precision; for normalized integer formats, the dequantization just requires dividing by 2^N-1 for unorm and 2^(N-1)-1 for snorm variants. For example, manually reversing `meshopt_quantizeUnorm(v, 10)` can be done by dividing by 1023.
### Index filtering
Some meshes may contain triangles that are processed during rendering but do not contribute to the rendered result. If any two vertices of a triangle result in the same position after vertex shader, the triangle is degenerate and will be skipped by the rasterizer. Some triangles may also be duplicates of an earlier triangle with the same post-transform positions and winding, in which case only one of the triangles will be visible depending on depth testing settings (assuming blending is disabled). In either case, such triangles require extra processing and removing them may improve rasterization or ray tracing performance; this library provides an algorithm that removes such triangles from the index buffer:
```c++
indices.resize(meshopt_filterIndexBuffer(&indices[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(float) * 3, sizeof(Vertex)));
```
Note that the example above assumes only positions are relevant for transforming the vertices, but for deformable meshes skinning data may need to be added to the vertex portion used as a key; `meshopt_filterIndexBufferMulti` can be useful for these cases if the relevant data is not contiguous.
Filtering after quantization is convenient because quantization may increase the number of redundant triangles if triangles had similar but not identical vertex positions before quantization. However, filtering can be done at any point in the pipeline as soon as the index buffer becomes available; you could also run vertex fetch optimization after filtering, since it will naturally filter out any vertices that may become unused after redundant triangles are eliminated, potentially saving extra memory.
### Shadow indexing
Many rendering pipelines require meshes to be rendered to depth-only targets, such as shadow maps or during a depth pre-pass, in addition to color/G-buffer targets. While using the same geometry data for both cases is possible, reducing the number of unique vertices for depth-only rendering can be beneficial, especially when the source geometry has many attribute seams due to faceted shading or lightmap texture seams.
To achieve this, this library provides the `meshopt_generateShadowIndexBuffer` algorithm, which generates a second (shadow) index buffer that can be used with the original vertex data:
```c++
std::vector<unsigned int> shadow_indices(index_count);
// note: this assumes Vertex starts with float3 positions and should be adjusted accordingly for quantized positions
meshopt_generateShadowIndexBuffer(&shadow_indices[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(float) * 3, sizeof(Vertex));
```
Because the vertex data is shared, shadow indexing should be done after other optimizations of the vertex/index data. However, it's possible (and recommended) to optimize the resulting shadow index buffer for vertex cache:
```c++
meshopt_optimizeVertexCache(&shadow_indices[0], &shadow_indices[0], index_count, vertex_count);
```
In some cases, it may be beneficial to split the vertex positions into a separate buffer to maximize efficiency for depth-only rendering. Note that the example above assumes only positions are relevant for shadow rendering, but more complex materials may require adding texture coordinates (for alpha testing) or skinning data to the vertex portion used as a key. `meshopt_generateShadowIndexBufferMulti` can be useful for these cases if the relevant data is not contiguous.
Note that for meshes with optimal indexing and few attribute seams, the shadow index buffer will be very similar to the original index buffer, so it may not be always worth generating a separate shadow index buffer even if the rendering pipeline relies on depth-only passes.
## Clusterization
While traditionally meshes have served as a unit of rendering, new approaches to rendering and raytracing are starting to use a smaller unit of work, such as clusters or meshlets. This allows more freedom in how the geometry is processed, and can lead to better performance and more efficient use of GPU hardware. This section describes algorithms designed to work with meshes as sets of clusters.
### Mesh shading
Modern GPUs are beginning to deviate from the traditional rasterization model. NVidia GPUs starting from Turing and AMD GPUs starting from RDNA2 provide a new programmable geometry pipeline that, instead of being built around index buffers and vertex shaders, is built around mesh shaders - a new shader type that allows to provide a batch of work to the rasterizer.
Using mesh shaders in context of traditional mesh rendering provides an opportunity to use a variety of optimization techniques, starting from more efficient vertex reuse, using various forms of culling (e.g. cluster frustum or occlusion culling) and in-memory compression to maximize the utilization of GPU hardware. Beyond traditional rendering mesh shaders provide a richer programming model that can synthesize new geometry more efficiently than common alternatives such as geometry shaders. Mesh shading can be accessed via Vulkan or Direct3D 12 APIs; please refer to [Introduction to Turing Mesh Shaders](https://developer.nvidia.com/blog/introduction-turing-mesh-shaders/) and [Mesh Shaders and Amplification Shaders: Reinventing the Geometry Pipeline](https://devblogs.microsoft.com/directx/coming-to-directx-12-mesh-shaders-and-amplification-shaders-reinventing-the-geometry-pipeline/) for additional information.
To use mesh shaders for conventional rendering efficiently, geometry needs to be converted into a series of meshlets; each meshlet represents a small subset of the original mesh and comes with a small set of vertices and a separate micro-index buffer that references vertices in the meshlet. This information can be directly fed to the rasterizer from the mesh shader. This library provides algorithms to create meshlet data for a mesh, and - assuming geometry is static - can compute bounding information that can be used to perform cluster culling, rejecting meshlets that are invisible on screen.
To generate meshlet data, this library provides `meshopt_buildMeshlets` algorithm, which tries to balance topological efficiency (by maximizing vertex reuse inside meshlets) with culling efficiency (by minimizing meshlet radius and triangle direction divergence) and produces GPU-friendly data. As an alternative (that can be useful for load-time processing), `meshopt_buildMeshletsScan` can create the meshlet data using a vertex cache-optimized index buffer as a starting point by greedily aggregating consecutive triangles until they go over the meshlet limits. `meshopt_buildMeshlets` is recommended for offline data processing even if cone culling is not used.
```c++
const size_t max_vertices = 64;
const size_t max_triangles = 126; // note: in v0.25 or prior, max_triangles needs to be divisible by 4
const float cone_weight = 0.0f;
size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, max_triangles);
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
std::vector<unsigned int> meshlet_vertices(indices.size());
std::vector<unsigned char> meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3
size_t meshlet_count = meshopt_buildMeshlets(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(),
indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, max_triangles, cone_weight);
```
To generate the meshlet data, `max_vertices` and `max_triangles` need to be set within limits supported by the hardware; for NVidia the values of 64 and 126 are recommended. `cone_weight` should be left as 0 if cluster cone culling is not used, and set to a value between 0 and 1 to balance cone culling efficiency with other forms of culling like frustum or occlusion culling (`0.25` is a reasonable default).
> Note that for earlier AMD GPUs, the best configurations tend to use the same limits for `max_vertices` and `max_triangles`, such as 64 and 64, or 128 and 128. Additionally, while NVidia recommends 64/126 as a good configuration, consider using a different configuration like `max_vertices 64, max_triangles 96`, to provide more realistic limits that are achievable on real-world meshes, and to reduce the overhead on other GPUs.
Each resulting meshlet refers to a portion of `meshlet_vertices` and `meshlet_triangles` arrays; the arrays are overallocated for the worst case so it's recommended to trim them before saving them as an asset / uploading them to the GPU:
```c++
const meshopt_Meshlet& last = meshlets[meshlet_count - 1];
meshlet_vertices.resize(last.vertex_offset + last.vertex_count);
meshlet_triangles.resize(last.triangle_offset + last.triangle_count * 3);
meshlets.resize(meshlet_count);
```
Depending on the application, other strategies of storing the data can be useful; for example, `meshlet_vertices` serves as indices into the original vertex buffer but it might be worthwhile to generate a mini vertex buffer for each meshlet to remove the extra indirection when accessing vertex data, or it might be desirable to compress vertex data as vertices in each meshlet are likely to be very spatially coherent.
> Some proprietary platforms have additional restrictions on the index range that can be referenced by a meshlet. Building the library with `MESHOPTIMIZER_CLUSTERIZER_INDEXLIMIT` defined will ensure that generated meshlets conform to these restrictions, but the resulting meshlets can *not* be processed further as additional reordering may break the limits.
For optimal performance, it is recommended to further optimize each meshlet in isolation for better triangle and vertex locality by calling `meshopt_optimizeMeshlet` on vertex and index data like so:
```c++
meshopt_optimizeMeshlet(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset], m.triangle_count, m.vertex_count);
```
Different applications will choose different strategies for rendering meshlets; on a GPU capable of mesh shading, meshlets can be rendered directly; for example, a basic GLSL shader for `VK_EXT_mesh_shader` extension could look like this (parts omitted for brevity):
```glsl
layout(binding = 0) readonly buffer Meshlets { Meshlet meshlets[]; };
layout(binding = 1) readonly buffer MeshletVertices { uint meshlet_vertices[]; };
layout(binding = 2) readonly buffer MeshletTriangles { uint8_t meshlet_triangles[]; };
void main() {
Meshlet meshlet = meshlets[gl_WorkGroupID.x];
SetMeshOutputsEXT(meshlet.vertex_count, meshlet.triangle_count);
for (uint i = gl_LocalInvocationIndex; i < meshlet.vertex_count; i += gl_WorkGroupSize.x) {
uint index = meshlet_vertices[meshlet.vertex_offset + i];
gl_MeshVerticesEXT[i].gl_Position = world_view_projection * vec4(vertex_positions[index], 1);
}
for (uint i = gl_LocalInvocationIndex; i < meshlet.triangle_count; i += gl_WorkGroupSize.x) {
uint offset = meshlet.triangle_offset + i * 3;
gl_PrimitiveTriangleIndicesEXT[i] = uvec3(
meshlet_triangles[offset], meshlet_triangles[offset + 1], meshlet_triangles[offset + 2]);
}
}
```
> Note that DirectX 12 mesh shaders cannot index raw buffers using arbitrary byte offsets. Use a typed SRV buffer (`Buffer<uint>`) with `DXGI_FORMAT_R8_UINT` format, repack each triangle to 32 bits to be able to use aligned 32-bit loads with `ByteAddressBuffer`, or consider utilizing [16-bit scalar types](https://github.com/microsoft/DirectXShaderCompiler/wiki/16-Bit-Scalar-Types) to load a 3-byte triangle using two aligned 16-bit loads like so: `Buffer.Load<uint16_t2>(triangle_offset & ~1)`, then extracting indices with bitwise operations based on `triangle_offset & 1`.
After generating the meshlet data, it's possible to generate extra data for each meshlet that can be saved and used at runtime to perform cluster culling, where each meshlet can be discarded if it's guaranteed to be invisible. To generate the data, `meshopt_computeMeshletBounds` can be used:
```c++
meshopt_Bounds bounds = meshopt_computeMeshletBounds(&meshlet_vertices[m.vertex_offset], &meshlet_triangles[m.triangle_offset],
m.triangle_count, &vertices[0].x, vertices.size(), sizeof(Vertex));
```
The resulting `bounds` values can be used to perform frustum or occlusion culling using the bounding sphere, or cone culling using the cone axis/angle (which will reject the entire meshlet if all triangles are guaranteed to be back-facing from the camera point of view):
```c++
if (dot(normalize(cone_apex - camera_position), cone_axis) >= cone_cutoff) reject();
```
Cluster culling should ideally run at a lower frequency than mesh shading, either using amplification/task shaders, or using a separate compute dispatch.
By default, the meshlet builder tries to form complete meshlets even if that requires merging disconnected regions of the mesh into a single meshlet. In some cases, such as hierarchical level of detail, or when advanced culling is used, it may be beneficial to prioritize spatial locality of triangles in a meshlet even if that results in partially filled meshlets. To that end, `meshopt_buildMeshletsFlex` function can be used instead of `meshopt_buildMeshlets`; it provides two triangle limits, `min_triangles` and `max_triangles`, and uses an additional configuration parameter, `split_factor` (recommended value is 2.0), to decide whether increasing the meshlet radius is worth it to fit more triangles in the meshlet. When using this function, the worst case bound for the number of meshlets has to be computed using `meshopt_buildMeshletsBound` with `min_triangles` parameter instead of `max_triangles`.
### Clustered raytracing
In addition to rasterization, meshlets can also be used for ray tracing. NVidia GPUs starting from Turing with recent drivers provide support for cluster acceleration structures (via `VK_NV_cluster_acceleration_structure` extension / NVAPI); instead of building a traditional BLAS, a cluster acceleration structure can be built for each meshlet and combined into a single clustered BLAS. While this currently results in reduced ray tracing performance for static geometry (for which a traditional BLAS may be more suitable), it allows updating the individual clusters without having to rebuild or refit the entire BLAS, which can be useful for mesh deformation or hierarchical level of detail.
When using meshlets for raytracing, the performance characteristics that matter differ from when rendering meshes with rasterization. For raytracing, clusters with optimal spatial division that minimize ray-triangle intersection tests are preferred, while for rasterization, clusters with maximum triangle count within vertex limits are ideal.
To generate meshlets optimized for raytracing, this library provides `meshopt_buildMeshletsSpatial` algorithm, which builds clusters using surface area heuristic (SAH) to produce raytracing-friendly cluster distributions:
```c++
const size_t max_vertices = 64;
const size_t min_triangles = 16;
const size_t max_triangles = 64;
const float fill_weight = 0.5f;
size_t max_meshlets = meshopt_buildMeshletsBound(indices.size(), max_vertices, min_triangles); // note: use min_triangles to compute worst case bound
std::vector<meshopt_Meshlet> meshlets(max_meshlets);
std::vector<unsigned int> meshlet_vertices(indices.size());
std::vector<unsigned char> meshlet_triangles(indices.size()); // note: in v0.25 or prior, use indices.size() + max_meshlets * 3
size_t meshlet_count = meshopt_buildMeshletsSpatial(meshlets.data(), meshlet_vertices.data(), meshlet_triangles.data(), indices.data(),
indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex), max_vertices, min_triangles, max_triangles, fill_weight);
```
The algorithm recursively subdivides the triangles into a BVH-like hierarchy using SAH for optimal spatial partitioning while balancing cluster size; this results in clusters that are significantly more efficient to raytrace compared to clusters generated by `meshopt_buildMeshlets`, but can still be used for rasterization (for example, to build visibility buffers or G-buffers).
The `min_triangles` and `max_triangles` parameters control the allowed range of triangles per cluster. For optimal raytracing performance, `min_triangles` should be at most `max_triangles/2` (or, ideally, `max_triangles/4`) to give the algorithm enough freedom to produce high-quality spatial partitioning. For meshes with few seams due to normal or UV discontinuities, using `max_vertices` equal to `max_triangles` is recommended when rasterization performance is a concern; for meshes with many seams or for renderers that primarily use meshlets for ray tracing, a higher `max_vertices` value should be used as it ensures that more clusters can fully utilize the triangle limit.
The `fill_weight` parameter (typically between 0 and 1, although values higher than 1 could be used to prioritize cluster fill even more) controls the trade-off between pure SAH optimization and triangle utilization. A value of 0 will optimize purely for SAH, resulting in best raytracing performance but potentially smaller clusters. Values between 0.25 and 0.75 typically provide a good balance of SAH quality vs triangle count.
When the resulting meshlets are used to generate hardware-specific acceleration structures, using fast trace (e.g. `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR`) builds results in maximum performance; if build performance is important, using `meshopt_optimizeMeshlet` can help improve ray tracing performance when using fast build (e.g. `VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_BUILD_BIT_KHR`), although the tracing performance will still be lower than with fast trace builds.
### Point cloud clusterization
Both of the meshlet algorithms are designed to work with triangle meshes. In some cases, splitting a point cloud into fixed size clusters can be useful; the resulting point clusters could be rendered via mesh or compute shaders, or the resulting subdivision can be used to parallelize point processing while maintaining locality of points. To that end, this library provides `meshopt_spatialClusterPoints` algorithm:
```c++
const size_t cluster_size = 256;
std::vector<unsigned int> index(vertices.size());
meshopt_spatialClusterPoints(&index[0], &vertices[0].px, vertices.size(), sizeof(Vertex), cluster_size);
```
The resulting index buffer could be used to process the points directly, or reorganize the point data into flat contiguous arrays. Every consecutive chunk of `cluster_size` points in the index buffer refers to a single cluster, with just the last cluster containing fewer points if the total number of points is not a multiple of `cluster_size`. Note that the index buffer is not a remap table, so `meshopt_remapVertexBuffer` can't be used to flatten the point data.
### Cluster partitioning
When working with clustered geometry, it can be beneficial to organize clusters into larger groups (partitions) for more efficient processing or workload distribution. This library provides an algorithm to partition clusters into groups of similar size while prioritizing locality:
```c++
const size_t partition_size = 24;
std::vector<unsigned int> cluster_partitions(cluster_count);
size_t partition_count = meshopt_partitionClusters(&cluster_partitions[0], &cluster_indices[0], total_index_count,
&cluster_index_counts[0], cluster_count, &vertices[0].x, vertex_count, sizeof(Vertex), partition_size);
```
The algorithm assigns each cluster to a partition, aiming for a target partition size while prioritizing topological locality (sharing vertices) and spatial locality. The resulting partitions can be used for more efficient batched processing of clusters, or for hierarchical simplification schemes similar to Nanite.
Two clusters are considered topologically adjacent if they reference the same indices. In some cases, it can be helpful to process the indices using `meshopt_generateShadowIndexBuffer` (or remap them manually using the remap table generated by `meshopt_generatePositionRemap`), which allows clusters to be considered adjacent even when boundary vertices have different indices due to attribute discontinuities.
If vertex positions are specified (not `NULL`), spatial locality will influence priority of merging clusters; otherwise, the algorithm will rely solely on topological connections and will not merge disconnected clusters into the same partition, which may result in smaller partitions for some inputs.
After partitioning, each element in the destination array contains the partition ID (ranging from 0 to the returned partition count minus 1) for the corresponding cluster. Note that the partitions may be both smaller and larger than the target size; given a target size, the maximum partition size returned currently is `target + target / 3`.
### Cluster position quantization
When working with clustered geometry, it may be possible to use cluster-relative quantization for vertex positions. In particular, a common representation involves quantizing positions to lie on an integer grid with an exponent shared between clusters, to avoid quantizing shared vertices differently between clusters, and storing each position as an offset from the cluster anchor. A variant of this representation called [Compressed1 position encoding](https://microsoft.github.io/DirectX-Specs/d3d/Raytracing2.html#compressed1-position-encoding) is supported by DXR2. This library provides a helper function, `meshopt_computePositionExponent`, that can be used to compute the exponent, as well as an example (see `encodeMeshletsDXR` in `demo/main.cpp`) demonstrating the full pipeline.
First, compute the shared exponent to guarantee that any vertex in every cluster fits in a 24-bit signed integer, and additionally can be encoded as a K-bit unsigned delta from the per-cluster anchor (in the example below, `K=16` which matches DXR2 limits):
```c++
const int min_exp = -13; // start with an acceptable level of precision; 2^-13 ~= 0.1mm in metric units
const int max_bits = 16; // maximum number of offset bits per axis
int exponent = min_exp;
for (size_t i = 0; i < meshlets.size(); ++i)
{
AABB meshlet_aabb = ...; // compute meshlet AABB based on vertex positions
int cexp = meshopt_computePositionExponent(meshlet_aabb.min, meshlet_aabb.max, min_exp, max_bits);
exponent = std::max(exponent, cexp);
}
float scale = ldexpf(1.f, exponent);
```
After this, each cluster can be encoded independently; when encoding each position, positions should be converted to the integer grid, and the anchor should be established:
```c++
int positions[256][3];
int anchor[3] = {INT_MAX, INT_MAX, INT_MAX};
for (size_t j = 0; j < meshlet.vertex_count; ++j)
{
unsigned int v = meshlet_vertices[meshlet.vertex_offset + j];
const float* p = &vertices[v].px;
for (int k = 0; k < 3; ++k)
{
positions[j][k] = int(roundf(p[k] / scale));
anchor[k] = std::min(anchor[k], positions[j][k]);
}
}
```
The exponent selection guarantees that as a result, for each cluster, we get 24-bit `anchor` components, and for each vertex `position[k] - anchor[k]` fits into a 16-bit unsigned integer. The positions *could* then be encoded using a fixed-width 16-bit encoding, with 6 bytes per vertex; however, because many clusters will require fewer bits, it's more efficient to determine the bit count per axis per cluster (by computing the number of bits per axis that is sufficient to fit `position[k] - anchor[k]`), and encoding the resulting deltas from the anchor into a bitstream. When using DXR2, the resulting position bits can be given directly to the cluster BVH builder by using `D3D12_VERTEX_FORMAT_COMPRESSED1` format.
The same data could be directly decoded from a mesh shader in hybrid rendering pipelines, since each cluster uses a consistent number of bits per axis; this would require implementing unaligned bitstream read access in the shader. Alternatively, deltas could be stored as 16-bit integers alongside other vertex data (taking as much space as `snorm16`/`half` positions with significantly increased precision for larger meshes).
> In the examples above, `max_bits` is set to 16 to match DXR2 limits. If DXR2 compatibility is not required, it may sometimes be necessary to use more bits to accommodate larger clusters. This is particularly relevant when using hierarchical cluster LOD, as all clusters at all levels of detail must share the exponent to eliminate gaps between adjacent clusters at different resolutions; Nanite uses up to 21 bits per axis for this reason.
This scheme works well together with meshlet compression described below, which can be used to encode topology (triangles) efficiently.
## Mesh compression
In case storage size or transmission bandwidth is of importance, you might want to additionally compress vertex and index data. While several mesh compression libraries, like Google Draco, are available, they typically are designed to maximize the compression ratio at the cost of disturbing the vertex/index order (which makes the meshes inefficient to render on GPU) or decompression performance. They also frequently don't support custom game-ready quantized vertex formats and thus require to re-quantize the data after loading it, introducing extra quantization errors and making decoding slower.
Alternatively you can use general purpose compression libraries like zstd or Oodle to compress vertex/index data - however these compressors aren't designed to exploit redundancies in vertex/index data and as such compression rates can be unsatisfactory.
To that end, this library provides algorithms to "encode" vertex and index data. The result of the encoding is generally significantly smaller than initial data, and remains compressible with general purpose compressors - so you can either store encoded data directly (for modest compression ratios and maximum decoding performance), or further compress it with LZ4/zstd/Oodle to maximize compression ratio.
> Note: this compression scheme is available as a glTF extension [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) as well as [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/pull/2517).
### Vertex compression
This library provides a lossless algorithm to encode/decode vertex data. To encode vertices, you need to allocate a target buffer (using the worst case bound) and call the encoding function:
```c++
std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(vertex_count, sizeof(Vertex)));
vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), vertices, vertex_count, sizeof(Vertex)));
```
To decode the data at runtime, call the decoding function:
```c++
int res = meshopt_decodeVertexBuffer(vertices, vertex_count, sizeof(Vertex), &vbuf[0], vbuf.size());
assert(res == 0);
```
Note that vertex encoding assumes that vertex buffer was optimized for vertex fetch, and that vertices are quantized. Feeding unoptimized data into the encoder may result in poor compression ratios. The codec is lossless by itself - the only lossy step is quantization/reordering or filters that you may apply before encoding. Additionally, if the vertex data contains padding bytes, they should be zero-initialized to ensure that the encoder does not need to store uninitialized data.
Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 3-6 GB/s on modern desktop CPUs. Compression ratio depends on the data; vertex data compression ratio is typically around 2-4x (compared to already quantized and optimally packed data). General purpose lossless compressors can further improve the compression ratio at some cost to decoding performance. If decoding always targets regular cached memory, compiling the library with `MESHOPTIMIZER_VERTEXCODEC_ZEROCOPY` define can yield additional ~10% decoding performance.
The vertex codec tries to take advantage of the inherent locality of sequential vertices and identify bit patterns that repeat in consecutive vertices. Typically, vertex cache + vertex fetch provides a reasonably local vertex traversal order; without an index buffer, it is recommended to sort vertices spatially (via `meshopt_spatialSortRemap`) to improve the compression ratio.
It is crucial to correctly specify the stride when encoding vertex data; however, for compression ratio it does not matter whether the vertices are interleaved or deinterleaved, as the codecs perform full byte deinterleaving internally. The stride of each stream must be a multiple of 4 bytes.
For optimal compression results, the values should be quantized to small integers. It can be valuable to use bit counts that are not multiples of 8. For example, instead of using 16 bits to represent texture coordinates, use 12-bit integers and divide by 4095 in the shader. Alternatively, using half-precision floats can often achieve good results.
For single-precision floating-point data, it's recommended to use `meshopt_quantizeFloat` to remove entropy from the lower bits of the mantissa; for best results, consider using 15 bits or 7 bits for extreme compression.
For normal or tangent vectors, using octahedral encoding is recommended over three components as it reduces redundancy; similarly, consider using 10-12 bits per component instead of 16.
When data is bit packed, specifying compression level 3 (via `meshopt_encodeVertexBufferLevel`) can improve the compression further by redistributing bits between components.
### Index compression
This library also provides algorithms to encode/decode index data. To encode triangle indices, you need to allocate a target buffer (using the worst case bound) and call the encoding function:
```c++
std::vector<unsigned char> ibuf(meshopt_encodeIndexBufferBound(index_count, vertex_count));
ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), indices, index_count));
```
To decode the data at runtime, call the decoding function:
```c++
int res = meshopt_decodeIndexBuffer(indices, index_count, &ibuf[0], ibuf.size());
assert(res == 0);
```
Note that index encoding assumes that the index buffer was optimized for vertex cache and vertex fetch. Feeding unoptimized data into the encoder will result in poor compression ratios. Codec preserves the order of triangles, however it can rotate each triangle to improve compression ratio (which means the provoking vertex may change).
Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 3-6 GB/s on modern desktop CPUs.
The index codec targets 1 byte per triangle as a best case (6x smaller than raw 16-bit index data); on real-world meshes, it's typical to achieve 1-1.2 bytes per triangle. To reach this, the index data needs to be optimized for vertex cache and vertex fetch. Optimizations that do not disrupt triangle locality (such as overdraw) are safe to use in between.
To reduce the data size further, it's possible to use `meshopt_optimizeVertexCacheStrip` instead of `meshopt_optimizeVertexCache` when optimizing for vertex cache. This trades off some efficiency in vertex transform for smaller index (and sometimes vertex) data.
When referenced vertex indices are not sequential, the index codec will use around 2 bytes per index. This can happen when the referenced vertices are a sparse subset of the vertex buffer, such as when encoding LODs. General-purpose compression can be especially helpful in this case.
Index buffer codec only supports triangle list topology; when encoding triangle strips or line lists, use `meshopt_encodeIndexSequence`/`meshopt_decodeIndexSequence` instead. This codec typically encodes indices into ~1 byte per index, but compressing the results further with a general purpose compressor can improve the results to 1-3 bits per index.
### Meshlet compression
When using mesh shading or clustered raytracing, meshlet vertex reference and triangle data can be compressed similarly to index data. This library provides a dedicated codec that exploits locality inherent in meshlet data. Unlike vertex and index buffer codecs that work on entire buffers, the meshlet codec encodes each meshlet independently; this allows applications to have more flexibility in structuring the runtime storage and adjust the decoded data during decoding. This also means that in some applications, additional data describing the meshlet (vertex/triangle count, encoded size) will need to be encoded into the meshlet stream, if it isn't already available during decoding.
To encode a meshlet, you need to allocate a target buffer (using the worst case bound) and call the encoding function with the vertex index references and micro-index buffer, as produced by `meshopt_buildMeshlets`:
```c++
std::vector<unsigned char> mbuf(meshopt_encodeMeshletBound(max_vertices, max_triangles));
for (const meshopt_Meshlet& m : meshlets)
{
size_t msize = meshopt_encodeMeshlet(&mbuf[0], mbuf.size(),
&meshlet_vertices[m.vertex_offset], m.vertex_count, &meshlet_triangles[m.triangle_offset], m.triangle_count);
// write m.vertex_count, m.triangle_count, msize and mbuf[0..msize-1] to the output stream
}
```
To decode the data at runtime, call the decoding function:
```c++
uint16_t* vertices = ...;
uint8_t* triangles = ...;
// automatically deduces `vertex_size=2` and `triangle_size=3` based on pointer types
int res = meshopt_decodeMeshlet(vertices, m.vertex_count, triangles, m.triangle_count, stream, encoded_size);
assert(res == 0);
```
Vertex index references can be decoded as either 16-bit or 32-bit integers; triangle data can be decoded as 3 bytes per triangle (matching `meshopt_buildMeshlets` output format) or as a 32-bit integer per triangle (with indices packed as `a | (b << 8) | (c << 16)` and top byte unused). Output buffers must have available space aligned to 4 bytes; for example, decoding a 3-triangle stream using 3 bytes per triangle needs to be able to write 12 bytes to the output triangles array.
When using the C++ API, `meshopt_decodeMeshlet` will automatically deduce the element sizes based on the types of vertex and triangle pointers; when using the C API, the sizes need to be specified explicitly.
Decoder is heavily optimized and can directly target write-combined memory; you can expect it to run at 7-10 GB/s on modern desktop CPUs.
> Applications that do most of the streaming decompression on the GPU can also decode meshlet data on the GPU if CPU decoding is inconvenient; an example [meshletdec.slang](./demo/meshletdec.slang) shader is provided for 32-bit output format, and can be easily adapted to other formats, including custom ones.
Note that meshlet encoding assumes that the meshlet data was optimized; meshlets should be processed using `meshopt_optimizeMeshletLevel` with level 1 or higher (3 recommended for improved compression) before encoding. Additionally, vertex references should have a high degree of reference locality; this can be achieved by building meshlets from meshes optimized for vertex cache/fetch, or linearizing the vertex reference data and reordering the vertex buffer using `meshopt_optimizeVertexFetch`. Feeding unoptimized data into the encoder will result in poor compression ratios. Codec preserves the order of triangles, however it can rotate each triangle to improve compression ratio (which means the provoking vertex may change).
Meshlets without vertex references are supported; passing `NULL` vertices and `0` vertex count during encoding and decoding will produce encoded meshlets with just triangle data. Note that parameters supplied during decoding must match those used during encoding; if a meshlet was encoded with vertex references, it must be decoded with the same number of vertex references.
The meshlet codec targets 5-7 bits per triangle for triangle data; when vertex references are encoded, the encoded size strongly depends on how linear the references are, but it's typical to see 9-12 bits per triangle in aggregate. To reduce the compressed size further, it's possible to compress the resulting encoded data with a general purpose compressor, which usually achieves 5-8 bits/triangle in aggregate; note that in this case general purpose compressors should be applied to a stream with many encoded meshlets at once to amortize their overhead.
### Point cloud compression
The vertex encoding algorithms can be used to compress arbitrary streams of attribute data; one other use case besides triangle meshes is point cloud data. Typically point clouds come with position, color and possibly other attributes but don't have an implied point order.
To compress point clouds efficiently, it's recommended to first preprocess the points by sorting them using the spatial sort algorithm:
```c++
std::vector<unsigned int> remap(point_count);
meshopt_spatialSortRemap(&remap[0], positions, point_count, sizeof(vec3));
// for each attribute stream
meshopt_remapVertexBuffer(positions, positions, point_count, sizeof(vec3), &remap[0]);
```
After this the resulting arrays should be quantized (e.g. using 16-bit fixed point numbers for positions and 8-bit color components), and the result can be compressed using `meshopt_encodeVertexBuffer` as described in the previous section. To decompress, `meshopt_decodeVertexBuffer` will recover the quantized data that can be used directly or converted back to original floating-point data. The compression ratio depends on the nature of source data, for colored points it's typical to get 35-40 bits per point.
### Vertex filters
To further leverage the inherent structure of some vertex data, it's possible to use filters that encode and decode the data in a lossy manner. This is similar to quantization but can be used without having to change the shader code. After decoding, the filter transformation needs to be reversed. For native game engine pipelines, it is usually more optimal to carefully prequantize and pretransform the vertex data, but sometimes (for example when serializing data in glTF format) this is not a practical option and filters are more convenient. This library provides four filters:
- Octahedral filter (`meshopt_encodeFilterOct`/`meshopt_decodeFilterOct`) encodes quantized (snorm) normal or tangent vectors using octahedral encoding. Any number of bits between 2 and 16 can be used with 4 bytes or 8 bytes per vector.
- Quaternion filter (`meshopt_encodeFilterQuat`/`meshopt_decodeFilterQuat`) encodes quantized (snorm) quaternion vectors; this can be used to encode rotations or tangent frames. Any number of bits between 4 and 16 can be used with 8 bytes per vector.
- Exponential filter (`meshopt_encodeFilterExp`/`meshopt_decodeFilterExp`) encodes single-precision floating-point vectors; this can be used to encode arbitrary floating-point data more efficiently. In addition to an arbitrary bit count (<= 24), the filter takes a "mode" parameter that allows specifying how the exponent sharing is performed to trade off compression ratio and quality:
- `meshopt_EncodeExpSeparate` does not share exponents and results in the largest output
- `meshopt_EncodeExpSharedVector` shares exponents between different components of the same vector
- `meshopt_EncodeExpSharedComponent` shares exponents between the same component in different vectors
- `meshopt_EncodeExpClamped` does not share exponents but clamps the exponent range to reduce exponent entropy
- Color filter (`meshopt_encodeFilterColor`/`meshopt_decodeFilterColor`) encodes quantized (unorm) RGBA colors using YCoCg encoding. Any number of bits between 2 and 16 can be used with 4 bytes or 8 bytes per vector.
Note that all filters are lossy and require the data to be deinterleaved with one attribute per stream; this facilitates efficient SIMD implementation of filter decoders, which decodes at 5-10 GB/s on modern desktop CPUs, allowing the overall decompression speed to be closer to that of the raw vertex codec.
### Versioning and compatibility
The following guarantees on data compatibility are provided for point releases (*no* guarantees are given for development branch):
- Data encoded with older versions of the library can always be decoded with newer versions;
- Data encoded with newer versions of the library can be decoded with older versions, provided that encoding versions are set correctly; if binary stability of encoded data is important, use `meshopt_encodeVertexVersion` and `meshopt_encodeIndexVersion` to 'pin' the data versions (or `version` argument of `meshopt_encodeVertexBufferLevel`).
By default, vertex data is encoded for format version 1 (compatible with meshoptimizer v0.23+), and index data is encoded for format version 1 (compatible with meshoptimizer v0.14+). When decoding the data, the decoder will automatically detect the version from the data header.
Meshlet data uses a frameless format without an embedded version header, so it's compatible across all library versions (starting with meshoptimizer v1.1). Any future change to meshlet encoding would be exposed as new functions; to adopt future improvements without breaking existing data, applications could record a format version externally, once per entire meshlet stream or file.
## Simplification
All algorithms presented so far don't affect visual appearance at all, with the exception of quantization that has minimal controlled impact. However, fundamentally the most effective way to reduce the rendering or transmission cost of a mesh is to reduce the number of triangles in the mesh.
### Basic simplification
This library provides a simplification algorithm, `meshopt_simplify`, that reduces the number of triangles in the mesh. Given a vertex and an index buffer, it generates a second index buffer that uses existing vertices in the vertex buffer. This index buffer can be used directly for rendering with the original vertex buffer (preferably after vertex cache optimization using `meshopt_optimizeVertexCache`), or a new compact vertex/index buffer can be generated using `meshopt_optimizeVertexFetch` that uses the optimal number and order of vertices.
```c++
float threshold = 0.2f;
size_t target_index_count = size_t(index_count * threshold);
float target_error = 1e-2f;
std::vector<unsigned int> lod(index_count);
float lod_error = 0.f;
lod.resize(meshopt_simplify(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex),
target_index_count, target_error, /* options= */ 0, &lod_error));
```
Target error is an approximate measure of the deviation from the original mesh using distance normalized to `[0..1]` range (e.g. `1e-2f` means that simplifier will try to maintain the error to be below 1% of the mesh extents). Note that the simplifier attempts to produce the requested number of indices at minimal error, but because of topological restrictions and error limit it is not guaranteed to reach the target index count and can stop earlier.
To disable the error limit, `target_error` can be set to `FLT_MAX`. This makes it more likely that the simplifier will reach the target index count, but it may produce a mesh that looks significantly different from the original, so using the resulting error to control viewing distance would be required. Conversely, setting `target_index_count` to 0 will simplify the input mesh as much as possible within the specified error limit; this can be useful for generating LODs that should look good at a given viewing distance.
The algorithm follows the topology of the original mesh in an attempt to preserve attribute seams, borders and overall appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully. Therefore it's critical that identical vertices are "welded" together, that is, the input vertex buffer does not contain duplicates. Additionally, it may be worthwhile to weld the vertices without taking into account vertex attributes that aren't critical and can be rebuilt later, or use "permissive" mode described below.
Alternatively, the library provides another simplification algorithm, `meshopt_simplifySloppy`, which doesn't follow the topology of the original mesh. This means that it doesn't preserve attribute seams or borders, but it can collapse internal details that are too small to matter because it can merge mesh features that are topologically disjoint but spatially close. In general, this algorithm produces meshes with worse geometric quality and poor attribute quality compared to `meshopt_simplify`.
The algorithm can also return the resulting normalized deviation that can be used to choose the correct level of detail based on screen size or solid angle; the error can be converted to object space by multiplying by the scaling factor returned by `meshopt_simplifyScale`. For example, given a mesh with a precomputed LOD and a prescaled error, the screen-space normalized error can be computed and used for LOD selection:
```c++
// lod_factor can be 1 or can be adjusted for more or less aggressive LOD selection
float d = max(0, distance(camera_position, mesh_center) - mesh_radius);
float e = d * (tan(camera_fovy / 2) * 2 / screen_height); // 1px in mesh space
bool lod_ok = e * lod_factor >= lod_error;
```
When a sequence of LOD meshes is generated that all use the original vertex buffer, care must be taken to order vertices optimally to not penalize mobile GPU architectures that are only capable of transforming a sequential vertex buffer range. It's recommended in this case to first optimize each LOD for vertex cache, then assemble all LODs in one large index buffer starting from the coarsest LOD (the one with fewest triangles), and call `meshopt_optimizeVertexFetch` on the final large index buffer. This will make sure that coarser LODs require a smaller vertex range and are efficient with respect to vertex fetch and transform.
### Attribute-aware simplification
While `meshopt_simplify` is aware of attribute discontinuities by default (and infers them through the supplied index buffer) and tries to preserve them, it can be useful to provide information about attribute values. This allows the simplifier to take attribute error into account which can improve shading (by using vertex normals), texture deformation (by using texture coordinates), and may be necessary to preserve vertex colors when textures are not used in the first place. This can be done by using a variant of the simplification function that takes attribute values and weight factors, `meshopt_simplifyWithAttributes`:
```c++
const float nrm_weight = 0.5f;
const float attr_weights[3] = {nrm_weight, nrm_weight, nrm_weight};
std::vector<unsigned int> lod(index_count);
float lod_error = 0.f;
lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex),
&vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL,
target_index_count, target_error, /* options= */ 0, &lod_error));
```
The attributes are passed as a separate buffer (in the example above it's a subset of the same vertex buffer) and should be stored as consecutive floats; attribute weights are used to control the importance of each attribute in the simplification process. For normalized attributes like normals and vertex colors, a weight around 1.0 is usually appropriate; internally, a change of `1/weight` in attribute value over a distance `d` is approximately equivalent to a change of `d` in position. Using higher weights may be appropriate to preserve attribute quality at the cost of position quality. If the attribute has a different scale (e.g. unnormalized vertex colors in [0..255] range), the weight should be divided by the scaling factor (1/255 in this example).
Including texture coordinates in the attribute set is optional, as simplification generally preserves texture quality reasonably well by default; if included, a weight of around 10-100 is usually appropriate depending on the UV density. It's also possible to compute the weight automatically by setting it to the reciprocal average density of UVs, which can be computed as `1/sqrt(average UV area)` = `1/sqrt(sum(abs(uv area)) / triangle count)` over all triangles in the mesh, possibly scaled by a constant factor if necessary.
Both the target error and the resulting error combine positional error and attribute error, so the error can be used to control the LOD while taking attribute quality into account, assuming carefully chosen weights. When the error is used for LOD selection, enabling `meshopt_SimplifyErrorClamped` is recommended to avoid large attribute errors producing overly conservative LOD selection.
### Permissive simplification
By default, `meshopt_simplify` preserves attribute discontinuities inferred from the supplied index buffer. For meshes with many seams, the simplifier can get "stuck" and fail to fully simplify the mesh, as it cannot collapse vertices across attribute seams. This is especially problematic for meshes with faceted normals (flat shading), as the simplifier may not be able to reduce the triangle count at all. The `meshopt_SimplifyPermissive` option relaxes these restrictions, allowing the simplifier to collapse vertices across attribute discontinuities when the resulting error is acceptable:
```c++
std::vector<unsigned int> lod(index_count);
float lod_error = 0.f;
lod.resize(meshopt_simplifyWithAttributes(&lod[0], indices, index_count, &vertices[0].x, vertex_count, sizeof(Vertex),
&vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL,
target_index_count, target_error, /* options= */ meshopt_SimplifyPermissive, &lod_error));
```
To maintain appearance, it's highly recommended to use this option together with attribute-aware simplification, as shown above, as it allows the simplifier to maintain attribute quality. In this mode, it is often desirable to selectively preserve certain attribute seams, such as UV seams or sharp creases. This can be achieved by using the `vertex_lock` array with flag `meshopt_SimplifyVertex_Protect` set for individual vertices to protect specific discontinuities. To fill this array, use `meshopt_generatePositionRemap` to create a mapping table for vertices with identical positions, and then compare each vertex to the remapped vertex to determine which attributes are different:
```c++
std::vector<unsigned int> remap(vertices.size());
meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex));
std::vector<unsigned char> locks(vertices.size());
for (size_t i = 0; i < vertices.size(); ++i) {
unsigned int r = remap[i];
if (r != i && (vertices[r].tx != vertices[i].tx || vertices[r].ty != vertices[i].ty))
locks[i] |= meshopt_SimplifyVertex_Protect; // protect UV seams
}
```
This approach provides fine-grained control over which discontinuities to preserve. The permissive mode combined with selective locking provides a balance between simplification quality and attribute preservation, and usually results in higher quality LODs for the same target triangle count (and dramatically higher quality compared to `meshopt_simplifySloppy`).
### Simplification with vertex update
All simplification functions described so far reuse the original vertex buffer and only produce a new index buffer. This means that the resulting mesh will have the same vertex positions and attributes as the original mesh; this is optimal for minimizing the memory consumption and for highly detailed meshes often provides good quality. However, for more aggressive simplification to retain visual quality, it may be necessary to adjust vertex data for optimal appearance. This can be done by using a variant of the simplification function that updates vertex positions and attributes, `meshopt_simplifyWithUpdate`:
```c++
float result_error = 0.f;
indices.resize(meshopt_simplifyWithUpdate(&indices[0], indices.size(), &vertices[0].px, vertices.size(), sizeof(Vertex),
&vertices[0].nx, sizeof(Vertex), attr_weights, 3, /* vertex_lock= */ NULL,
target_index_count, target_error, /* options= */ 0, &result_error));
```
Unlike `meshopt_simplify`/`meshopt_simplifyWithAttributes`, this function updates the index buffer as well as vertex positions and attributes in place. The resulting indices still refer to the original vertex buffer; any attributes that are not passed to the simplifier can be left unchanged. However, since the original contents of `vertices` is no longer valid for rendering the original mesh, a new compact vertex/index buffer should be generated using `meshopt_optimizeVertexFetch` (after optimizing the index data with `meshopt_optimizeVertexCache`). If the original data was important, it should be copied before calling this function.
Since the vertex positions are updated, this may require updating some attributes that could previously be left as-is when using the original vertex buffer. Notably, texture coordinates need to be updated to avoid texture distortion; thus it's highly recommended to include texture coordinates in the attribute data passed to the simplifier. For attributes to be updated, the corresponding attribute weight must not be zero; for texture coordinates, a weight of 1.0 is usually sufficient in this case (although a higher or mesh dependent weight could be used with this function or other functions to reduce UV stretching).
Attributes that have specific constraints like normals and colors should be renormalized or clamped after the function returns new data. Attributes like bone indices/weights don't have to be updated for reasonable results (but regularization via `meshopt_SimplifyRegularize` may still be helpful to maintain deformation quality). If bone weights *are* provided as attributes, they will need to be clamped and renormalized after the update to ensure they continue to add up to 1.
Using unique vertex data for each LOD in a chain can improve visual quality, but it comes at a cost of ~doubling vertex memory used (if each LOD is using half the triangles of the previous LOD). To reduce the memory footprint, it is possible to use shared vertices with `meshopt_simplifyWithAttributes` for the first one or two LODs in the chain, and only switch to `meshopt_simplifyWithUpdate` for the remainder. In that case, similarly to the use of `meshopt_simplify` described earlier, care must be taken to optimally arrange the vertices in the original vertex buffer.
### Advanced simplification
`meshopt_simplify*` functions expose additional options and parameters that can be used to control the simplification process in more detail.
For basic customization, a number of options can be passed via `options` bitmask that adjust the behavior of the simplifier:
- `meshopt_SimplifyLockBorder` restricts the simplifier from collapsing edges that are on the border of the mesh. This can be useful for simplifying mesh subsets independently, so that the LODs can be combined without introducing cracks.
- `meshopt_SimplifyErrorAbsolute` changes the error metric from relative to absolute both for the input error limit as well as for the resulting error. This can be used instead of `meshopt_simplifyScale`.
- `meshopt_SimplifySparse` improves simplification performance assuming input indices are a sparse subset of the mesh. This can be useful when simplifying small mesh subsets independently, and is intended to be used for meshlet simplification. For consistency, it is recommended to use absolute errors when sparse simplification is desired, as this flag changes the meaning of the relative errors.
- `meshopt_SimplifyPrune` allows the simplifier to remove isolated components regardless of the topological restrictions inside the component. This is generally recommended for full-mesh simplification as it can improve quality and reduce triangle count; note that with this option, triangles connected to locked vertices may be removed as part of their component.
- `meshopt_SimplifyRegularize` produces more regular triangle sizes and shapes during simplification, at some cost to geometric quality. This can improve geometric quality under deformation such as skinning. `meshopt_SimplifyRegularizeLight` can be used instead of this flag to use a smaller regularization factor, reducing the impact on geometric quality.
- `meshopt_SimplifyPermissive` allows collapses across attribute discontinuities, except for vertices that are tagged with `meshopt_SimplifyVertex_Protect` via `vertex_lock`.
- `meshopt_SimplifyPreserveFolds` tries to preserve fold lines between opposite-facing triangles at a small performance cost.
- `meshopt_SimplifyErrorClamped` clamps attribute error to match position error scale and avoid extreme error values in areas with high attribute variance. This setting is recommended when using attribute aware simplification if error values are used for LOD selection or generation.
When using `meshopt_simplifyWithAttributes`, it is also possible to lock certain vertices by providing a `vertex_lock` array that contains a value for each vertex in the mesh, with `meshopt_SimplifyVertex_Lock` set for vertices that should not be collapsed. This can be useful to preserve certain vertices, such as the boundary of the mesh, with more control than `meshopt_SimplifyLockBorder` option provides. When using `meshopt_simplifyWithUpdate`, locking vertices (whether via `vertex_lock` or `meshopt_SimplifyLockBorder`) will also prevent the simplifier from updating their positions and attributes; this can be useful together with `meshopt_SimplifySparse` for meshlet simplification, as meshlets at one level of hierarchy can be simplified together without excessive data copying.
Locking vertices restricts simplification and makes it more likely that the simplifier gets stuck before reaching the index target; if some areas of the mesh are more important than others but should still be eligible for simplification, `vertex_lock` array can be used to mark specific vertices as high priority using `meshopt_SimplifyVertex_Priority` bit, which makes it more likely that the vertex will be preserved during simplification.
In addition to the `meshopt_SimplifyPrune` flag, you can explicitly prune isolated components by calling the `meshopt_simplifyPrune` function. This can be done before regular simplification or as the only step, which is useful for scenarios like isosurface cleanup. Similar to other simplification functions, the `target_error` argument controls the cutoff of component radius and is specified in relative units (e.g., `1e-2f` will remove components under 1%). If an absolute cutoff is desired, divide the parameter by the factor returned by `meshopt_simplifyScale`.
Simplification currently assumes that the input mesh is using the same material for all triangles. If the mesh uses multiple materials, it is possible to split the mesh into subsets based on the material and simplify each subset independently, using `meshopt_SimplifyLockBorder` or `vertex_lock` to preserve material boundaries; however, this limits the collapses and may reduce the resulting quality. An alternative approach is to encode information about the material into the vertex buffer, ensuring that all three vertices referencing the same triangle have the same material ID; this may require duplicating vertices on the boundary between materials. After this, simplification can be performed as usual, and after simplification per-triangle material information can be computed from the vertex material IDs. There is no need to inform the simplifier of the value of the material ID: the implicit boundaries created by duplicating vertices with conflicting material IDs will be preserved automatically (unless permissive simplification is used, in which case material boundaries should be protected via `vertex_lock`). If the source mesh is already split into subsets with non-overlapping vertex indices, and permissive simplification is not used, it should be sufficient to concatenate the subsets into a single vertex/index buffer and simplify the entire mesh at once; the result can be split back into subsets after simplification.
When generating a LOD chain, you can either re-simplify each LOD from the original mesh or use the previous LOD as the starting point for the next level. The latter approach is more efficient and produces smoother visual transitions between LOD levels while preserving mesh attributes better. With this method, resulting error values from previous levels should be accumulated for LOD selection. Additionally, consider using `meshopt_SimplifySparse` to improve performance when generating deep LOD chains.
### Point cloud simplification
In addition to triangle mesh simplification, this library provides a function to simplify point clouds. The algorithm reduces the point cloud to a specified number of points while preserving the overall appearance, and can optionally take per-point colors into account:
```c++
const float color_weight = 1;
std::vector<unsigned int> indices(target_count);
indices.resize(meshopt_simplifyPoints(&indices[0], &points[0].x, points.size(), sizeof(Point),
&points[0].r, sizeof(Point), color_weight, target_count));
```
The resulting indices can be used to render the simplified point cloud; to reduce the memory footprint, the point cloud can be reindexed to create an array of points from the indices.
## Efficiency analyzers
While the only way to get precise performance data is to measure performance on the target GPU, it can be valuable to measure the impact of these optimization in a GPU-independent manner. To this end, the library provides analyzers for all three major optimization routines. For each optimization there is a corresponding analyze function, like `meshopt_analyzeOverdraw`, that returns a struct with statistics.
`meshopt_analyzeVertexCache` returns vertex cache statistics. The common metric to use is ACMR - average cache miss ratio, which is the ratio of the total number of vertex invocations to the triangle count. The worst-case ACMR is 3 (GPU has to process 3 vertices for each triangle); on regular grids the optimal ACMR approaches 0.5. On real meshes it usually is in [0.5..1.5] range depending on the amount of vertex splits. One other useful metric is ATVR - average transformed vertex ratio - which represents the ratio of vertex shader invocations to the total vertices, and has the best case of 1.0 regardless of mesh topology (each vertex is transformed once).
`meshopt_analyzeVertexFetch` returns vertex fetch statistics. The main metric it uses is overfetch - the ratio between the number of bytes read from the vertex buffer to the total number of bytes in the vertex buffer. Assuming non-redundant vertex buffers, the best case is 1.0 - each byte is fetched once.
`meshopt_analyzeOverdraw` returns overdraw statistics. The main metric it uses is overdraw - the ratio between the number of pixel shader invocations to the total number of covered pixels, as measured from several different orthographic cameras. The best case for overdraw is 1.0 - each pixel is shaded once.
`meshopt_analyzeCoverage` returns coverage statistics: the ratio of covered pixels to the viewport extent from each cardinal axis. This is not an efficiency measure per se, but it can be used to measure silhouette change after simplification as well as more precise distance based culling, where the amount of view dependent coverage can be estimated by computing a dot product between the view direction and the coverage vector.
Note that all analyzers use approximate models for the relevant GPU units, so the numbers you will get as the result are only a rough approximation of the actual performance.
## Deinterleaved geometry
All of the examples above assume that geometry is represented as a single vertex buffer and a single index buffer. This requires storing all vertex attributes - position, normal, texture coordinate, skinning weights etc. - in a single contiguous struct. However, in some cases using multiple vertex streams may be preferable. In particular, if some passes require only positional data - such as depth pre-pass or shadow map - then it may be beneficial to split it from the rest of the vertex attributes to make sure the bandwidth use during these passes is optimal. On some mobile GPUs a position-only attribute stream also improves efficiency of tiling algorithms.
Most of the functions in this library either only need the index buffer (such as vertex cache optimization) or only need positional information (such as overdraw optimization). However, several tasks require knowledge about all vertex attributes.
For indexing, `meshopt_generateVertexRemap` assumes that there's just one vertex stream; when multiple vertex streams are used, it's necessary to use `meshopt_generateVertexRemapMulti` as follows:
```c++
meshopt_Stream streams[] = {
{&unindexed_pos[0], sizeof(float) * 3, sizeof(float) * 3},
{&unindexed_nrm[0], sizeof(float) * 3, sizeof(float) * 3},
{&unindexed_uv[0], sizeof(float) * 2, sizeof(float) * 2},
};
std::vector<unsigned int> remap(index_count);
size_t vertex_count = meshopt_generateVertexRemapMulti(&remap[0], NULL, index_count, index_count, streams, sizeof(streams) / sizeof(streams[0]));
```
After this `meshopt_remapVertexBuffer` needs to be called once for each vertex stream to produce the correctly reindexed stream. For shadow indexing, similarly `meshopt_generateShadowIndexBufferMulti` is available as a replacement.
Instead of calling `meshopt_optimizeVertexFetch` for reordering vertices in a single vertex buffer for efficiency, calling `meshopt_optimizeVertexFetchRemap` and then calling `meshopt_remapVertexBuffer` for each stream again is recommended.
Finally, when compressing vertex data, `meshopt_encodeVertexBuffer` should be used on each vertex stream separately - this allows the encoder to best utilize correlation between attribute values for different vertices.
## Specialized processing
In addition to the core optimization techniques, the library provides several specialized algorithms for specific rendering techniques and pipeline optimizations that require a particular configuration of vertex and index data.
### Triangle strip conversion
On most hardware, indexed triangle lists are the most efficient way to drive the GPU. However, in some cases triangle strips might prove beneficial:
- On some older GPUs, triangle strips may be a bit more efficient to render
- On extremely memory constrained systems, index buffers for triangle strips could save a bit of memory
This library provides an algorithm for converting a vertex cache optimized triangle list to a triangle strip:
```c++
std::vector<unsigned int> strip(meshopt_stripifyBound(index_count));
unsigned int restart_index = ~0u;
size_t strip_size = meshopt_stripify(&strip[0], indices, index_count, vertex_count, restart_index);
```
Typically you should expect triangle strips to have ~50-60% of indices compared to triangle lists (~1.5-1.8 indices per triangle) and have ~5% worse ACMR.
Note that triangle strips can be stitched with or without restart index support. Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance.
To reduce the triangle strip size further, it's recommended to use `meshopt_optimizeVertexCacheStrip` instead of `meshopt_optimizeVertexCache` when optimizing for vertex cache. This trades off some efficiency in vertex transform for smaller index buffers.
### Geometry shader adjacency
For algorithms that use geometry shaders and require adjacency information, this library can generate an index buffer with adjacency data:
```c++
std::vector<unsigned int> adjacency(indices.size() * 2);
meshopt_generateAdjacencyIndexBuffer(&adjacency[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex));
```
This creates an index buffer suitable for rendering with triangle-with-adjacency topology, providing 3 extra vertices per triangle that represent vertices opposite to each triangle's edge. This data can be used to compute silhouettes and perform other types of local geometric processing in geometry shaders. To render the mesh with adjacency data, the index buffer should be used with `D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ`/`VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY`/`GL_TRIANGLES_ADJACENCY` topology.
Note that the use of geometry shaders may have a performance impact on some GPUs; in some cases alternative implementation strategies may be more efficient.
### Tessellation with displacement mapping
For hardware tessellation with crack-free displacement mapping, this library can generate a special index buffer that supports PN-AEN tessellation:
```c++
std::vector<unsigned int> tess(indices.size() * 4);
meshopt_generateTessellationIndexBuffer(&tess[0], &indices[0], indices.size(), &vertices[0].x, vertices.size(), sizeof(Vertex));
```
This generates a 12-vertex patch for each input triangle with the following layout:
- 0, 1, 2: original triangle vertices
- 3, 4: opposing edge for edge 0, 1
- 5, 6: opposing edge for edge 1, 2
- 7, 8: opposing edge for edge 2, 0
- 9, 10, 11: dominant vertices for corners 0, 1, 2
This allows the use of hardware tessellation to implement PN-AEN and/or displacement mapping without cracks along UV seams or normal discontinuities. To render the mesh, the index buffer should be used with `D3D_PRIMITIVE_TOPOLOGY_12_CONTROL_POINT_PATCHLIST`/`VK_PRIMITIVE_TOPOLOGY_PATCH_LIST` (`patchControlPoints=12`) topology. For more details please refer to the following papers: [Crack-Free Point-Normal Triangles using Adjacent Edge Normals](https://developer.download.nvidia.com/whitepapers/2010/PN-AEN-Triangles-Whitepaper.pdf), [Tessellation on Any Budget](https://www.nvidia.com/content/pdf/gdc2011/john_mcdonald.pdf) and [My Tessellation Has Cracks!](https://developer.download.nvidia.com/assets/gamedev/files/gdc12/GDC12_DUDASH_MyTessellationHasCracks.pdf).
### Visibility buffers
To render geometry into visibility buffers, access to primitive index in fragment shader is required. While it is possible to use `SV_PrimitiveID`/`gl_PrimitiveID` in the fragment shader, this can result in suboptimal performance on some GPUs (notably, AMD RDNA1 and all NVidia GPUs), and may not be supported on mobile or console hardware. Using mesh shaders to generate primitive IDs is efficient but requires hardware support that is not universally available. To work around these limitations, this library provides a way to generate a special index buffer that uses provoking vertex to encode primitive IDs:
```c++
std::vector<unsigned int> provoke(indices.size());
std::vector<unsigned int> reorder(vertices.size() + indices.size() / 3);
reorder.resize(meshopt_generateProvokingIndexBuffer(&provoke[0], &reorder[0], &indices[0], indices.size(), vertices.size()));
```
This generates a special index buffer along with a reorder table that satisfies two constraints:
- `provoke[3 * tri] == tri`
- `reorder[provoke[x]]` refers to the original triangle vertices
To render the mesh with provoking vertex data, the application should use `provoke` as an index buffer and a vertex shader that passes vertex index (`SV_VertexID`/`gl_VertexIndex`) via a `flat`/`nointerpolation` attribute to the fragment shader as a primitive index, and loads vertex data manually by computing the real vertex index based on `reorder` table (`reorder[gl_VertexIndex]`). For more details please refer to [Variable Rate Shading with Visibility Buffer Rendering](https://advances.realtimerendering.com/s2024/content/Hable/Advances_SIGGRAPH_2024_VisibilityVRS-SIGGRAPH_Advances_2024.pptx); naturally, this technique does not require VRS.
> Note: This assumes the provoking vertex is the first vertex of a triangle, which is true for all graphics APIs except OpenGL/WebGL. For OpenGL/WebGL, you may need to rotate each triangle (abc -> bca) in the resulting index buffer, or use the `glProvokingVertex` function (OpenGL 3.2+) or `WEBGL_provoking_vertex` extension (WebGL2) to change the provoking vertex convention. For WebGL2, this is highly recommended to avoid a variety of emulation slowdowns that happen by default if `flat` attributes are used, such as an implicit use of geometry shaders.
Because the order of indices in the resulting index buffer must be preserved exactly for the technique to work, all optimizations that reorder indices (such as vertex cache optimization) must be applied before generating the provoking index buffer. Additionally, if index compression is used, `meshopt_encodeIndexSequence` should be used instead of `meshopt_encodeIndexBuffer` to ensure that the triangles are not rotated during encoding.
### Opacity micromaps
When using hardware raytracing with alpha-tested transparency, tracing the ray requires invoking any-hit shaders for each intersected surface; this can be inefficient as it requires extra communication and synchronization between raytracing hardware and shader units. Opacity micromaps (OMMs) can significantly accelerate the tests by providing opacity masks for each triangle; this requires subdividing each triangle using a uniform grid (4^N microtriangles for subdivision level N), with each microtriangle storing 1 bit (for 2-state micromaps) or 2 bits (for 4-state micromaps) of opacity data. To minimize the memory overhead, the maps can be reused between triangles using a per-triangle OMM index buffer. This library provides algorithms to generate OMM data for a given mesh from an alpha texture; the resulting data can be used directly in Vulkan via [VK_KHR_opacity_micromap](https://docs.vulkan.org/spec/latest/chapters/raytraversal.html#ray-opacity-micromap) or in DirectX via [DXR1.2](https://github.com/microsoft/DirectX-Specs/blob/master/d3d/Raytracing.md#opacity-micromaps).
Generating opacity micromaps happens in three stages: measure (and layout), rasterize and compact. Compaction is optional but recommended, and can be performed on each mesh individually or on all meshes in the same model/scene.
First, call `meshopt_opacityMapMeasure` to compute a subdivision level for each triangle based on its texel footprint; this also computes initial per-triangle OMM indices as it's common for triangles in the source mesh to refer to the same UVs:
```c++
const int states = 4; // 2-state or 4-state OMMs (used after measure)
const int max_level = 6; // max subdivision level
const float target_edge = 3.0f; // target 3x3px area for each microtriangle
std::vector<unsigned char> levels(indices.size() / 3);
std::vector<unsigned int> sources(indices.size() / 3);
std::vector<int> omm_indices(indices.size() / 3);
size_t omm_count = meshopt_opacityMapMeasure(&levels[0], &sources[0], &omm_indices[0], &indices[0], indices.size(),
&vertices[0].u, vertices.size(), sizeof(Vertex), texture_width, texture_height, max_level, target_edge);
```
Each OMM entry requires separate storage which can be determined based on subdivision level and format (2-state or 4-state), and can be computed via `meshopt_opacityMapEntrySize`:
```c++
std::vector<unsigned int> offsets(omm_count);
size_t data_size = 0;
for (size_t i = 0; i < omm_count; ++i)
{
offsets[i] = unsigned(data_size);
data_size += meshopt_opacityMapEntrySize(levels[i], states);
}
```
Second, call `meshopt_opacityMapRasterize` for each triangle to compute the opacity state per microtriangle. This can be done sequentially or in parallel; it can use the original texture resolution or a smaller mip level to balance rasterization cost vs quality. When generating 4-state micromaps, using mip 0 is recommended to produce maximally conservative output so that enabling opacity micromaps does not noticeably change the raytraced output. For 2-state micromaps, or if the original textures are much higher resolution than the micromap subdivision, smaller mips (e.g. 1 or 2) can also work well.
```c++
for (size_t i = 0; i < omm_count; ++i)
{
unsigned int tri = sources[i];
const float* uv0 = &vertices[indices[tri * 3 + 0]].u;
const float* uv1 = &vertices[indices[tri * 3 + 1]].u;
const float* uv2 = &vertices[indices[tri * 3 + 2]].u;
// texture addressing below assumes RGBA texture input without padding; +3 points to A
meshopt_opacityMapRasterize(&data[offsets[i]], levels[i], states, uv0, uv1, uv2,
texture.data() + 3, 4, texture_width * 4, texture_width, texture_height);
}
```
> Note: Opacity micromap data is sensitive to triangle corner order, which index or meshlet compression can change. If compression is used, index data should be decompressed from the compressed representation before rasterization, or order should be normalized before rasterization as well as after decompression (e.g. by rotating each triangle so that `a < b, c`). Since per-triangle OMM indices use the original triangle order, it's recommended to perform OMM processing after the index order has been finalized.
After rasterization, the OMM data *can* be used as is; however, it's typical to see redundant entries that either can be reused between different triangles, or that have consistent states for all micro-triangles, which can be represented using "special" indices (-4..-1) per triangle. Thus it's recommended to compact the data - if it's already laid out sequentially similarly to the example above, then just calling `meshopt_opacityMapCompact` and trimming the output arrays is sufficient for optimal output:
```c++
omm_count = meshopt_opacityMapCompact(&data[0], data_size, &levels[0], &offsets[0], omm_count, &omm_indices[0], indices.size() / 3, states);
data_size = (omm_count == 0) ? 0 : offsets[omm_count - 1] + meshopt_opacityMapEntrySize(levels[omm_count - 1], states);
```
After compaction, `levels` and `offsets` (`omm_count` entries) and `data` (`data_size` bytes) can be serialized and later passed to the raytracing runtime to build the opacity micromap structures. Often, compacting OMM data across multiple meshes can produce smaller results; in that case, all resulting OMM indices will point to a single OMM array object.
Additionally, note that while the code above works with 32-bit OMM indices, after compaction it's typical to see each mesh refer to a small section of OMM array data which can be represented using 16-bit (or, sometimes, 8-bit indices). The index data can be narrowed to a shorter type in these cases; note that when using 16-bit OMM index data, due to special indices, the index values should be in range `[0..65531]` (and `[0..251]` for 8-bit indices, assuming 4-state OMMs are used).
When using 4-state OMMs, rasterization code produces both unknown-transparent and unknown-opaque states based on microtriangle coverage; this enables the use of forced 2-state flag during traversal for specific effects where micromap data is sufficient for reasonable quality; this is recommended for performance as this results in no any-hit invocations.
### Tangent spaces
Meshes that use tangent space normal maps often require per-vertex tangent vectors in addition to normals. These could be exported alongside mesh data, but this library also provides an algorithm similar to MikkTSpace that can generate them from positions, normals and texture coordinates:
```c++
std::vector<vec4> tangents(indices.size());
meshopt_generateTangents(&tangents[0].x, &indices[0], indices.size(),
&vertices[0].px, vertices.size(), sizeof(Vertex), &vertices[0].nx, sizeof(Vertex), &vertices[0].tx, sizeof(Vertex));
```
For each triangle *corner* this writes a normalized tangent vector (xyz) and an orientation sign (+-1); the bitangent can be reconstructed in the shader as `cross(normal, tangent.xyz) * tangent.w`. Note that some coordinate space conventions that flip V direction in the texture space require negating orientation sign. The input can be indexed, as in the example above, or not (`indices=NULL`); this does not affect the output tangents.
Because tangents are computed per corner, applying them to mesh vertices requires de-indexing the mesh and generating a new index/vertex buffer afterwards (see `Indexing` section earlier), or using indexed data and copying tangents to existing vertex data while duplicating vertices with different tangents (see `tangents` example in `demo/main.cpp`). With the indexed input, if it contains UV mirroring, vertices along the mirror edge may have different tangent spaces on different sides of the edge and need to be split - copying tangents to existing vertex data without splitting will not produce correct results.
If splitting is required, it can be efficiently done with an extra pass and an index list per vertex:
```c++
// seed each vertex with one of its corner tangents; the loop below fixes any mismatches
for (size_t i = 0; i < indices.size(); ++i)
vertices[indices[i]].tangent = tangents[i];
std::vector<unsigned int> splits(vertices.size(), ~0u);
for (size_t i = 0; i < indices.size(); ++i)
{
// walk the chain of split copies looking for a vertex whose tangent matches
unsigned int v = indices[i];
while (v != ~0u && vertices[v].tangent != tangents[i])
v = splits[v];
// no match in chain: append a new split copy with the target tangent and chain it
if (v == ~0u)
{
v = unsigned(vertices.size());
vertices.push_back(vertices[indices[i]]);
vertices[v].tangent = tangents[i];
splits.push_back(splits[indices[i]]);
splits[indices[i]] = v;
}
indices[i] = v;
}
```
The algorithm uses a MikkTSpace-like construction but by default, uses a modified weighting scheme that significantly improves tangent quality around beveled regions in the mesh. If the normal maps are baked from higher resolution geometry using MikkTSpace weighting, it's possible to produce MikkTSpace-compatible tangents by passing `meshopt_TangentCompatible` option as an extra argument to the function.
While it is expected that the normals come from the authored mesh, this library also provides an algorithm to generate normals from positions alone:
```c++
const float crease = 1.f; // 1 rad ~= 60 deg
std::vector<vec3> normals(indices.size());
meshopt_generateNormals(&normals[0].x, &indices[0], indices.size(), &vertices[0].px, vertices.size(), sizeof(Vertex), crease);
```
This function classifies edges as soft/hard based on the specified crease angle (in radians; 30/45/60 degrees are commonly used values) and computes a normal for every corner by averaging normals of incident faces connected by soft edges. Additionally, the optional `smoothing` parameter can be used to perform additional iterative smoothing of the resulting normals, with larger values resulting in smoother normals (recommended range `[0..5]`, defaults to `0`).
Similarly to `meshopt_generateTangents`, this function computes per-corner normals (3 floats for each of 3 corners of each triangle). Applying them to mesh vertices requires de-indexing the mesh, or copying normals to existing vertices while duplicating vertices with different normals.
## Memory management
Many algorithms allocate temporary memory to store intermediate results or accelerate processing. The amount of memory allocated is a function of various input parameters such as vertex count and index count. By default memory is allocated using `operator new` and `operator delete`; if these operators are overloaded by the application, the overloads will be used instead. Alternatively it's possible to specify custom allocation/deallocation functions using `meshopt_setAllocator`, e.g.
```c++
meshopt_setAllocator(malloc, free);
```
> Note that the library expects the allocation function to either throw in case of out-of-memory (in which case the exception will propagate to the caller) or abort, so technically the use of `malloc` above isn't safe. If you want to handle out-of-memory errors without using C++ exceptions, you can use `setjmp`/`longjmp` instead.
When building meshoptimizer as a shared library, allocations from the templated index wrappers provided in the header (used when index data is not `unsigned int`) will only be redirected to these callbacks if the library is built with `MESHOPTIMIZER_ALLOC_EXPORT` defined.
When building meshoptimizer with MSVC, if a non-default calling convention is used such as `__vectorcall` (via `/Gv`), `MESHOPTIMIZER_ALLOC_CALLCONV` should be defined to match the calling convention of `operator new`/`delete`.
Vertex, index and meshlet decoders (`meshopt_decodeVertexBuffer`, `meshopt_decodeIndexBuffer`, `meshopt_decodeIndexSequence`, `meshopt_decodeMeshlet`, `meshopt_decodeMeshletRaw`) do not allocate memory and work completely within the buffer space provided via arguments.
All functions have bounded stack usage that does not exceed 32 KB for any algorithms.
## Experimental APIs
Several algorithms provided by this library are marked as "experimental"; this status is reflected in the comments as well as the annotation `MESHOPTIMIZER_EXPERIMENTAL` for each function.
APIs that are not experimental (annotated with `MESHOPTIMIZER_API`) are considered stable, which means that library updates will not break compatibility: existing calls should compile (API compatibility), existing binaries should link (ABI compatibility), and existing behavior should not change significantly (for example, floating point parameters will have similar behavior). This does not mean that the output of the algorithms will be identical: future versions may improve the algorithms and produce different results.
APIs that *are* experimental may have their interface change, both in ways that will cause existing calls to not compile, and in ways that may compile but have significantly different behavior (e.g., changes in parameter order, meaning, valid ranges). Experimental APIs may also, in rare cases, be removed from future library versions. It is recommended to carefully read release notes when updating the library if experimental APIs are in use. Some experimental APIs may also lack documentation in this README.
Applications may configure the library to change the attributes of experimental APIs, for example defining `MESHOPTIMIZER_EXPERIMENTAL` as `__attribute__((deprecated))` will emit compiler warnings when experimental APIs are used. When building a shared library with CMake, `MESHOPT_STABLE_EXPORTS` option can be set to only export stable APIs; this produces an ABI-stable shared library that can be updated without recompiling the application code.
Currently, the following APIs are experimental:
- `meshopt_SimplifyPreserveFolds` and `meshopt_SimplifyErrorClamped` flags for `meshopt_simplify*` functions
- `meshopt_generateNormals` function
- `meshopt_remesh` function and `meshopt_Remesh*` flags
## License
This library is available to anybody free of charge, under the terms of [MIT License](LICENSE.md).
To honor the license agreement, please include attribution into the user-facing product documentation and/or credits, for example using this or similar text:
> Uses meshoptimizer. Copyright (c) 2016-2026, Arseny Kapoulkine
+2
View File
@@ -0,0 +1,2 @@
/* This file makes sure the library can be used by C89 code */
#include "../src/meshoptimizer.h"
+962
View File
@@ -0,0 +1,962 @@
/**
* clusterlod - a small "library"/example built on top of meshoptimizer to generate cluster LOD hierarchies
* This is intended to either be used as is, or as a reference for implementing similar functionality in your engine.
*
* To use this code, you need to have one source file which includes meshoptimizer.h and defines CLUSTERLOD_IMPLEMENTATION
* before including this file. Other source files in your project can just include this file and use the provided functions.
*
* Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* This code is distributed under the MIT License. See notice at the end of this file.
*/
#pragma once
#include <stddef.h>
struct clodConfig
{
// configuration of each cluster; maps to meshopt_buildMeshlets* parameters
size_t max_vertices;
size_t min_triangles;
size_t max_triangles;
// partitioning setup; maps to meshopt_partitionClusters parameters (plus optional partition sorting)
// note: partition size is the target size, not maximum; actual partitions may be up to 1/3 larger (e.g. target 24 results in maximum 32)
bool partition_spatial;
bool partition_sort;
size_t partition_size;
// clusterization setup; maps to meshopt_buildMeshletsSpatial / meshopt_buildMeshletsFlex
bool cluster_spatial;
float cluster_fill_weight;
float cluster_split_factor;
// every level aims to reduce the number of triangles by ratio, and considers clusters that don't reach the threshold stuck
float simplify_ratio;
float simplify_threshold;
// to compute the error of simplified clusters, we use the formula that combines previous accumulated error as follows:
// max(previous_error * simplify_error_merge_previous, current_error) + current_error * simplify_error_merge_additive
float simplify_error_merge_previous;
float simplify_error_merge_additive;
// amplify the error of clusters that go through sloppy simplification to account for appearance degradation
float simplify_error_factor_sloppy;
// limit error by edge length, aiming to remove subpixel triangles even if the attribute error is high
float simplify_error_edge_limit;
// clamp attribute error to match position error scale and avoid overly conservative LOD selection
bool simplify_error_clamped;
// use permissive simplification instead of regular simplification (make sure to use attribute_protect_mask if this is set!)
bool simplify_permissive;
// use permissive or sloppy simplification but only if regular simplification gets stuck
bool simplify_fallback_permissive;
bool simplify_fallback_sloppy;
// use regularization during simplification to make triangle density more uniform, at some cost to overall triangle count; recommended for deformable objects
bool simplify_regularize;
// try to preserve fold lines between opposite-facing triangles during simplification, at a small performance cost
bool simplify_preserve_folds;
// dilate open borders for each cluster to try to mitigate area loss; most useful for foliage, should not be enabled everywhere
// note: this will mutate vertex_positions in place; the caller should copy positions for each cluster in the output callback as they will be overwritten by simplified clusters
bool simplify_dilate_borders;
// should clodCluster::bounds be computed based on the geometry of each cluster
bool optimize_bounds;
// should clodCluster::indices be optimized for locality; helps with rasterization performance and ray tracing performance in fast-build modes
bool optimize_clusters;
int optimize_clusters_level;
};
struct clodMesh
{
// input triangle indices
const unsigned int* indices;
size_t index_count;
// total vertex count
size_t vertex_count;
// input vertex positions; must be 3 floats per vertex
const float* vertex_positions;
size_t vertex_positions_stride;
// input vertex attributes; used for attribute-aware simplification and permissive simplification
const float* vertex_attributes;
size_t vertex_attributes_stride;
// input vertex locks; allows to preserve additional seams (when not using attribute_protect_mask) or lock vertices via meshopt_SimplifyVertex_* flags
const unsigned char* vertex_lock;
// attribute weights for attribute-aware simplification; maps to meshopt_simplifyWithAttributes parameters
const float* attribute_weights;
size_t attribute_count;
// attribute mask to flag attribute discontinuities for permissive simplification; mask (1<<K) corresponds to attribute K
unsigned int attribute_protect_mask;
};
// To compute approximate (perspective) projection error of a cluster in screen space (0..1; multiply by screen height to get pixels):
// - camera_proj is projection[1][1], or cot(fovy/2); camera_znear is *positive* near plane distance
// - for simplicity, we ignore perspective distortion and use rotationally invariant projection size estimation
// - return: bounds.error / max(distance(bounds.center, camera_position) - bounds.radius, camera_znear) * (camera_proj * 0.5f)
struct clodBounds
{
// sphere bounds, in mesh coordinate space
float center[3];
float radius;
// combined simplification error, in mesh coordinate space
float error;
};
struct clodCluster
{
// index of more refined group (with more triangles) that produced this cluster during simplification, or -1 for original geometry
int refined;
// cluster bounds; should only be used for culling, as bounds.error is not monotonic across DAG
clodBounds bounds;
// cluster indices; refer to the original mesh vertex buffer
const unsigned int* indices;
size_t index_count;
// cluster vertex count; indices[] has vertex_count unique entries
size_t vertex_count;
};
struct clodGroup
{
// DAG level the group was generated at
int depth;
// simplified group bounds (reflects error for clusters with clodCluster::refined == group id; error is FLT_MAX for terminal groups)
// cluster should be rendered if:
// 1. clodGroup::simplified for the group it's in is over error threshold
// 2. cluster.refined is -1 *or* clodGroup::simplified for groups[cluster.refined].simplified is at or under error threshold
clodBounds simplified;
};
// gets called for each group in sequence
// returned value gets saved for clusters emitted from this group (clodCluster::refined)
typedef int (*clodOutput)(void* output_context, clodGroup group, const clodCluster* clusters, size_t cluster_count);
// when using BVH to determine cluster visibility, traverse the tree into nodes while bounds is over error threshold; for leaves,
// group clusters should be rendered if cluster.refined is -1 *or* clodGroup::simplified for groups[cluster.refined].simplified is at or under error threshold
struct clodNode
{
// BVH node bounds, reflecting worst case error for all groups in the subtree
clodBounds bounds;
// leaf node: index of the group this node represents (or -1 for internal nodes)
int group;
// internal node: children are at nodes[child_offset..child_offset+child_count)
unsigned int child_offset;
unsigned int child_count;
};
#ifdef __cplusplus
extern "C"
{
#endif
// default configuration optimized for rasterization / raytracing
clodConfig clodDefaultConfig(size_t max_triangles);
clodConfig clodDefaultConfigRT(size_t max_triangles);
// build cluster LOD hierarchy, calling output callbacks as new clusters and groups are generated
void clodBuild(clodConfig config, clodMesh mesh, void* output_context, clodOutput output_callback);
// extract meshlet-local indices from cluster indices produced by clodBuild
// fills triangles[] and vertices[] such that vertices[triangles[i]] == indices[i]
// returns number of unique vertices (which will be equal to clodCluster::vertex_count)
size_t clodLocalIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count);
// upper bound on the number of nodes clodBuildHierarchy can produce
// level_count is the number of levels in the group DAG (max(clodGroup::depth) + 1)
size_t clodBuildHierarchyBound(size_t group_count, size_t node_width, size_t level_count);
// build a spatial hierarchy over the groups produced by clodBuild for accelerating cluster visibility
// level_count is the number of levels in the group DAG (max(clodGroup::depth) + 1)
// the result is a forest with one tree per DAG level:
// - nodes 0..level_count-1 are the roots of per-level trees, each covering groups with clodGroup::depth == level
// - remaining nodes are the tree nodes; each internal node has up to node_width children, and each leaf node refers to a single group
size_t clodBuildHierarchy(clodNode* nodes, const clodGroup* groups, size_t group_count, size_t node_width, size_t level_count);
#ifdef __cplusplus
} // extern "C"
template <typename Output>
void clodBuild(clodConfig config, clodMesh mesh, Output output)
{
struct Call
{
static int output(void* output_context, clodGroup group, const clodCluster* clusters, size_t cluster_count)
{
return (*static_cast<Output*>(output_context))(group, clusters, cluster_count);
}
};
clodBuild(config, mesh, &output, &Call::output);
}
#endif
#ifdef CLUSTERLOD_IMPLEMENTATION
// For reference, see the original Nanite paper:
// Brian Karis. Nanite: A Deep Dive. 2021
#include <float.h>
#include <math.h>
#include <string.h>
#include <algorithm>
#include <vector>
namespace clod
{
struct Cluster
{
size_t vertices;
// offset into indices vector in the processing loop
size_t index_offset;
size_t index_count;
int refined;
clodBounds bounds;
};
static clodBounds boundsCompute(const clodMesh& mesh, const unsigned int* indices, size_t index_count, float error)
{
meshopt_Bounds bounds = meshopt_computeClusterBounds(indices, index_count, mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride);
return {{bounds.center[0], bounds.center[1], bounds.center[2]}, bounds.radius, error};
}
static clodBounds boundsMerge(const clodBounds* bounds, size_t count, size_t stride)
{
meshopt_Bounds merged = meshopt_computeSphereBounds(&bounds[0].center[0], count, stride, &bounds[0].radius, stride);
// merged bounds error must be conservative wrt cluster errors
float error = 0.f;
for (size_t j = 0; j < count; ++j)
error = std::max(error, reinterpret_cast<const clodBounds*>(reinterpret_cast<const char*>(bounds) + j * stride)->error);
return {{merged.center[0], merged.center[1], merged.center[2]}, merged.radius, error};
}
static void clusterize(std::vector<Cluster>& clusters, std::vector<unsigned int>& cluster_indices, const clodConfig& config, const clodMesh& mesh, const unsigned int* indices, size_t index_count, int refined = -1)
{
size_t max_meshlets = meshopt_buildMeshletsBound(index_count, config.max_vertices, config.min_triangles);
size_t buffer_size = max_meshlets * sizeof(meshopt_Meshlet) + index_count * (sizeof(unsigned int) + sizeof(unsigned char));
// use stack buffer for small mesh subsets (should be enough for group clusterization; initial clusterization uses heap)
alignas(meshopt_Meshlet) char buffer_stack[32768];
std::vector<char> buffer_heap;
char* buffer = (buffer_size <= sizeof(buffer_stack)) ? buffer_stack : (buffer_heap.resize(buffer_size), buffer_heap.data());
meshopt_Meshlet* meshlets = reinterpret_cast<meshopt_Meshlet*>(buffer);
unsigned int* meshlet_vertices = reinterpret_cast<unsigned int*>(meshlets + max_meshlets);
unsigned char* meshlet_triangles = reinterpret_cast<unsigned char*>(meshlet_vertices + index_count);
size_t meshlet_count;
if (config.cluster_spatial)
meshlet_count = meshopt_buildMeshletsSpatial(meshlets, meshlet_vertices, meshlet_triangles, indices, index_count,
mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride,
config.max_vertices, config.min_triangles, config.max_triangles, config.cluster_fill_weight);
else
meshlet_count = meshopt_buildMeshletsFlex(meshlets, meshlet_vertices, meshlet_triangles, indices, index_count,
mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride,
config.max_vertices, config.min_triangles, config.max_triangles, 0.f, config.cluster_split_factor);
size_t cluster_offset = clusters.size();
clusters.resize(cluster_offset + meshlet_count);
size_t index_offset = cluster_indices.size();
cluster_indices.resize(index_offset + index_count);
for (size_t i = 0; i < meshlet_count; ++i)
{
const meshopt_Meshlet& meshlet = meshlets[i];
Cluster& cluster = clusters[cluster_offset + i];
if (config.optimize_clusters)
meshopt_optimizeMeshletLevel(&meshlet_vertices[meshlet.vertex_offset], meshlet.vertex_count, &meshlet_triangles[meshlet.triangle_offset], meshlet.triangle_count, config.optimize_clusters_level);
cluster.vertices = meshlet.vertex_count;
// note: we discard meshlet-local indices; they can be recovered by the caller using clodLocalIndices
cluster.index_offset = index_offset;
cluster.index_count = meshlet.triangle_count * 3;
for (size_t j = 0; j < meshlet.triangle_count * 3; ++j)
cluster_indices[index_offset + j] = meshlet_vertices[meshlet.vertex_offset + meshlet_triangles[meshlet.triangle_offset + j]];
index_offset += meshlet.triangle_count * 3;
cluster.refined = refined;
}
}
static std::vector<unsigned int> partition(std::vector<Cluster>& clusters, const clodConfig& config, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& remap)
{
if (clusters.size() <= config.partition_size)
return {0, unsigned(clusters.size())};
// copy remapped cluster index data into a flat array for partitioning
std::vector<unsigned int> cluster_counts(clusters.size());
std::vector<unsigned int> cluster_indices;
cluster_indices.reserve(indices.size());
for (size_t i = 0; i < clusters.size(); ++i)
{
const Cluster& cluster = clusters[i];
cluster_counts[i] = unsigned(cluster.index_count);
for (size_t j = 0; j < cluster.index_count; ++j)
cluster_indices.push_back(remap[indices[cluster.index_offset + j]]);
}
// partition clusters into groups; the output is a partition id per cluster
std::vector<unsigned int> cluster_part(clusters.size());
size_t partition_count = meshopt_partitionClusters(&cluster_part[0], &cluster_indices[0], cluster_indices.size(), &cluster_counts[0], cluster_counts.size(),
config.partition_spatial ? mesh.vertex_positions : NULL, remap.size(), mesh.vertex_positions_stride, config.partition_size);
if (config.partition_sort)
{
// compute partition points for sorting; any representative point will do, we use last cluster center for simplicity
std::vector<float> partition_point(partition_count * 3);
for (size_t i = 0; i < clusters.size(); ++i)
memcpy(&partition_point[cluster_part[i] * 3], clusters[i].bounds.center, sizeof(float) * 3);
// sort partitions spatially; the output is a remap table from old index (partition id) to new index
std::vector<unsigned int> partition_remap(partition_count);
meshopt_spatialSortRemap(partition_remap.data(), partition_point.data(), partition_count, sizeof(float) * 3);
for (size_t i = 0; i < clusters.size(); ++i)
cluster_part[i] = partition_remap[cluster_part[i]];
}
// count clusters in each partition and compute offsets via prefix sum
std::vector<unsigned int> partition_offsets(partition_count + 1);
for (size_t i = 0; i < clusters.size(); ++i)
partition_offsets[cluster_part[i] + 1]++;
for (size_t i = 1; i <= partition_count; ++i)
partition_offsets[i] += partition_offsets[i - 1];
// distribute clusters into contiguous partitions
std::vector<unsigned int> write = partition_offsets;
std::vector<Cluster> result(clusters.size());
for (size_t i = 0; i < clusters.size(); ++i)
result[write[cluster_part[i]]++] = clusters[i];
clusters.swap(result);
return partition_offsets;
}
static void lockBoundary(std::vector<unsigned char>& locks, const Cluster* clusters, const unsigned int* group_offsets, size_t group_count, const std::vector<unsigned int>& cluster_indices, const std::vector<unsigned int>& remap, const unsigned char* vertex_lock)
{
// for each remapped vertex, use bit 7 as temporary storage to indicate that the vertex has been used by a different group previously
for (size_t i = 0; i < locks.size(); ++i)
locks[i] &= ~((1 << 0) | (1 << 7));
for (size_t i = 0; i < group_count; ++i)
{
const Cluster* group = clusters + group_offsets[i];
size_t group_size = group_offsets[i + 1] - group_offsets[i];
// mark all remapped vertices as locked if seen by a prior group
for (size_t j = 0; j < group_size; ++j)
{
const Cluster& cluster = group[j];
for (size_t k = 0; k < cluster.index_count; ++k)
{
unsigned int v = cluster_indices[cluster.index_offset + k];
unsigned int r = remap[v];
locks[r] |= locks[r] >> 7;
}
}
// mark all remapped vertices as seen
for (size_t j = 0; j < group_size; ++j)
{
const Cluster& cluster = group[j];
for (size_t k = 0; k < cluster.index_count; ++k)
{
unsigned int v = cluster_indices[cluster.index_offset + k];
unsigned int r = remap[v];
locks[r] |= 1 << 7;
}
}
}
for (size_t i = 0; i < locks.size(); ++i)
{
unsigned int r = remap[i];
// consistently lock all vertices with the same position; keep protect bit if set
locks[i] = (locks[r] & 1) | (locks[i] & meshopt_SimplifyVertex_Protect);
if (vertex_lock)
locks[i] |= vertex_lock[i];
}
}
struct SloppyVertex
{
float x, y, z;
unsigned int id;
};
static void simplifyFallback(std::vector<unsigned int>& lod, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned char>& locks, size_t target_count, float* error)
{
std::vector<SloppyVertex> subset(indices.size());
std::vector<unsigned char> subset_locks(indices.size());
lod.resize(indices.size());
size_t positions_stride = mesh.vertex_positions_stride / sizeof(float);
// deindex the mesh subset to avoid calling simplifySloppy on the entire vertex buffer (which is prohibitively expensive without sparsity)
for (size_t i = 0; i < indices.size(); ++i)
{
unsigned int v = indices[i];
assert(v < mesh.vertex_count);
subset[i].x = mesh.vertex_positions[v * positions_stride + 0];
subset[i].y = mesh.vertex_positions[v * positions_stride + 1];
subset[i].z = mesh.vertex_positions[v * positions_stride + 2];
subset[i].id = v;
subset_locks[i] = locks[v];
lod[i] = unsigned(i);
}
lod.resize(meshopt_simplifySloppy(&lod[0], &lod[0], lod.size(), &subset[0].x, subset.size(), sizeof(SloppyVertex), subset_locks.data(), target_count, FLT_MAX, error));
// convert error to absolute
*error *= meshopt_simplifyScale(&subset[0].x, subset.size(), sizeof(SloppyVertex));
// restore original vertex indices
for (size_t i = 0; i < lod.size(); ++i)
lod[i] = subset[lod[i]].id;
}
static void simplify(std::vector<unsigned int>& lod, const clodConfig& config, const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned char>& locks, size_t target_count, float* error)
{
assert(target_count <= indices.size());
lod.resize(indices.size());
unsigned int options = meshopt_SimplifySparse | meshopt_SimplifyErrorAbsolute;
if (config.simplify_error_clamped)
options |= meshopt_SimplifyErrorClamped;
if (config.simplify_permissive)
options |= meshopt_SimplifyPermissive;
if (config.simplify_regularize)
options |= meshopt_SimplifyRegularize;
if (config.simplify_preserve_folds)
options |= meshopt_SimplifyPreserveFolds;
lod.resize(meshopt_simplifyWithAttributes(&lod[0], &indices[0], indices.size(),
mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride,
mesh.vertex_attributes, mesh.vertex_attributes_stride, mesh.attribute_weights, mesh.attribute_count,
&locks[0], target_count, FLT_MAX, options, error));
if (lod.size() > target_count && config.simplify_fallback_permissive && !config.simplify_permissive)
lod.resize(meshopt_simplifyWithAttributes(&lod[0], &indices[0], indices.size(),
mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride,
mesh.vertex_attributes, mesh.vertex_attributes_stride, mesh.attribute_weights, mesh.attribute_count,
&locks[0], target_count, FLT_MAX, options | meshopt_SimplifyPermissive, error));
// while it's possible to call simplifySloppy directly, it doesn't support sparsity or absolute error, so we need to do some extra work
if (lod.size() > target_count && config.simplify_fallback_sloppy)
{
simplifyFallback(lod, mesh, indices, locks, target_count, error);
*error *= config.simplify_error_factor_sloppy; // scale error up to account for appearance degradation
}
// replace degenerate outputs with the first triangle; this ensures we do not produce empty clusters that break DAG invariants
if (lod.empty() && !indices.empty())
lod.assign(indices.begin(), indices.begin() + 3);
// optionally limit error by edge length, aiming to remove subpixel triangles even if the attribute error is high
if (config.simplify_error_edge_limit > 0)
{
float max_edge_sq = 0;
for (size_t i = 0; i < indices.size(); i += 3)
{
unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2];
assert(a < mesh.vertex_count && b < mesh.vertex_count && c < mesh.vertex_count);
const float* va = &mesh.vertex_positions[a * (mesh.vertex_positions_stride / sizeof(float))];
const float* vb = &mesh.vertex_positions[b * (mesh.vertex_positions_stride / sizeof(float))];
const float* vc = &mesh.vertex_positions[c * (mesh.vertex_positions_stride / sizeof(float))];
// compute squared edge lengths
float eab = (va[0] - vb[0]) * (va[0] - vb[0]) + (va[1] - vb[1]) * (va[1] - vb[1]) + (va[2] - vb[2]) * (va[2] - vb[2]);
float eac = (va[0] - vc[0]) * (va[0] - vc[0]) + (va[1] - vc[1]) * (va[1] - vc[1]) + (va[2] - vc[2]) * (va[2] - vc[2]);
float ebc = (vb[0] - vc[0]) * (vb[0] - vc[0]) + (vb[1] - vc[1]) * (vb[1] - vc[1]) + (vb[2] - vc[2]) * (vb[2] - vc[2]);
float emax = std::max(std::max(eab, eac), ebc);
float emin = std::min(std::min(eab, eac), ebc);
// we prefer using min edge length to reduce the number of triangles <1px thick, but need some stopgap for thin and long triangles like wires
max_edge_sq = std::max(max_edge_sq, std::max(emin, emax / 4));
}
// adjust the error to limit it for dense clusters based on edge lengths
*error = std::min(*error, sqrtf(max_edge_sq) * config.simplify_error_edge_limit);
}
}
static int outputGroup(const clodConfig& config, const clodMesh& mesh, const Cluster* group, size_t group_size, const std::vector<unsigned int>& cluster_indices, const clodBounds& simplified, int depth, void* output_context, clodOutput output_callback)
{
// use stack buffer for small groups; should pretty much never trigger heap allocations
clodCluster clusters_stack[32];
std::vector<clodCluster> clusters_heap;
clodCluster* group_clusters = (group_size <= sizeof(clusters_stack) / sizeof(clusters_stack[0])) ? clusters_stack : (clusters_heap.resize(group_size), clusters_heap.data());
for (size_t i = 0; i < group_size; ++i)
{
const Cluster& cluster = group[i];
clodCluster& result = group_clusters[i];
result.refined = cluster.refined;
result.bounds = (config.optimize_bounds && cluster.refined != -1) ? boundsCompute(mesh, cluster_indices.data() + cluster.index_offset, cluster.index_count, cluster.bounds.error) : cluster.bounds;
result.indices = cluster_indices.data() + cluster.index_offset;
result.index_count = cluster.index_count;
result.vertex_count = cluster.vertices;
}
return output_callback ? output_callback(output_context, {depth, simplified}, group_clusters, group_size) : -1;
}
static unsigned long long* edgeLookup(std::vector<unsigned long long>& table, unsigned long long key)
{
size_t mask = table.size() - 1;
size_t h = size_t((key * 0x9E3779B97F4A7C15ull) >> 32) & mask;
while (table[h] != ~0ull && table[h] != key)
h = (h + 1) & mask;
return &table[h];
}
static float boundaryArea(const clodMesh& mesh, const std::vector<unsigned int>& indices, const std::vector<unsigned char>& locks, const std::vector<unsigned int>& remap, std::vector<unsigned long long>& table)
{
memset(table.data(), -1, sizeof(unsigned long long) * table.size());
for (size_t i = 0; i < indices.size(); i += 3)
for (int e = 0; e < 3; ++e)
{
unsigned int a = remap[indices[i + e]], b = remap[indices[i + (e == 2 ? 0 : e + 1)]];
unsigned long long id = (unsigned long long)a << 32 | b;
*edgeLookup(table, id) = id;
}
float area = 0.f;
for (size_t i = 0; i < indices.size(); i += 3)
{
bool border = false;
for (int e = 0; e < 3; ++e)
{
unsigned int a = remap[indices[i + e]], b = remap[indices[i + (e == 2 ? 0 : e + 1)]];
unsigned long long id = (unsigned long long)b << 32 | a;
// we only track border triangles - at least one edge should be unlocked and open (the reverse edge does not exist)
border |= (locks[a] & locks[b] & 1) == 0 && *edgeLookup(table, id) == ~0ull;
}
if (!border)
continue;
const float* va = &mesh.vertex_positions[indices[i + 0] * (mesh.vertex_positions_stride / sizeof(float))];
const float* vb = &mesh.vertex_positions[indices[i + 1] * (mesh.vertex_positions_stride / sizeof(float))];
const float* vc = &mesh.vertex_positions[indices[i + 2] * (mesh.vertex_positions_stride / sizeof(float))];
float ab[3] = {vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]};
float ac[3] = {vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]};
float nt[3] = {ab[1] * ac[2] - ab[2] * ac[1], ab[2] * ac[0] - ab[0] * ac[2], ab[0] * ac[1] - ab[1] * ac[0]};
area += sqrtf(nt[0] * nt[0] + nt[1] * nt[1] + nt[2] * nt[2]) * 0.5f;
}
return area;
}
static void dilateBorders(const clodMesh& mesh, const std::vector<unsigned int>& merged, const std::vector<unsigned int>& simplified, const std::vector<unsigned char>& locks, const std::vector<unsigned int>& remap, clodBounds& bounds, std::vector<float>& dilate_offsets, std::vector<unsigned long long>& edge_table)
{
// hash table is sized to be able to track all edges
size_t table_size = 16;
while (table_size < merged.size() + merged.size() / 2)
table_size *= 2;
edge_table.resize(table_size);
float old_area = boundaryArea(mesh, merged, locks, remap, edge_table);
if (old_area == 0.f)
return;
// limit expansion to cases of moderate area shrinkage; it may be unsafe to dilate otherwise
float new_area = boundaryArea(mesh, simplified, locks, remap, edge_table);
if (new_area >= old_area || new_area < old_area / 4)
return;
float perimeter = 0.f;
for (size_t i = 0; i < simplified.size(); i += 3)
for (int e = 0; e < 3; ++e)
{
unsigned int a = remap[simplified[i + e]], b = remap[simplified[i + (e == 2 ? 0 : e + 1)]];
unsigned long long id = (unsigned long long)b << 32 | a;
// the border check must match the one in boundaryArea (we also rely on table being filled by boundaryArea to avoid extra work)
if ((locks[a] & locks[b] & 1) != 0 || *edgeLookup(edge_table, id) != ~0ull)
continue;
const float* va = &mesh.vertex_positions[simplified[i + e] * (mesh.vertex_positions_stride / sizeof(float))];
const float* vb = &mesh.vertex_positions[simplified[i + (e == 2 ? 0 : e + 1)] * (mesh.vertex_positions_stride / sizeof(float))];
const float* vc = &mesh.vertex_positions[simplified[i + (e + 2) % 3] * (mesh.vertex_positions_stride / sizeof(float))];
float ev[3] = {vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]};
float el = sqrtf(ev[0] * ev[0] + ev[1] * ev[1] + ev[2] * ev[2]);
// compute edge normal by projecting C onto AB
float cv[3] = {va[0] - vc[0], va[1] - vc[1], va[2] - vc[2]};
float cp = (cv[0] * ev[0] + cv[1] * ev[1] + cv[2] * ev[2]) / (el > 0.f ? el * el : 1.f);
float nv[3] = {cv[0] - ev[0] * cp, cv[1] - ev[1] * cp, cv[2] - ev[2] * cp};
float nl = sqrtf(nv[0] * nv[0] + nv[1] * nv[1] + nv[2] * nv[2]);
// accumulate edge normals weighted by edge length; we will renormalize by perimeter before applying
float ns = nl > 0.f ? el / nl : 0;
if ((locks[a] & 1) == 0)
dilate_offsets[a * 4 + 0] += nv[0] * ns, dilate_offsets[a * 4 + 1] += nv[1] * ns, dilate_offsets[a * 4 + 2] += nv[2] * ns, dilate_offsets[a * 4 + 3] += el;
if ((locks[b] & 1) == 0)
dilate_offsets[b * 4 + 0] += nv[0] * ns, dilate_offsets[b * 4 + 1] += nv[1] * ns, dilate_offsets[b * 4 + 2] += nv[2] * ns, dilate_offsets[b * 4 + 3] += el;
perimeter += el;
}
float distance = perimeter > 0.f ? (old_area - new_area) / perimeter : 0.f;
for (size_t i = 0; i < simplified.size(); ++i)
{
unsigned int r = remap[simplified[i]];
if (locks[r] & 1)
continue;
float* vi = const_cast<float*>(&mesh.vertex_positions[simplified[i] * (mesh.vertex_positions_stride / sizeof(float))]);
float* vr = const_cast<float*>(&mesh.vertex_positions[r * (mesh.vertex_positions_stride / sizeof(float))]);
float* n = &dilate_offsets[r * 4];
if (n[3] > 0.f)
{
// we average the normals and then divide by length twice: once to normalize, and once to extend the offset into a miter
float nx = n[0] / n[3], ny = n[1] / n[3], nz = n[2] / n[3];
float nn = nx * nx + ny * ny + nz * nz;
float ns = distance / (nn > 0.15f ? nn : 0.15f);
vr[0] += nx * ns, vr[1] += ny * ns, vr[2] += nz * ns;
n[0] = n[1] = n[2] = n[3] = 0.f;
// expand group bounds to accommodate the offset vertex
float dx = vr[0] - bounds.center[0], dy = vr[1] - bounds.center[1], dz = vr[2] - bounds.center[2];
bounds.radius = std::max(bounds.radius, sqrtf(dx * dx + dy * dy + dz * dz));
}
// copy dilated positions back to all referencing wedges from the canonical copy updated above; no-op for vertices that weren't dilated
vi[0] = vr[0], vi[1] = vr[1], vi[2] = vr[2];
}
}
static clodNode mergeNodes(const clodNode* nodes, size_t offset, size_t count)
{
clodNode result = {};
result.bounds = boundsMerge(&nodes[offset].bounds, count, sizeof(clodNode));
result.group = -1;
result.child_offset = unsigned(offset);
result.child_count = unsigned(count);
return result;
}
} // namespace clod
clodConfig clodDefaultConfig(size_t max_triangles)
{
assert(max_triangles >= 4 && max_triangles <= 256);
clodConfig config = {};
config.max_vertices = max_triangles;
config.min_triangles = max_triangles / 3;
config.max_triangles = max_triangles;
config.partition_spatial = true;
config.partition_size = 16;
config.cluster_spatial = false;
config.cluster_split_factor = 2.0f;
config.optimize_clusters = true;
config.optimize_clusters_level = 1;
config.simplify_ratio = 0.5f;
config.simplify_threshold = 0.85f;
config.simplify_error_merge_previous = 1.0f;
config.simplify_error_factor_sloppy = 2.0f;
config.simplify_error_clamped = true;
config.simplify_permissive = true;
config.simplify_fallback_permissive = false; // note: by default we run in permissive mode, but it's also possible to disable that and use it only as a fallback
config.simplify_fallback_sloppy = true;
return config;
}
clodConfig clodDefaultConfigRT(size_t max_triangles)
{
clodConfig config = clodDefaultConfig(max_triangles);
// for ray tracing, we may want smaller clusters when that improves BVH quality further; for maximum ray tracing performance this could be reduced even further
config.min_triangles = max_triangles / 4;
// by default, we use larger max_vertices for RT; the vertex count is not important for ray tracing performance, and this helps improve cluster utilization
config.max_vertices = std::min(size_t(256), max_triangles * 2);
config.cluster_spatial = true;
config.cluster_fill_weight = 0.5f;
return config;
}
void clodBuild(clodConfig config, clodMesh mesh, void* output_context, clodOutput output_callback)
{
using namespace clod;
assert(mesh.vertex_attributes_stride % sizeof(float) == 0);
assert(mesh.attribute_count * sizeof(float) <= mesh.vertex_attributes_stride);
assert(mesh.attribute_protect_mask < (1u << (mesh.vertex_attributes_stride / sizeof(float))));
std::vector<unsigned char> locks(mesh.vertex_count);
// for cluster connectivity, we need a position-only remap that maps vertices with the same position to the same index
std::vector<unsigned int> remap(mesh.vertex_count);
meshopt_generatePositionRemap(&remap[0], mesh.vertex_positions, mesh.vertex_count, mesh.vertex_positions_stride);
// set up protect bits on UV seams for permissive mode
if (mesh.attribute_protect_mask)
{
size_t max_attributes = mesh.vertex_attributes_stride / sizeof(float);
for (size_t i = 0; i < mesh.vertex_count; ++i)
{
unsigned int r = remap[i]; // canonical vertex with the same position
for (size_t j = 0; j < max_attributes; ++j)
if (r != i && (mesh.attribute_protect_mask & (1u << j)) && mesh.vertex_attributes[i * max_attributes + j] != mesh.vertex_attributes[r * max_attributes + j])
locks[i] |= meshopt_SimplifyVertex_Protect;
}
}
std::vector<float> dilate_offsets(config.simplify_dilate_borders ? mesh.vertex_count * 4 : 0);
std::vector<unsigned long long> dilate_table;
// initial clusterization splits the original mesh
std::vector<unsigned int> cluster_indices;
std::vector<Cluster> clusters;
clusterize(clusters, cluster_indices, config, mesh, mesh.indices, mesh.index_count);
// compute initial precise bounds; subsequent bounds will be using group-merged bounds
for (Cluster& cluster : clusters)
cluster.bounds = boundsCompute(mesh, cluster_indices.data() + cluster.index_offset, cluster.index_count, 0.f);
std::vector<Cluster> pending;
std::vector<unsigned int> pending_indices;
pending.reserve(clusters.size());
pending_indices.reserve(size_t(cluster_indices.size() * config.simplify_threshold));
std::vector<unsigned int> merged, simplified;
merged.reserve((config.partition_size + config.partition_size / 3) * config.max_triangles * 3);
int depth = 0;
// merge and simplify clusters until we can't merge anymore
while (clusters.size() > 1)
{
std::vector<unsigned int> group_offsets = partition(clusters, config, mesh, cluster_indices, remap);
pending.clear();
pending_indices.clear();
// mark boundaries between groups with a lock bit to avoid gaps in simplified result
lockBoundary(locks, clusters.data(), group_offsets.data(), group_offsets.size() - 1, cluster_indices, remap, mesh.vertex_lock);
// every group needs to be simplified now
for (size_t i = 0; i + 1 < group_offsets.size(); ++i)
{
const Cluster* group = clusters.data() + group_offsets[i];
size_t group_size = group_offsets[i + 1] - group_offsets[i];
merged.clear();
for (size_t j = 0; j < group_size; ++j)
merged.insert(merged.end(), cluster_indices.begin() + group[j].index_offset, cluster_indices.begin() + group[j].index_offset + group[j].index_count);
size_t target_size = size_t((merged.size() / 3) * config.simplify_ratio) * 3;
// enforce bounds and error monotonicity
// note: it is incorrect to use the precise bounds of the merged or simplified mesh, because this may violate monotonicity
clodBounds bounds = boundsMerge(&group[0].bounds, group_size, sizeof(Cluster));
float error = 0.f;
simplify(simplified, config, mesh, merged, locks, target_size, &error);
if (simplified.size() > merged.size() * config.simplify_threshold)
{
bounds.error = FLT_MAX; // terminal group, won't simplify further
outputGroup(config, mesh, group, group_size, cluster_indices, bounds, depth, output_context, output_callback);
continue; // simplification is stuck; abandon the merge
}
// enforce error monotonicity (with an optional hierarchical factor to separate transitions more)
bounds.error = std::max(bounds.error * config.simplify_error_merge_previous, error) + error * config.simplify_error_merge_additive;
// output the new group with all clusters; the resulting id will be recorded in new clusters as clodCluster::refined
int refined = outputGroup(config, mesh, group, group_size, cluster_indices, bounds, depth, output_context, output_callback);
// now that we've output the group with the original clusters, we need to dilate simplified clusters if requested
if (config.simplify_dilate_borders)
dilateBorders(mesh, merged, simplified, locks, remap, bounds, dilate_offsets, dilate_table);
// enqueue new clusters for further processing
size_t cluster_offset = pending.size();
clusterize(pending, pending_indices, config, mesh, simplified.data(), simplified.size(), refined);
// update cluster group bounds to the group-merged bounds; this ensures that we compute the group bounds for whatever group this cluster will be part of conservatively
for (size_t j = cluster_offset; j < pending.size(); ++j)
pending[j].bounds = bounds;
}
clusters.swap(pending);
cluster_indices.swap(pending_indices);
depth++;
}
if (clusters.size())
{
assert(clusters.size() == 1);
const Cluster& cluster = clusters[0];
clodBounds bounds = cluster.bounds;
bounds.error = FLT_MAX; // terminal group, won't simplify further
outputGroup(config, mesh, clusters.data(), clusters.size(), cluster_indices, bounds, depth, output_context, output_callback);
}
}
size_t clodLocalIndices(unsigned int* vertices, unsigned char* triangles, const unsigned int* indices, size_t index_count)
{
return meshopt_extractMeshletIndices(vertices, triangles, indices, index_count);
}
size_t clodBuildHierarchyBound(size_t group_count, size_t node_width, size_t level_count)
{
// count nodes for each tree depth; we pad by level_count at each iteration to account for unknown level distribution in the forest
size_t total = level_count;
for (size_t frontier = group_count; frontier > 1; frontier = (frontier + node_width - 1) / node_width)
total += frontier + level_count;
return total;
}
size_t clodBuildHierarchy(clodNode* nodes, const clodGroup* groups, size_t group_count, size_t node_width, size_t level_count)
{
using namespace clod;
// reserve space for per-level roots
size_t offset = level_count;
std::vector<clodNode> row(group_count);
std::vector<unsigned int> order(group_count);
for (size_t level = 0; level < level_count; ++level)
{
// start each tree hierarchy from the groups of that level as leaves
row.clear();
for (size_t i = 0; i < group_count; ++i)
if (groups[i].depth == int(level))
{
clodNode node = {};
node.bounds = groups[i].simplified;
node.group = int(i);
row.push_back(node);
}
// build the tree going up one level at a time, using spatial grouping of centers as a proxy for locality
while (row.size() > 1)
{
size_t count = row.size();
meshopt_spatialClusterPoints(order.data(), row[0].bounds.center, count, sizeof(clodNode), node_width);
for (size_t i = 0; i < count; ++i)
nodes[offset + i] = row[order[i]];
row.clear();
for (size_t i = 0; i < count; i += node_width)
{
// spatialClusterPoints guarantees that each cluster except for last one is full
size_t children = std::min(node_width, count - i);
row.push_back(mergeNodes(nodes, offset + i, children));
}
offset += count;
}
// the root of the current level goes into the fixed section at the beginning
assert(row.size() == 1);
nodes[level] = row[0];
}
assert(offset <= clodBuildHierarchyBound(group_count, node_width, level_count));
return offset;
}
#endif
/**
* Copyright (c) 2016-2026 Arseny Kapoulkine
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
+148
View File
@@ -0,0 +1,148 @@
<!doctype html>
<html lang="en">
<head>
<title>meshoptimizer - demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<style>
body {
font-family: Monospace;
background-color: #000;
color: #fff;
margin: 0px;
overflow: hidden;
}
#info {
color: #fff;
position: absolute;
top: 10px;
width: 100%;
text-align: center;
z-index: 100;
display: block;
}
#info a,
.button {
color: #f00;
font-weight: bold;
text-decoration: underline;
cursor: pointer;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="info">
<a href="https://github.com/zeux/meshoptimizer" target="_blank" rel="noopener">meshoptimizer</a>
</div>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
var container;
var camera, scene, renderer, mixer, timer;
var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;
init();
animate();
function init() {
container = document.createElement('div');
document.body.appendChild(container);
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 100);
camera.position.y = 1.0;
camera.position.z = 3.0;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
var pointLight = new THREE.PointLight(0xffffff, 4);
pointLight.position.set(3, 3, 0);
pointLight.decay = 0.5;
camera.add(pointLight);
scene.add(camera);
var onProgress = function (xhr) {};
var onError = function (e) {
console.log(e);
};
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
'pirate.glb',
function (gltf) {
var bbox = new THREE.Box3().setFromObject(gltf.scene);
var scale = 2 / (bbox.max.y - bbox.min.y);
gltf.scene.scale.set(scale, scale, scale);
gltf.scene.position.set(0, 0, 0);
scene.add(gltf.scene);
mixer = new THREE.AnimationMixer(gltf.scene);
if (gltf.animations.length) {
mixer.clipAction(gltf.animations[gltf.animations.length - 1]).play();
}
},
onProgress,
onError
);
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
window.addEventListener('resize', onWindowResize, false);
timer = new THREE.Timer();
timer.connect(document);
}
function onWindowResize() {
windowHalfX = window.innerWidth / 2;
windowHalfY = window.innerHeight / 2;
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function animate(timestamp) {
timer.update(timestamp);
if (mixer) {
mixer.update(timer.getDelta());
}
requestAnimationFrame(animate);
render();
}
function render() {
renderer.render(scene, camera);
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+209
View File
@@ -0,0 +1,209 @@
/**
* meshletdec.slang - an example GPU decoder for meshlet data encoded using meshopt_encodeMeshlet
* This is intended to be used as a starting point for applications that want to decode meshlet data on the GPU.
*
* The shader exposes an entrypoint, decodeMeshlets, that decodes a set of meshlets; each meshlet is decoded independently,
* and the output vertex/triangle data is written as uint32 per element (triangle data is written as 0xccbbaa).
* This matches the output format for meshopt_decodeMeshlet with vertex_size=4 triangle_size=4. If alternative formats are
* needed, the code should be changed to output them; note that for triangle data, it may make sense to output data to shared
* memory to be able to use larger aligned 32-bit writes to global memory after that.
*
* Copyright (C) 2016-2026, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
* This code is distributed under the MIT License. See notice at the end of this file.
*/
struct MeshletDesc
{
uint stream_offset;
uint output_offset;
uint16_t encoded_size;
uint8_t vertex_count;
uint8_t triangle_count;
};
[[vk::binding(0)]]
StructuredBuffer<uint8_t> gStream : register(t0);
[[vk::binding(1)]]
StructuredBuffer<MeshletDesc> gMeshlets : register(t1);
[[vk::binding(2)]]
RWStructuredBuffer<uint> gOutput : register(u2);
[[vk::binding(3)]]
cbuffer MeshletConfigCB : register(b3) { uint gMeshletCount; }
uint decodeVertices(uint out_vertices, uint ctrl, uint data, uint bound, uint vertex_count)
{
uint last = ~0u;
for (uint i = 0; i < vertex_count; i += 4)
{
if (data > bound)
return ~0u;
uint code4 = uint(gStream[ctrl + i / 4]);
for (int k = 0; k < 4; ++k)
{
int code = ((code4 >> k) & 1) | ((code4 >> (k + 3)) & 2);
int length = code4 == 0xff ? 4 : code;
// branchlessly read up to 4 bytes
uint mask = (length == 4) ? ~0u : (1 << (8 * length)) - 1;
uint v = (uint(gStream[data + 0]) | (uint(gStream[data + 1]) << 8) | (uint(gStream[data + 2]) << 16) | (uint(gStream[data + 3]) << 24)) & mask;
// unzigzag + 1
uint d = (v >> 1) ^ -int(v & 1);
uint r = last + d + 1;
if (i + k < vertex_count)
gOutput[out_vertices + i + k] = r;
data += length;
last = r;
}
}
return data;
}
uint decodeTriangle(uint code, uint extra0, uint extra1, uint extra2, inout uint fifo0, inout uint fifo1, inout uint fifo2, inout uint next, inout uint extra)
{
// reuse: 0-1 extra vertices
uint fifo = code < 4 ? fifo0 : (code < 8 ? fifo1 : fifo2);
uint edge = fifo >> ((code << 3) & 16); // shift by 16 if bit 1 is set (odd edge for each triangle)
uint c_reuse = (code & 1) == 1 ? extra0 : next;
// restart: 0-3 extra vertices
uint extran = code & 3;
uint a = extran > 0 ? extra0 : next;
uint b = extran > 1 ? extra1 : next + (1 - extran);
uint c = extran > 2 ? extra2 : next + (2 - extran);
// select between reuse and restart and repack triangle into edge format (0xcbac)
a = code >= 12 ? a : (edge >> 8) & 0xff;
b = code >= 12 ? b : edge & 0xff;
c = code >= 12 ? c : c_reuse;
uint tri = c | (a << 8) | (b << 16) | (c << 24);
// advance next/extra; reuse codes use 1 lsb for extra count, restart codes use 2 lsbs
uint extrab = code < 12 ? 1 : 3;
next += extrab - (code & extrab);
extra += code & extrab;
// rotate fifo
fifo2 = fifo1;
fifo1 = fifo0;
fifo0 = tri;
// output triangle is stored without extra edge vertex (0xcbac => 0xcba)
return tri >> 8;
}
uint decodeTriangles(uint out_triangles, uint codes, uint extra, uint bound, uint triangle_count)
{
uint next = 0;
uint fifo0 = 0, fifo1 = 0, fifo2 = 0; // two edge fifo entries in one uint: 0xcbac
for (uint i = 0; i < triangle_count; i += 2)
{
if (extra > bound)
return ~0u;
uint codeg = uint(gStream[codes + i / 2]);
// first triangle
uint extra0 = uint(gStream[extra + 0]);
uint extra1 = uint(gStream[extra + 1]);
uint extra2 = uint(gStream[extra + 2]);
uint tri = decodeTriangle(codeg & 15, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra);
gOutput[out_triangles + i] = tri;
// second triangle, if any
extra0 = uint(gStream[extra + 0]);
extra1 = uint(gStream[extra + 1]);
extra2 = uint(gStream[extra + 2]);
tri = decodeTriangle(codeg >> 4, extra0, extra1, extra2, fifo0, fifo1, fifo2, next, extra);
if (i + 1 < triangle_count)
gOutput[out_triangles + i + 1] = tri;
}
return extra;
}
int decodeMeshlet(uint out_vertices, uint vertex_count, uint out_triangles, uint triangle_count, uint buffer, uint buffer_size)
{
uint codes_size = (triangle_count + 1) / 2;
uint ctrl_size = (vertex_count + 3) / 4;
uint gap_size = (codes_size + ctrl_size < 16) ? 16 - (codes_size + ctrl_size) : 0;
if (buffer_size < codes_size + ctrl_size + gap_size)
return -2;
uint end = buffer + buffer_size;
uint codes = end - codes_size;
uint ctrl = codes - ctrl_size;
uint data = buffer;
// gap ensures we have at least 16 bytes available after bound; this allows decoder to over-read safely
uint bound = ctrl - gap_size;
data = decodeVertices(out_vertices, ctrl, data, bound, vertex_count);
if (data == ~0u)
return -2;
data = decodeTriangles(out_triangles, codes, data, bound, triangle_count);
if (data == ~0u)
return -2;
return (data == bound) ? 0 : -3;
}
[shader("compute")]
[numthreads(32, 1, 1)]
void decodeMeshlets(uint3 dispatch_thread_id: SV_DispatchThreadID)
{
uint meshlet_count = gMeshletCount;
uint tid = dispatch_thread_id.x;
if (tid >= meshlet_count)
return;
MeshletDesc desc = gMeshlets[tid];
uint out_vertices = desc.output_offset;
uint out_triangles = desc.output_offset + uint(desc.vertex_count);
int rc = decodeMeshlet(out_vertices, uint(desc.vertex_count), out_triangles, uint(desc.triangle_count), desc.stream_offset, uint(desc.encoded_size));
// if decoding failed, we write 0xff.. to the first word of the output data
// this can be adjusted arbitrarily; for example, a separate buffer with a single status for the entire stream could be used
// note that decoding fails only if the input data is corrupt; so this may not be required at all depending on the requirements
if (rc < 0)
gOutput[desc.output_offset] = ~0u;
}
/**
* Copyright (c) 2016-2026 Arseny Kapoulkine
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
+523
View File
@@ -0,0 +1,523 @@
// For reference, see the original Nanite paper:
// Brian Karis. Nanite: A Deep Dive. 2021
#include "../src/meshoptimizer.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <vector>
#define CLUSTERLOD_IMPLEMENTATION
#include "clusterlod.h"
struct Vertex
{
float px, py, pz;
float nx, ny, nz;
float tx, ty;
};
// computes approximate (perspective) projection error of a cluster in screen space (0..1; multiply by screen height to get pixels)
// camera_proj is projection[1][1], or cot(fovy/2); camera_znear is *positive* near plane distance
// for DAG cut to be valid, boundsError must be monotonic: it must return a larger error for parent cluster
// for simplicity, we ignore perspective distortion and use rotationally invariant projection size estimation
static float boundsError(const clodBounds& bounds, float camera_x, float camera_y, float camera_z, float camera_proj, float camera_znear)
{
float dx = bounds.center[0] - camera_x, dy = bounds.center[1] - camera_y, dz = bounds.center[2] - camera_z;
float d = sqrtf(dx * dx + dy * dy + dz * dz) - bounds.radius;
return bounds.error / (d > camera_znear ? d : camera_znear) * (camera_proj * 0.5f);
}
static int measureComponents(std::vector<int>& parents, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& remap);
static size_t measureBoundary(std::vector<int>& used, const std::vector<std::vector<unsigned int> >& clusters, const std::vector<unsigned int>& remap);
static float sahOverhead(const std::vector<std::vector<unsigned int> >& clusters, const std::vector<Vertex>& vertices);
void dumpObj(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices, bool recomputeNormals = false);
void dumpObj(const char* section, const std::vector<unsigned int>& indices);
// simulate BVH based LOD cut and return triangle count
static size_t countTriangles(const std::vector<clodNode>& nodes, unsigned int index, const std::vector<clodGroup>& groups, const std::vector<std::pair<unsigned int, unsigned int> >& group_clusters, const std::vector<std::pair<int, unsigned int> >& clusters, float cx, float cy, float cz, float proj, float znear, float threshold)
{
const clodNode& node = nodes[index];
// prune the subtree once it's too detailed
if (boundsError(node.bounds, cx, cy, cz, proj, znear) <= threshold)
return 0;
size_t tris = 0;
if (node.group < 0)
{
// internal node: recurse into children
for (unsigned int i = 0; i < node.child_count; ++i)
tris += countTriangles(nodes, node.child_offset + i, groups, group_clusters, clusters, cx, cy, cz, proj, znear, threshold);
}
else
{
// leaf node: keep each cluster that is the least detailed one passing the threshold
std::pair<unsigned int, unsigned int> range = group_clusters[node.group];
for (unsigned int i = range.first; i < range.first + range.second; ++i)
if (clusters[i].first < 0 || boundsError(groups[clusters[i].first].simplified, cx, cy, cz, proj, znear) <= threshold)
tris += clusters[i].second;
}
return tris;
}
void nanite(const std::vector<Vertex>& vertices, const std::vector<unsigned int>& indices)
{
#ifdef _MSC_VER
static const char* dump = NULL; // tired of C4996
static const char* clrt = NULL;
#else
static const char* dump = getenv("DUMP");
static const char* clrt = getenv("CLRT");
#endif
clodConfig config = clodDefaultConfig(/*max_triangles=*/128);
if (clrt)
{
config = clodDefaultConfigRT(config.max_triangles);
config.cluster_fill_weight = float(atof(clrt));
}
const float attribute_weights[3] = {0.5f, 0.5f, 0.5f};
clodMesh mesh = {};
mesh.indices = indices.data();
mesh.index_count = indices.size();
mesh.vertex_count = vertices.size();
mesh.vertex_positions = &vertices[0].px;
mesh.vertex_positions_stride = sizeof(Vertex);
mesh.vertex_attributes = &vertices[0].nx;
mesh.vertex_attributes_stride = sizeof(Vertex);
mesh.attribute_weights = attribute_weights;
mesh.attribute_count = sizeof(attribute_weights) / sizeof(attribute_weights[0]);
mesh.attribute_protect_mask = (1 << 3) | (1 << 4); // protect UV seams, maps to Vertex::tx/ty
struct Stats
{
size_t groups;
size_t clusters;
size_t triangles;
size_t vertices;
size_t full_clusters;
size_t singleton_groups;
size_t stuck_clusters;
size_t stuck_triangles;
double radius;
std::vector<std::vector<unsigned int> > indices; // for detailed connectivity analysis
};
std::vector<Stats> stats;
std::vector<clodGroup> groups;
std::vector<std::pair<unsigned int, unsigned int> > group_clusters; // per group: (offset, count) into cluster_data
std::vector<std::pair<int, unsigned int> > cluster_data; // per cluster: (clodCluster::refined, triangle count)
std::vector<std::vector<unsigned int> > cut;
int cut_level = dump ? atoi(dump) : -2;
// for testing purposes, we can compute a DAG cut from a given viewpoint and dump it as an OBJ
float maxx = 0.f, maxy = 0.f, maxz = 0.f;
for (size_t i = 0; i < vertices.size(); ++i)
{
maxx = std::max(maxx, vertices[i].px * 2);
maxy = std::max(maxy, vertices[i].py * 2);
maxz = std::max(maxz, vertices[i].pz * 2);
}
float threshold = 2e-3f; // 2 pixels at 1080p
float fovy = 60.f;
float znear = 1e-2f;
float proj = 1.f / tanf(fovy * 3.1415926f / 180.f * 0.5f);
clodBuild(config, mesh, [&](clodGroup group, const clodCluster* clusters, size_t cluster_count) -> int { // clang-format!
if (stats.size() <= size_t(group.depth))
stats.push_back({});
Stats& level = stats[group.depth];
level.groups++;
level.clusters += cluster_count;
if (group.simplified.error == FLT_MAX)
level.stuck_clusters += cluster_count;
level.singleton_groups += cluster_count == 1;
group_clusters.push_back(std::make_pair(unsigned(cluster_data.size()), unsigned(cluster_count)));
for (size_t i = 0; i < cluster_count; ++i)
{
const clodCluster& cluster = clusters[i];
cluster_data.push_back(std::make_pair(cluster.refined, unsigned(cluster.index_count / 3)));
level.triangles += cluster.index_count / 3;
if (group.simplified.error == FLT_MAX)
level.stuck_triangles += cluster.index_count / 3;
level.vertices += cluster.vertex_count;
level.full_clusters += (cluster.index_count == config.max_triangles * 3);
level.radius += cluster.bounds.radius;
level.indices.push_back(std::vector<unsigned int>(cluster.indices, cluster.indices + cluster.index_count));
// when requesting DAG cut at a given level, we need to render all terminal clusters at lower depth as well
if (cut_level >= 0 && (group.depth == cut_level || (group.depth < cut_level && group.simplified.error == FLT_MAX)))
cut.push_back(std::vector<unsigned int>(cluster.indices, cluster.indices + cluster.index_count));
// when requesting DAG cut from a viewpoint, we need to check if each cluster is the least detailed cluster that passes the error threshold
if (cut_level == -1 && (cluster.refined < 0 || boundsError(groups[cluster.refined].simplified, maxx, maxy, maxz, proj, znear) <= threshold) && boundsError(group.simplified, maxx, maxy, maxz, proj, znear) > threshold)
cut.push_back(std::vector<unsigned int>(cluster.indices, cluster.indices + cluster.index_count));
}
level.indices.push_back(std::vector<unsigned int>()); // mark end of group for measureBoundary
groups.push_back(group);
return int(groups.size() - 1);
});
// for cluster connectivity analysis and boundary statistics, we need a position-only remap that maps vertices with the same position to the same index
std::vector<unsigned int> remap(vertices.size());
meshopt_generatePositionRemap(&remap[0], &vertices[0].px, vertices.size(), sizeof(Vertex));
std::vector<int> used(vertices.size());
size_t lowest_clusters = 0;
size_t lowest_triangles = 0;
for (size_t i = 0; i < stats.size(); ++i)
{
Stats& level = stats[i];
lowest_clusters += level.stuck_clusters;
lowest_triangles += level.stuck_triangles;
size_t connected = 0;
for (const auto& cluster : level.indices)
connected += measureComponents(used, cluster, remap);
size_t boundary = measureBoundary(used, level.indices, remap);
float saho = clrt ? sahOverhead(level.indices, vertices) : 0.f;
double inv_clusters = 1.0 / double(level.clusters);
printf("lod %d: %d clusters (%.1f%% full, %.1f tri/cl, %.1f vtx/cl, %.2f connected, %.1f boundary, %.1f partition, %d singletons, %.3f sah overhead, %f radius), %d triangles",
int(i), int(level.clusters),
double(level.full_clusters) * inv_clusters * 100, double(level.triangles) * inv_clusters, double(level.vertices) * inv_clusters,
double(connected) * inv_clusters, double(boundary) * inv_clusters,
double(level.clusters) / double(level.groups), int(level.singleton_groups),
saho, level.radius * inv_clusters,
int(level.triangles));
if (level.stuck_clusters && level.clusters > 1)
printf("; stuck %d clusters (%d triangles)", int(level.stuck_clusters), int(level.stuck_triangles));
printf("\n");
}
printf("lowest lod: %d clusters, %d triangles\n", int(lowest_clusters), int(lowest_triangles));
// build a spatial hierarchy over the groups and use it to compute the cut size from a given viewpoint
{
size_t node_width = 8;
size_t levels = groups.back().depth + 1; // groups are generated in order of increasing depth
std::vector<clodNode> nodes(clodBuildHierarchyBound(groups.size(), node_width, levels));
nodes.resize(clodBuildHierarchy(nodes.data(), groups.data(), groups.size(), node_width, levels));
size_t bvh_tris = 0;
for (size_t i = 0; i < levels; ++i) // the hierarchy is a forest with one root per level
bvh_tris += countTriangles(nodes, unsigned(i), groups, group_clusters, cluster_data, maxx, maxy, maxz, proj, znear, threshold);
printf("bvh cut (error %.3f): %d triangles\n", threshold, int(bvh_tris));
}
if (cut_level >= -1)
{
size_t cut_tris = 0;
for (auto& cluster : cut)
cut_tris += cluster.size() / 3;
if (cut_level >= 0)
printf("cut (level %d): %d triangles\n", cut_level, int(cut_tris));
else
printf("cut (error %.3f): %d triangles\n", threshold, int(cut_tris));
dumpObj(vertices, std::vector<unsigned int>());
for (auto& cluster : cut)
dumpObj("cluster", cluster);
}
}
// What follows is code that is helpful for collecting metrics, visualizing cuts, etc.
// This code is not used in the actual clustering implementation and can be ignored.
static int follow(std::vector<int>& parents, int index)
{
while (index != parents[index])
{
int parent = parents[index];
parents[index] = parents[parent];
index = parent;
}
return index;
}
static int measureComponents(std::vector<int>& parents, const std::vector<unsigned int>& indices, const std::vector<unsigned int>& remap)
{
assert(parents.size() == remap.size());
for (size_t i = 0; i < indices.size(); ++i)
{
unsigned int v = remap[indices[i]];
parents[v] = v;
}
for (size_t i = 0; i < indices.size(); ++i)
{
int v0 = remap[indices[i]];
int v1 = remap[indices[i + (i % 3 == 2 ? -2 : 1)]];
v0 = follow(parents, v0);
v1 = follow(parents, v1);
parents[v0] = v1;
}
for (size_t i = 0; i < indices.size(); ++i)
{
unsigned int v = remap[indices[i]];
parents[v] = follow(parents, v);
}
int roots = 0;
for (size_t i = 0; i < indices.size(); ++i)
{
unsigned int v = remap[indices[i]];
roots += parents[v] == int(v);
parents[v] = -1; // make sure we only count each root once
}
return roots;
}
static size_t measureBoundary(std::vector<int>& used, const std::vector<std::vector<unsigned int> >& clusters, const std::vector<unsigned int>& remap)
{
for (size_t i = 0; i < used.size(); ++i)
used[i] = -1;
// mark vertices that are used by multiple groups with -2
int group = 0;
for (size_t i = 0; i < clusters.size(); ++i)
{
group += clusters[i].empty();
for (size_t j = 0; j < clusters[i].size(); ++j)
{
unsigned int v = remap[clusters[i][j]];
used[v] = (used[v] == -1 || used[v] == group) ? group : -2;
}
}
size_t result = 0;
for (size_t i = 0; i < clusters.size(); ++i)
{
// count vertices that are used by multiple groups and change marks to -1
for (size_t j = 0; j < clusters[i].size(); ++j)
{
unsigned int v = remap[clusters[i][j]];
result += (used[v] == -2);
used[v] = (used[v] == -2) ? -1 : used[v];
}
// change marks back from -1 to -2 for the next pass
for (size_t j = 0; j < clusters[i].size(); ++j)
{
unsigned int v = remap[clusters[i][j]];
used[v] = (used[v] == -1) ? -2 : used[v];
}
}
return int(result);
}
struct Box
{
float min[3];
float max[3];
};
static const Box kDummyBox = {{FLT_MAX, FLT_MAX, FLT_MAX}, {-FLT_MAX, -FLT_MAX, -FLT_MAX}};
static void mergeBox(Box& box, const Box& other)
{
for (int k = 0; k < 3; ++k)
{
box.min[k] = other.min[k] < box.min[k] ? other.min[k] : box.min[k];
box.max[k] = other.max[k] > box.max[k] ? other.max[k] : box.max[k];
}
}
inline float surface(const Box& box)
{
float sx = box.max[0] - box.min[0], sy = box.max[1] - box.min[1], sz = box.max[2] - box.min[2];
return sx * sy + sx * sz + sy * sz;
}
static float sahCost(const Box* boxes, unsigned int* order, unsigned int* temp, size_t count)
{
Box total = boxes[order[0]];
for (size_t i = 1; i < count; ++i)
mergeBox(total, boxes[order[i]]);
int best_axis = -1;
int best_bin = -1;
float best_cost = FLT_MAX;
const int kBins = 15;
for (int axis = 0; axis < 3; ++axis)
{
Box bins[kBins];
unsigned int counts[kBins] = {};
float extent = total.max[axis] - total.min[axis];
if (extent <= 0.f)
continue;
for (int i = 0; i < kBins; ++i)
bins[i] = kDummyBox;
for (size_t i = 0; i < count; ++i)
{
unsigned int index = order[i];
float p = (boxes[index].min[axis] + boxes[index].max[axis]) * 0.5f;
int bin = int((p - total.min[axis]) / extent * (kBins - 1) + 0.5f);
assert(bin >= 0 && bin < kBins);
mergeBox(bins[bin], boxes[index]);
counts[bin]++;
}
Box laccum = kDummyBox, raccum = kDummyBox;
size_t lcount = 0, rcount = 0;
float costs[kBins] = {};
for (int i = 0; i < kBins - 1; ++i)
{
mergeBox(laccum, bins[i]);
mergeBox(raccum, bins[kBins - 1 - i]);
lcount += counts[i];
costs[i] += lcount ? surface(laccum) * lcount : 0.f;
rcount += counts[kBins - 1 - i];
costs[kBins - 2 - i] += rcount ? surface(raccum) * rcount : 0.f;
}
for (int i = 0; i < kBins - 1; ++i)
if (costs[i] < best_cost)
{
best_cost = costs[i];
best_bin = i;
best_axis = axis;
}
}
if (best_axis == -1)
return surface(total) * float(count);
float best_extent = total.max[best_axis] - total.min[best_axis];
size_t offset0 = 0, offset1 = count;
for (size_t i = 0; i < count; ++i)
{
unsigned int index = order[i];
float p = (boxes[index].min[best_axis] + boxes[index].max[best_axis]) * 0.5f;
int bin = int((p - total.min[best_axis]) / best_extent * (kBins - 1) + 0.5f);
assert(bin >= 0 && bin < kBins);
if (bin <= best_bin)
temp[offset0++] = index;
else
temp[--offset1] = index;
}
assert(offset0 == offset1);
if (offset0 == 0 || offset0 == count)
return surface(total) * float(count);
return surface(total) + sahCost(boxes, temp, order, offset0) + sahCost(boxes, temp + offset0, order + offset0, count - offset0);
}
static float sahCost(const Box* boxes, size_t count)
{
if (count == 0)
return 0.f;
std::vector<unsigned int> order(count);
for (size_t i = 0; i < count; ++i)
order[i] = unsigned(i);
std::vector<unsigned int> temp(count);
return sahCost(boxes, &order[0], &temp[0], count);
}
static float sahOverhead(const std::vector<std::vector<unsigned int> >& clusters, const std::vector<Vertex>& vertices)
{
std::vector<Box> all_tris, cluster_tris, cluster_boxes;
float sahc = 0.f;
for (size_t i = 0; i < clusters.size(); ++i)
{
if (clusters[i].empty())
continue;
cluster_tris.clear();
Box cluster_box = kDummyBox;
for (size_t k = 0; k < clusters[i].size(); k += 3)
{
Box box = kDummyBox;
for (int v = 0; v < 3; ++v)
{
const Vertex& vertex = vertices[clusters[i][k + v]];
Box p = {{vertex.px, vertex.py, vertex.pz}, {vertex.px, vertex.py, vertex.pz}};
mergeBox(box, p);
}
mergeBox(cluster_box, box);
all_tris.push_back(box);
cluster_tris.push_back(box);
}
cluster_boxes.push_back(cluster_box);
sahc += sahCost(&cluster_tris[0], cluster_tris.size());
sahc -= surface(cluster_box); // box will be accounted for in tlas
}
sahc += sahCost(&cluster_boxes[0], cluster_boxes.size());
float saht = sahCost(all_tris.data(), all_tris.size());
return sahc / saht;
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
+815
View File
@@ -0,0 +1,815 @@
<!doctype html>
<html lang="en">
<head>
<title>meshoptimizer - demo</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<style>
body {
font-family: Monospace;
background-color: #300a24;
color: #fff;
margin: 0px;
overflow: hidden;
display: flex;
flex-direction: column;
height: 100vh;
}
#container {
width: 100%;
flex: 1;
position: relative;
}
.renderer-container {
position: relative;
width: 100%;
height: 100%;
}
.renderer-container canvas {
width: 100% !important;
height: 100% !important;
display: block;
user-select: none;
}
.renderer-label {
position: absolute;
top: 10px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 14px;
}
.stats-label {
position: absolute;
top: 45px;
left: 10px;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 5px;
pointer-events: none;
font-size: 12px;
line-height: 1.4;
}
#progress-label {
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
#texture-view,
#normal-view,
#material-view {
position: fixed;
left: 10px;
bottom: 10px;
width: 256px;
height: 256px;
display: none;
}
#normal-view {
left: 276px;
}
#material-view {
left: 542px;
}
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three-examples/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/",
"three-mesh-bvh": "https://cdn.jsdelivr.net/npm/three-mesh-bvh@0.9.10/build/index.module.js",
"watlas": "https://cdn.jsdelivr.net/npm/watlas@1.0.1/dist/watlas.js",
"lil-gui": "https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.esm.js"
}
}
</script>
</head>
<body>
<div id="progress-label" class="renderer-label" hidden></div>
<div id="container"></div>
<canvas id="texture-view"></canvas>
<canvas id="normal-view"></canvas>
<canvas id="material-view"></canvas>
<input type="file" id="fileInput" style="display: none" accept=".obj,.glb" />
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three-examples/loaders/GLTFLoader.js';
import { OBJLoader } from 'three-examples/loaders/OBJLoader.js';
import { GLTFExporter } from 'three-examples/exporters/GLTFExporter.js';
import { OrbitControls } from 'three-examples/controls/OrbitControls.js';
import { mergeGeometries, mergeVertices } from 'three-examples/utils/BufferGeometryUtils.js';
import { MeshoptDecoder } from '../js/meshopt_decoder.mjs';
import { MeshoptSimplifier } from '../js/meshopt_simplifier.js';
import { MeshoptTangents } from '../js/meshopt_tangents.js';
import * as repaint from './repaint.js';
import { Initialize as initializeWatlas, Atlas } from 'watlas';
import { GUI } from 'lil-gui';
var container;
var camera, renderer, scene, controls, model, merged, remeshed, remeshMaterial, wireframeMaterial, stats;
var paintCache = null;
var progressLabel = document.getElementById('progress-label');
var settings = {
wireframe: false,
wireframeOverlay: false,
resolution: 100,
thicken: false,
shell: false,
solve: true,
debug: false,
normalCrease: 60,
normalSmoothing: 1.5,
simplify: {
enabled: true,
regularize: true,
solve: true,
errorAuto: false,
errorThresholdLog10: 2.5,
forceTriangleCount: 0,
},
paint: {
mode: 'texture',
resolution: 1024,
gutter: 1,
atlasMode: 'balanced',
preserveNormalSeams: true,
infill: true,
normals: false,
materials: false,
},
loadFile: function () {
var input = document.getElementById('fileInput');
input.onchange = function () {
var file = input.files[0];
var uri = URL.createObjectURL(file);
var ext = file.name.split('.').pop().toLowerCase();
container.querySelector('.renderer-label').textContent = file.name;
load(uri, ext);
};
input.click();
},
remesh: function () {
remesh();
},
revert: function () {
revert();
},
exportFile: function () {
if (!remeshed) return;
exportGlb('remesh.glb');
},
updateModule: function () {
reload();
remesh();
},
autoUpdate: false,
autoUpdateStatus: '',
};
var gui = new GUI({ width: 300 });
gui.add(settings, 'remesh');
gui.add(settings, 'revert');
gui.add(settings, 'loadFile');
gui.add(settings, 'exportFile');
var guiDisplay = gui.addFolder('Display');
guiDisplay.add(settings, 'wireframe').onChange(update);
guiDisplay.add(settings, 'wireframeOverlay').onChange(update);
var guiRemesh = gui.addFolder('Remesh');
guiRemesh.add(settings, 'resolution', 4, 256, 1);
guiRemesh.add(settings, 'thicken').disable();
guiRemesh.add(settings, 'shell');
guiRemesh.add(settings, 'solve');
guiRemesh.add(settings, 'debug');
guiRemesh.add(settings, 'normalCrease', 0, 180, 1).name('normal crease');
guiRemesh.add(settings, 'normalSmoothing', 0, 10, 0.1).name('normal smoothing');
var guiSimplify = gui.addFolder('Simplify');
guiSimplify.add(settings.simplify, 'enabled');
guiSimplify.add(settings.simplify, 'regularize');
guiSimplify.add(settings.simplify, 'solve');
guiSimplify.add(settings.simplify, 'errorAuto');
guiSimplify.add(settings.simplify, 'errorThresholdLog10', 1.5, 3, 0.1);
guiSimplify.add(settings.simplify, 'forceTriangleCount');
var guiPaint = gui.addFolder('Paint');
guiPaint.add(settings.paint, 'mode', ['none', 'vertex', 'facets', 'texture']);
guiPaint.add(settings.paint, 'resolution', [256, 512, 1024, 2048]);
guiPaint.add(settings.paint, 'gutter', 0, 8, 1);
guiPaint.add(settings.paint, 'atlasMode', ['fast', 'balanced', 'quality']);
guiPaint.add(settings.paint, 'preserveNormalSeams');
guiPaint.add(settings.paint, 'infill');
guiPaint.add(settings.paint, 'normals').name('normal map');
guiPaint.add(settings.paint, 'materials').name('material map');
var guiUpdate = gui.addFolder('Update');
guiUpdate.add(settings, 'updateModule');
guiUpdate.add(settings, 'autoUpdate').onChange(autoReload);
guiUpdate.add(settings, 'autoUpdateStatus').listen();
init();
loadDefault();
render();
function createView(name) {
// Create renderer container
var rendererContainer = document.createElement('div');
rendererContainer.className = 'renderer-container';
container.appendChild(rendererContainer);
// Add label with file name
var label = document.createElement('div');
label.className = 'renderer-label';
label.textContent = name;
rendererContainer.appendChild(label);
// Add stats label
var statsLabel = document.createElement('div');
statsLabel.className = 'stats-label';
rendererContainer.appendChild(statsLabel);
// Initialize stats
stats = {
triangles: 0,
vertices: 0,
ratio: 0,
label: statsLabel,
};
// Create renderer
renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
rendererContainer.appendChild(renderer.domElement);
// Create scene
scene = new THREE.Scene();
scene.background = new THREE.Color(0x300a24);
// Add lighting
var ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
var pointLightR = new THREE.PointLight(0xffffff, 4);
pointLightR.position.set(3, 3, 0);
pointLightR.decay = 0.5;
scene.add(pointLightR);
var pointLightL = new THREE.PointLight(0xffffff, 1);
pointLightL.position.set(-3, 5, 0);
pointLightL.decay = 0.5;
scene.add(pointLightL);
remeshMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd, polygonOffsetFactor: 0.5, polygonOffsetUnits: 16 });
wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true, visible: false });
// Create controls
controls = new OrbitControls(camera, renderer.domElement);
// Adjust renderer size
updateRendererSizes();
}
function copyAttribute(attribute, count, itemSize, def) {
var result = new Float32Array(count * itemSize).fill(def);
if (attribute) {
var copy = Math.min(itemSize, attribute.itemSize);
for (var i = 0; i < count; ++i) {
for (var k = 0; k < copy; ++k) {
result[i * itemSize + k] = attribute.getComponent(i, k);
}
}
}
return new THREE.BufferAttribute(result, itemSize);
}
function skinAttribute(object, attribute, w) {
var v = new THREE.Vector4();
for (var i = 0; i < attribute.count; ++i) {
v.set(attribute.getX(i), attribute.getY(i), attribute.getZ(i), w);
object.applyBoneTransform(i, v);
if (w == 0) v.normalize();
attribute.setXYZ(i, v.x, v.y, v.z);
}
}
function mergeMesh(model) {
var geometries = [];
var materials = [];
model.updateMatrixWorld(true);
model.traverse(function (object) {
if (object.isMesh && object.geometry.attributes.position) {
var source = object.geometry.attributes.position;
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', copyAttribute(source, source.count, 3, 0));
// we need original normal/uv/colors for gpu repaint
// for mergeGeometries to agree to merge everything, we maintain consistent attribute sets
geometry.setAttribute('normal', copyAttribute(object.geometry.attributes.normal, source.count, 3, 0));
geometry.setAttribute('uv', copyAttribute(object.geometry.attributes.uv, source.count, 2, 0));
geometry.setAttribute('color', copyAttribute(object.geometry.attributes.color, source.count, 4, 1));
if (object.isSkinnedMesh) {
skinAttribute(object, geometry.attributes.position, 1);
skinAttribute(object, geometry.attributes.normal, 0);
}
var material = Array.isArray(object.material) ? object.material[0] : object.material;
if (materials.indexOf(material) < 0) materials.push(material);
var materialIndex = new Uint32Array(source.count).fill(materials.indexOf(material));
geometry.setAttribute('material', new THREE.BufferAttribute(materialIndex, 1));
if (object.geometry.index) {
geometry.setIndex(object.geometry.index);
} else {
var indices = new Uint32Array(source.count);
for (var i = 0; i < source.count; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
}
// we prefer to use the original tangents, but we need to generate them if not present so that normal baking works
if (object.geometry.attributes.tangent) {
geometry.setAttribute('tangent', copyAttribute(object.geometry.attributes.tangent, source.count, 4, 0));
if (object.isSkinnedMesh) skinAttribute(object, geometry.attributes.tangent, 0);
} else {
geometry = generateTangents(geometry, -1);
geometry = mergeVertices(geometry);
}
geometry.applyMatrix4(object.matrixWorld);
geometries.push(geometry);
}
});
var result = mergeGeometries(geometries);
result.computeBoundingBox();
result.computeBoundingSphere();
result.materials = materials;
result.thickness = result.boundingBox.getSize(new THREE.Vector3()).length() * 0.01;
return result;
}
function unwrapMesh(geometry, resolution, gutter, preserveNormalSeams, maxIterations, maxChartSize) {
var positions = geometry.attributes.position.array;
var indices = new Uint32Array(geometry.index.array);
var triangle = new THREE.Triangle();
var avgTriangleArea = 0;
for (var i = 0; i < indices.length; i += 3) {
triangle.a.fromBufferAttribute(geometry.attributes.position, indices[i + 0]);
triangle.b.fromBufferAttribute(geometry.attributes.position, indices[i + 1]);
triangle.c.fromBufferAttribute(geometry.attributes.position, indices[i + 2]);
avgTriangleArea += triangle.getArea() / (indices.length / 3);
}
var atlas = new Atlas();
atlas.addMesh({
vertexPositionData: positions,
vertexCount: positions.length / 3,
vertexPositionStride: 12,
vertexNormalData: geometry.attributes.normal.array,
vertexNormalStride: 12,
indexData: indices,
indexCount: indices.length,
});
atlas.generate(
{ normalSeamWeight: preserveNormalSeams ? 1000 : 0, maxIterations: maxIterations, maxChartArea: maxChartSize * avgTriangleArea },
{ resolution: resolution, padding: gutter }
);
var mesh = atlas.getMesh(0);
var atlasIndices = new Uint32Array(mesh.indexCount);
var uv = new Float32Array(mesh.indexCount * 2);
mesh.getIndexArray(atlasIndices);
for (var i = 0; i < mesh.indexCount; ++i) {
var v = mesh.getVertex(atlasIndices[i]);
uv[i * 2 + 0] = v.uv[0] / atlas.width;
uv[i * 2 + 1] = v.uv[1] / atlas.height;
}
atlas.delete();
// uvs are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = geometry.toNonIndexed();
geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
return geometry;
}
function generateTangents(geometry, sign = 1) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var tangents = MeshoptTangents.generateTangents(indices, attr.position.array, 3, attr.normal.array, 3, attr.uv.array, 2);
for (var i = 3; i < tangents.length; i += 4) tangents[i] *= sign;
// tangents are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
if (indices) geometry = geometry.toNonIndexed();
geometry.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4));
return geometry;
}
function generateNormals(geometry, creaseAngle, smoothing) {
var attr = geometry.attributes;
var indices = geometry.index ? new Uint32Array(geometry.index.array) : null;
var normals = MeshoptTangents.generateNormals(indices, attr.position.array, 3, creaseAngle, smoothing);
// normals are computed per corner, for simplicity we deindex the geometry and expect caller to reindex
geometry = indices ? geometry.toNonIndexed() : geometry.clone();
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
return geometry;
}
function showTexture(id, texture) {
var canvas = document.getElementById(id);
canvas.style.display = texture ? 'block' : 'none';
if (!texture) return;
canvas.width = texture.image.width;
canvas.height = texture.image.height;
canvas.getContext('2d').putImageData(new ImageData(new Uint8ClampedArray(texture.image.data), canvas.width, canvas.height), 0, 0);
}
function clearRemeshed() {
if (remeshed) {
scene.remove(remeshed);
remeshed.geometry.dispose();
}
remeshed = undefined;
if (remeshMaterial.map) remeshMaterial.map.dispose();
remeshMaterial.map = null;
if (remeshMaterial.normalMap) remeshMaterial.normalMap.dispose();
remeshMaterial.normalMap = null;
// roughness and metalness share one texture, so we only dispose once
if (remeshMaterial.metalnessMap) remeshMaterial.metalnessMap.dispose();
remeshMaterial.metalnessMap = null;
remeshMaterial.roughnessMap = null;
showTexture('texture-view', null);
showTexture('normal-view', null);
showTexture('material-view', null);
}
function simplifyMesh(geo, target, threshold, flags, solve) {
var positions = solve ? new Float32Array(geo.attributes.position.array) : geo.attributes.position.array;
var indices = solve ? new Uint32Array(geo.index.array) : geo.index.array;
var res;
if (solve) {
res = MeshoptSimplifier.simplifyWithUpdate(indices, positions, 3, new Float32Array(), 0, [], null, target * 3, threshold, flags);
res[0] = indices.slice(0, res[0]);
} else {
res = MeshoptSimplifier.simplify(indices, positions, 3, target * 3, threshold, flags);
}
var [remap, unique] = MeshoptSimplifier.compactMesh(res[0]);
var compact = new Float32Array(unique * 3);
for (var i = 0; i < remap.length; ++i) {
if (remap[i] < unique) compact.set(positions.subarray(i * 3, i * 3 + 3), remap[i] * 3);
}
var rgeo = geo.clone();
rgeo.setAttribute('position', new THREE.BufferAttribute(compact, 3));
rgeo.setIndex(new THREE.BufferAttribute(res[0], 1));
return rgeo;
}
function showProgress(stage) {
progressLabel.textContent = stage;
progressLabel.hidden = false;
return new Promise(requestAnimationFrame);
}
async function remesh() {
if (!merged) return;
await Promise.all([MeshoptSimplifier.ready, MeshoptTangents.ready, initializeWatlas()]);
await showProgress('remesh');
var flags = [];
if (settings.thicken) flags.push('Thicken');
if (settings.shell) flags.push('Shell');
if (settings.solve) flags.push('Solve');
if (settings.debug) flags.push('_InternalDebug');
var result = MeshoptSimplifier.remesh(merged.index.array, merged.attributes.position.array, 3, settings.resolution, flags);
var geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(result, 3));
geometry.setIndex(new THREE.BufferAttribute(MeshoptSimplifier.generatePositionRemap(result, 3), 1));
if (settings.simplify.enabled) {
await showProgress('simplify');
var sflags = ['PreserveFolds', 'Sparse']; // Sparse is needed because we skip position compaction above to be able to use quick remap to generate indices
if (settings.simplify.regularize) sflags.push('RegularizeLight');
var error = settings.simplify.forceTriangleCount ? 1 : Math.pow(10, -settings.simplify.errorThresholdLog10);
if (settings.simplify.errorAuto) {
sflags.push('ErrorAbsolute'); // merged mesh is in world space, so we don't need to adjust error by mesh scale and can simply use world space units
var distance = Math.max(camera.position.distanceTo(merged.boundingSphere.center) - merged.boundingSphere.radius, 0);
error = distance * 1e-3 * (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov) * 0.5)); // ~1 pixel at 1k x 1k
}
geometry = simplifyMesh(geometry, settings.simplify.forceTriangleCount, error, sflags, settings.simplify.solve);
}
await showProgress('normals');
geometry = mergeVertices(
generateNormals(geometry, THREE.MathUtils.degToRad(settings.debug ? 30 : settings.normalCrease), settings.normalSmoothing)
);
var material = remeshMaterial.clone();
material.map = material.normalMap = material.metalnessMap = material.roughnessMap = null;
material.vertexColors = false;
material.color.set(0xffffff);
material.metalness = 0;
material.roughness = 1;
if (settings.paint.mode != 'none' && !paintCache) {
await showProgress('paint cache');
paintCache = repaint.buildCache(merged, merged.materials);
}
if (settings.paint.mode == 'vertex') {
await showProgress('repaint');
repaint.transferColors(renderer, geometry, paintCache, merged.thickness);
material.vertexColors = true;
} else if (settings.paint.mode == 'facets') {
await showProgress('repaint');
geometry = geometry.toNonIndexed();
geometry.computeVertexNormals();
var indices = new Uint32Array(geometry.attributes.position.count);
for (var i = 0; i < indices.length; ++i) indices[i] = i;
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
repaint.transferColors(renderer, geometry, paintCache, merged.thickness, true);
material.vertexColors = true;
} else if (settings.paint.mode == 'texture') {
await showProgress('unwrap');
var maxIterations = settings.paint.atlasMode == 'quality' ? 1 : 0;
var maxChartSize = settings.paint.atlasMode == 'fast' ? 256 : settings.paint.atlasMode == 'balanced' ? 512 : 1024;
var preserveSeams = settings.paint.preserveNormalSeams;
geometry = unwrapMesh(geometry, settings.paint.resolution, settings.paint.gutter, preserveSeams, maxIterations, maxChartSize);
if (settings.paint.normals) {
await showProgress('tangents');
geometry = generateTangents(geometry);
}
geometry = mergeVertices(geometry);
await showProgress('repaint');
var textures = repaint.transferTexture(
renderer,
geometry,
paintCache,
merged.thickness,
settings.paint.resolution,
settings.paint.gutter,
settings.paint.infill,
settings.paint.normals,
settings.paint.materials
);
material.map = textures.map;
material.normalMap = textures.normalMap;
if (settings.paint.materials) {
material.metalnessMap = textures.materialMap;
material.roughnessMap = textures.materialMap;
material.metalness = 1;
} else if (settings.paint.normals) {
// we need this to have a specular response
material.roughness = 0.5;
}
} else {
material.color.set(0xdddddd);
}
material.needsUpdate = true;
// remesh material + wireframe overlay material
geometry.addGroup(0, geometry.index.count, 0);
geometry.addGroup(0, geometry.index.count, 1);
clearRemeshed();
remeshMaterial.dispose();
remeshMaterial = material;
remeshed = new THREE.Mesh(geometry, [remeshMaterial, wireframeMaterial]);
model.visible = false;
scene.add(remeshed);
showTexture('texture-view', remeshMaterial.map);
showTexture('normal-view', remeshMaterial.normalMap);
showTexture('material-view', remeshMaterial.metalnessMap);
update();
var triangles = geometry.index.count / 3;
stats.triangles = triangles;
stats.vertices = geometry.attributes.position.count;
stats.ratio = (triangles / (merged.index.count / 3)) * 100;
updateStatsDisplay();
progressLabel.hidden = true;
}
function exportGlb(name) {
// can't export remeshed directly as it uses debug materials for wireframe
var mesh = new THREE.Mesh(remeshed.geometry, remeshMaterial);
new GLTFExporter().parse(
mesh,
function (glb) {
var link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([glb], { type: 'model/gltf-binary' }));
link.download = name;
link.click();
// may resolve rare race conditions
setTimeout(function () {
URL.revokeObjectURL(link.href);
}, 0);
},
function (e) {
console.log(e);
},
{ binary: true }
);
}
function revert() {
if (!model) return;
model.visible = true;
clearRemeshed();
stats.triangles = 0;
stats.vertices = 0;
stats.ratio = 100;
model.traverse(function (object) {
if (object.isMesh) {
stats.triangles += (object.geometry.index ? object.geometry.index.count : object.geometry.attributes.position.count) / 3;
stats.vertices += object.geometry.attributes.position.count;
}
});
updateStatsDisplay();
}
function updateStatsDisplay() {
stats.label.innerHTML =
'Triangles: ' + stats.triangles + '<br>' + 'Vertices: ' + stats.vertices + '<br>' + 'Ratio: ' + stats.ratio.toFixed(1) + '%';
}
function reload() {
var simp = import('/js/meshopt_simplifier.js?x=' + Date.now());
MeshoptSimplifier.ready = simp.then(function (s) {
return s.MeshoptSimplifier.ready.then(function () {
for (var prop in s.MeshoptSimplifier) {
MeshoptSimplifier[prop] = s.MeshoptSimplifier[prop];
}
});
});
}
var moduleLastModified = 0;
function autoReload() {
if (!settings.autoUpdate) return;
fetch('/js/meshopt_simplifier.js?x=' + Date.now(), { method: 'HEAD' })
.then(function (r) {
var last = r.headers.get('Last-Modified');
if (last != moduleLastModified) {
moduleLastModified = last;
reload();
remesh();
}
settings.autoUpdateStatus = new Date(last).toLocaleTimeString();
setTimeout(autoReload, 1000);
})
.catch(function (e) {
settings.autoUpdateStatus = 'error';
setTimeout(autoReload, 5000);
});
}
function update() {
remeshMaterial.polygonOffset = settings.wireframeOverlay;
wireframeMaterial.visible = settings.wireframeOverlay;
scene.traverse(function (child) {
if (child.isMesh) {
if (Array.isArray(child.material)) {
child.material[0].wireframe = settings.wireframe;
} else {
child.material.wireframe = settings.wireframe;
}
}
});
}
function load(path, ext) {
if (model) scene.remove(model);
clearRemeshed();
if (paintCache) paintCache.dispose();
paintCache = null;
model = undefined;
merged = undefined;
var onProgress = function (xhr) {};
var onError = function (e) {
console.log(e);
};
function center(model) {
var bbox = new THREE.Box3().setFromObject(model);
var scale = 2 / Math.max(bbox.max.x - bbox.min.x, bbox.max.y - bbox.min.y, bbox.max.z - bbox.min.z);
var offset = new THREE.Vector3().addVectors(bbox.max, bbox.min).multiplyScalar(scale / 2);
model.scale.set(scale, scale, scale);
model.position.set(-offset.x, -offset.y, -offset.z);
}
function finish(object) {
model = object;
center(model);
scene.add(model);
merged = mergeMesh(model);
revert();
controls.target.set(0, 0, 0);
}
if (ext == 'gltf' || ext == 'glb') {
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load(
path,
function (gltf) {
finish(gltf.scene);
},
onProgress,
onError
);
} else if (ext == 'obj') {
var loader = new OBJLoader();
loader.load(
path,
function (obj) {
finish(obj);
},
onProgress,
onError
);
} else {
console.error('Unsupported file format');
}
}
function loadDefault() {
createView('pirate.glb');
load('pirate.glb', 'glb');
}
function init() {
container = document.getElementById('container');
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.y = 1.0;
camera.position.z = 3.0;
window.addEventListener('resize', updateRendererSizes, false);
}
function updateRendererSizes() {
var rendererContainer = renderer.domElement.parentElement;
var aspectRatio = rendererContainer.clientWidth / rendererContainer.clientHeight;
camera.aspect = aspectRatio;
camera.updateProjectionMatrix();
renderer.setSize(rendererContainer.clientWidth, rendererContainer.clientHeight);
}
function render() {
requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
}
</script>
</body>
</html>
+593
View File
@@ -0,0 +1,593 @@
// GPU color/texture transfer for remeshing demo
//
// Remeshed atlas is rasterized in UV space, the triangle edges are expanded to cover extra gutter space.
// The fragment shader traces rays against the original mesh (falling back to closest point on miss).
// The results are shaded using a simplified version of the original material, and written to the output texture.
// Texels that were not rasterized are infilled afterwards using the mips of the resulting texture.
// When painting vertex colors, we directly rasterize points for target vertex/triangles and trace rays/eval materials as above.
import * as THREE from 'three';
import { MeshBVH, BVHShaderGLSL, MeshBVHUniformStruct, FloatVertexAttributeTexture, UIntVertexAttributeTexture } from 'three-mesh-bvh';
function expandGeometry(geometry, size, gutter) {
var position = geometry.attributes.position;
var normal = geometry.attributes.normal;
var tangent = geometry.attributes.tangent;
var uv = geometry.attributes.uv;
var indices = geometry.index.array;
var count = indices.length;
var positions = new Float32Array(count * 3);
var normals = new Float32Array(count * 3);
var tangents = tangent ? new Float32Array(count * 4) : null;
var uvs = new Float32Array(count * 2);
for (var i = 0; i < count; i += 3) {
var a = indices[i],
b = indices[i + 1],
c = indices[i + 2];
var x0 = uv.getX(a) * size,
y0 = uv.getY(a) * size;
var x1 = uv.getX(b) * size,
y1 = uv.getY(b) * size;
var x2 = uv.getX(c) * size,
y2 = uv.getY(c) * size;
// shift corners away from the opposite edge center
var d0 = Math.hypot(x0 * 2 - x1 - x2, y0 * 2 - y1 - y2);
var d1 = Math.hypot(x1 * 2 - x0 - x2, y1 * 2 - y0 - y2);
var d2 = Math.hypot(x2 * 2 - x0 - x1, y2 * 2 - y0 - y1);
var e0 = d0 == 0 ? 0 : gutter / d0;
var e1 = d1 == 0 ? 0 : gutter / d1;
var e2 = d2 == 0 ? 0 : gutter / d2;
for (var j = 0; j < 3; ++j) {
// expand each triangle in barycentric space to accommodate extra gutter width
var ej = j == 0 ? e0 : j == 1 ? e1 : e2;
var w0 = j == 0 ? 1 + ej * 2 : -ej;
var w1 = j == 1 ? 1 + ej * 2 : -ej;
var w2 = j == 2 ? 1 + ej * 2 : -ej;
// interpolate all attributes; we will use UVs to rasterize and positions/normals to trace against the original mesh
var v = i + j;
positions[v * 3 + 0] = position.getX(a) * w0 + position.getX(b) * w1 + position.getX(c) * w2;
positions[v * 3 + 1] = position.getY(a) * w0 + position.getY(b) * w1 + position.getY(c) * w2;
positions[v * 3 + 2] = position.getZ(a) * w0 + position.getZ(b) * w1 + position.getZ(c) * w2;
normals[v * 3 + 0] = normal.getX(a) * w0 + normal.getX(b) * w1 + normal.getX(c) * w2;
normals[v * 3 + 1] = normal.getY(a) * w0 + normal.getY(b) * w1 + normal.getY(c) * w2;
normals[v * 3 + 2] = normal.getZ(a) * w0 + normal.getZ(b) * w1 + normal.getZ(c) * w2;
if (tangent) {
// tangents are only used when normal maps are baked
tangents[v * 4 + 0] = tangent.getX(a) * w0 + tangent.getX(b) * w1 + tangent.getX(c) * w2;
tangents[v * 4 + 1] = tangent.getY(a) * w0 + tangent.getY(b) * w1 + tangent.getY(c) * w2;
tangents[v * 4 + 2] = tangent.getZ(a) * w0 + tangent.getZ(b) * w1 + tangent.getZ(c) * w2;
tangents[v * 4 + 3] = tangent.getW(a) * w0 + tangent.getW(b) * w1 + tangent.getW(c) * w2;
}
uvs[v * 2 + 0] = (x0 * w0 + x1 * w1 + x2 * w2) / size;
uvs[v * 2 + 1] = (y0 * w0 + y1 * w1 + y2 * w2) / size;
}
}
var result = new THREE.BufferGeometry();
result.setAttribute('position', new THREE.BufferAttribute(positions, 3));
result.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
result.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
if (tangent) result.setAttribute('tangent', new THREE.BufferAttribute(tangents, 4));
return result;
}
function sampleGeometry(geometry, size) {
var position = geometry.attributes.position;
var normal = geometry.attributes.normal;
var indices = geometry.index.array;
var vertices = position.count;
var triangles = indices.length / 3;
var samples = vertices + triangles; // one sample per vertex plus one sample per triangle centroid
var positions = new Float32Array(samples * 3);
var normals = new Float32Array(samples * 3);
var uvs = new Float32Array(samples * 2);
for (var i = 0; i < vertices; ++i) {
for (var k = 0; k < 3; ++k) {
positions[i * 3 + k] = position.getComponent(i, k);
normals[i * 3 + k] = normal.getComponent(i, k);
}
}
for (var i = 0; i < triangles; ++i) {
var a = indices[i * 3 + 0],
b = indices[i * 3 + 1],
c = indices[i * 3 + 2];
// normals will be normalized in the shader
for (var k = 0; k < 3; ++k) {
positions[(vertices + i) * 3 + k] = (position.getComponent(a, k) + position.getComponent(b, k) + position.getComponent(c, k)) / 3;
normals[(vertices + i) * 3 + k] = normal.getComponent(a, k) + normal.getComponent(b, k) + normal.getComponent(c, k);
}
}
for (var i = 0; i < samples; ++i) {
uvs[i * 2 + 0] = ((i % size) + 0.5) / size;
uvs[i * 2 + 1] = (Math.floor(i / size) + 0.5) / size;
}
var result = new THREE.BufferGeometry();
result.setAttribute('position', new THREE.BufferAttribute(positions, 3));
result.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
result.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
return result;
}
function extractColors(data, colors, indices, facets) {
var vertices = colors.length / 4;
var triangles = indices.length / 3;
for (var i = 0; i < vertices; ++i) {
if (data[i * 4 + 3] == 0) continue;
for (var k = 0; k < 3; ++k) colors[i * 4 + k] = Math.pow(data[i * 4 + k] / 255, 2.2);
colors[i * 4 + 3] = 1;
}
for (var i = 0; i < triangles; ++i) {
var offset = (vertices + i) * 4;
if (data[offset + 3] == 0) continue;
for (var j = 0; j < 3; ++j) {
var v = indices[i * 3 + j];
for (var k = 0; k < 3; ++k) colors[v * 4 + k] += Math.pow(data[offset + k] / 255, 2.2);
colors[v * 4 + 3]++;
}
}
for (var i = 0; i < colors.length; i += 4) {
var weight = colors[i + 3];
if (weight == 0) continue;
for (var k = 0; k < 3; ++k) colors[i + k] /= weight;
colors[i + 3] = 1;
}
if (facets) {
for (var i = 0; i < triangles; ++i) {
var a = indices[i * 3 + 0],
b = indices[i * 3 + 1],
c = indices[i * 3 + 2];
for (var k = 0; k < 3; ++k) {
var avg = (colors[a * 4 + k] + colors[b * 4 + k] + colors[c * 4 + k]) / 3;
colors[a * 4 + k] = colors[b * 4 + k] = colors[c * 4 + k] = avg;
}
}
}
}
var MAP_SIZE = 1024;
var MAP_BUDGET = 1 << 30;
var MAP_SLOTS = ['map', 'aoMap', 'emissiveMap', 'roughnessMap', 'metalnessMap', 'normalMap'];
function createTextureArray(materials) {
var textures = [];
var layers = new Map();
for (var i = 0; i < materials.length; ++i) {
for (var j = 0; j < MAP_SLOTS.length; ++j) {
var texture = materials[i][MAP_SLOTS[j]];
// deduplicate images by source UUID to avoid wasting layers for reused images
if (texture && texture.image && !layers.has(texture.source)) {
layers.set(texture.source, textures.length);
textures.push(texture);
}
}
}
var depth = Math.max(textures.length, 1);
var size = MAP_SIZE;
// prevent accidental catastrophic memory use by downscaling texture array until it fits
while (size > 4 && depth * size * size * 4 > MAP_BUDGET) size /= 2;
// all images need to be resized to the same size because we need all of them in one texture array
var canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
var context = canvas.getContext('2d', { willReadFrequently: true });
var data = new Uint8Array(size * size * 4 * depth);
for (var i = 0; i < textures.length; ++i) {
context.clearRect(0, 0, size, size);
context.drawImage(textures[i].image, 0, 0, size, size);
data.set(context.getImageData(0, 0, size, size).data, i * size * size * 4);
}
var result = new THREE.DataArrayTexture(data, size, size, depth);
result.wrapS = THREE.RepeatWrapping;
result.wrapT = THREE.RepeatWrapping;
result.minFilter = THREE.LinearFilter;
result.magFilter = THREE.LinearFilter;
result.needsUpdate = true;
return { array: result, layers: layers };
}
function createMaterialTexture(materials, layers) {
var stride = 24;
var count = Math.max(materials.length, 1);
var data = new Float32Array(count * stride);
function getLayer(material, slot) {
var texture = material[slot];
return texture && layers.has(texture.source) ? layers.get(texture.source) : -1;
}
for (var i = 0; i < materials.length; ++i) {
var material = materials[i];
var map = material.map;
// simplify UV transform by only taking repeat/offset from main texture
data[i * stride + 0] = map ? map.repeat.x : 1;
data[i * stride + 1] = map ? map.repeat.y : 1;
data[i * stride + 2] = map ? map.offset.x : 0;
data[i * stride + 3] = map ? map.offset.y : 0;
// layer indices
for (var j = 0; j < MAP_SLOTS.length; ++j) {
data[i * stride + 4 + j] = getLayer(material, MAP_SLOTS[j]);
}
// material parameters
data[i * stride + 12] = material.color ? material.color.r : 1;
data[i * stride + 13] = material.color ? material.color.g : 1;
data[i * stride + 14] = material.color ? material.color.b : 1;
data[i * stride + 15] = material.aoMapIntensity !== undefined ? material.aoMapIntensity : 1;
data[i * stride + 16] = material.emissive ? material.emissive.r * material.emissiveIntensity : 0;
data[i * stride + 17] = material.emissive ? material.emissive.g * material.emissiveIntensity : 0;
data[i * stride + 18] = material.emissive ? material.emissive.b * material.emissiveIntensity : 0;
data[i * stride + 20] = material.metalness !== undefined ? material.metalness : 0;
data[i * stride + 21] = material.roughness !== undefined ? material.roughness : 1;
data[i * stride + 22] = material.normalScale ? material.normalScale.x : 1;
}
var texture = new THREE.DataTexture(data, stride / 4, count, THREE.RGBAFormat, THREE.FloatType);
texture.needsUpdate = true;
return texture;
}
var bakeShader = new THREE.ShaderMaterial({
glslVersion: THREE.GLSL3,
uniforms: {
bvh: { value: null },
attrNormal: { value: null },
attrUv: { value: null },
attrColor: { value: null },
attrTangent: { value: null },
attrMaterial: { value: null },
materials: { value: null },
textures: { value: null },
thickness: { value: 0 },
bakeNormals: { value: 0 },
bakeMaterial: { value: 0 },
},
vertexShader: `
attribute vec4 tangent;
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vTangent;
void main() {
vPosition = position;
vNormal = normal;
vTangent = tangent;
gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0);
gl_PointSize = 1.0; // for vertex colors
}
`,
fragmentShader: `
precision highp isampler2D;
precision highp usampler2D;
precision highp sampler2DArray;
${BVHShaderGLSL.common_functions}
${BVHShaderGLSL.bvh_struct_definitions}
${BVHShaderGLSL.bvh_ray_functions}
${BVHShaderGLSL.bvh_distance_functions}
uniform BVH bvh;
uniform sampler2D attrNormal;
uniform sampler2D attrUv;
uniform sampler2D attrColor;
uniform sampler2D attrTangent;
uniform usampler2D attrMaterial;
uniform sampler2D materials;
uniform sampler2DArray textures;
uniform float thickness;
uniform float bakeNormals;
uniform float bakeMaterial;
vec3 fromsrgb(vec3 c) {
return pow(c, vec3(2.2));
}
vec3 tosrgb(vec3 c) {
return pow(c, vec3(1.0 / 2.2));
}
varying vec3 vPosition;
varying vec3 vNormal;
varying vec4 vTangent;
layout(location = 0) out vec4 outColor;
layout(location = 1) out vec4 outNormal;
layout(location = 2) out vec4 outMaterial;
void main() {
vec3 normal = normalize(vNormal);
uvec4 face = uvec4(0u);
vec3 faceNormal = vec3(0.0), bary = vec3(0.0), closest = vec3(0.0);
float side = 1.0, dist = 0.0;
float maxClosest = thickness * 10.0;
if (bvhIntersectFirstHit(bvh, vPosition + normal * thickness, -normal, face, faceNormal, bary, side, dist) && dist < thickness * 2.0) {
// ray hit
} else if (bvhClosestPointToPoint(bvh, vPosition, maxClosest, face, faceNormal, bary, side, closest) < maxClosest) {
// closest point found; note, bary is ordered incorrectly in three-mesh-bvh for closest point so we need to swizzle it
bary = bary.zxy;
} else {
discard;
}
vec2 uv = textureSampleBarycoord(attrUv, bary, face.xyz).xy;
int mati = int(uTexelFetch1D(attrMaterial, face.x).r);
vec4 md0 = texelFetch(materials, ivec2(0, mati), 0);
vec4 md1 = texelFetch(materials, ivec2(1, mati), 0);
vec4 md2 = texelFetch(materials, ivec2(2, mati), 0);
vec4 md3 = texelFetch(materials, ivec2(3, mati), 0);
vec4 md4 = texelFetch(materials, ivec2(4, mati), 0);
vec4 md5 = texelFetch(materials, ivec2(5, mati), 0);
// md0: uv transform, md1.xyzw + md2.xy: layer indices in MAP_SLOTS order,
// md3: base color + ao intensity, md4.xyz: emissive color, md5.xyz: metalness + roughness + normal scale
vec2 point = uv * md0.xy + md0.zw;
vec3 color = md3.rgb;
if (md1.x >= 0.0) color *= fromsrgb(texture(textures, vec3(point, md1.x)).rgb);
// vertex colors are linear, and are white when the source mesh or material doesn't use them
color *= textureSampleBarycoord(attrColor, bary, face.xyz).rgb;
if (md1.y >= 0.0) {
float occlusion = texture(textures, vec3(point, md1.y)).r;
color *= 1.0 + md3.w * (occlusion - 1.0);
}
float roughness = md5.y;
float metalness = md5.x;
if (md1.w >= 0.0) roughness *= texture(textures, vec3(point, md1.w)).g;
if (md2.x >= 0.0) metalness *= texture(textures, vec3(point, md2.x)).b;
// approximate metalness/roughness impact when material is not baked
if (bakeMaterial < 0.5) color *= 1.0 - metalness * (1.0 - roughness * roughness * roughness);
vec3 emissive = md4.rgb;
if (md1.z >= 0.0) emissive *= fromsrgb(texture(textures, vec3(point, md1.z)).rgb);
color += emissive;
outColor = vec4(tosrgb(color), 1.0);
outNormal = vec4(0.0);
outMaterial = vec4(0.0, roughness, metalness, 1.0);
if (bakeNormals > 0.5) {
vec3 snormal = textureSampleBarycoord(attrNormal, bary, face.xyz).xyz;
if (md2.y >= 0.0) {
vec4 stangent = textureSampleBarycoord(attrTangent, bary, face.xyz);
vec3 bitangent = cross(snormal, stangent.xyz) * (stangent.w < 0.0 ? -1.0 : 1.0);
vec3 nmap = texture(textures, vec3(point, md2.y)).xyz * 2.0 - 1.0;
nmap.xy *= md5.z;
// replace source (smooth) normal with perturbed normal
snormal = stangent.xyz * nmap.x + bitangent * nmap.y + snormal * nmap.z;
}
vec3 axisN = normal;
vec3 axisT = vTangent.xyz - axisN * dot(axisN, vTangent.xyz);
vec3 axisB = cross(axisN, axisT) * (vTangent.w < 0.0 ? -1.0 : 1.0);
vec3 tbn = vec3(dot(snormal, axisT), dot(snormal, axisB), dot(snormal, axisN));
outNormal = vec4(normalize(tbn) * 0.5 + 0.5, 1.0);
}
}
`,
depthTest: false,
depthWrite: false,
side: THREE.DoubleSide,
});
var infillShader = new THREE.ShaderMaterial({
glslVersion: THREE.GLSL3,
uniforms: {
map: { value: null },
},
vertexShader: `
void main() {
gl_Position = vec4(vec2(gl_VertexID & 1, gl_VertexID >> 1) * 4.0 - 1.0, 0.0, 1.0);
}
`,
fragmentShader: `
uniform sampler2D map;
layout(location = 0) out vec4 result;
void main() {
ivec2 p = ivec2(gl_FragCoord.xy);
vec2 uv = (vec2(p) + 0.5) / vec2(textureSize(map, 0));
// use first texel with non-zero alpha as fallback
vec4 c = texelFetch(map, p, 0);
for (int l = 1; l <= 10 && c.a < 1e-4; ++l) c = textureLod(map, uv, float(l));
// filtering results in premultiplied value that we need to unpremultiply
result = vec4(c.rgb / max(c.a, 1e-4), 1.0);
}
`,
depthTest: false,
depthWrite: false,
});
function bindCache(cache, thickness, bakeNormals, bakeMaterial) {
bakeShader.uniforms.bvh.value = cache.bvh;
bakeShader.uniforms.attrNormal.value = cache.attributes.normal;
bakeShader.uniforms.attrUv.value = cache.attributes.uv;
bakeShader.uniforms.attrColor.value = cache.attributes.color;
bakeShader.uniforms.attrTangent.value = cache.attributes.tangent;
bakeShader.uniforms.attrMaterial.value = cache.attributes.material;
bakeShader.uniforms.materials.value = cache.materials;
bakeShader.uniforms.textures.value = cache.textures;
bakeShader.uniforms.thickness.value = thickness;
bakeShader.uniforms.bakeNormals.value = bakeNormals ? 1 : 0;
bakeShader.uniforms.bakeMaterial.value = bakeMaterial ? 1 : 0;
}
function renderSamples(renderer, object, size, count, infill) {
var target = new THREE.WebGLRenderTarget(size, size, {
count: count,
minFilter: THREE.LinearMipmapNearestFilter, // we use linear filtering with explicit lod for infill
depthBuffer: false,
generateMipmaps: infill,
});
var infilled = [];
for (var i = 0; infill && i < count; ++i) {
infilled.push(new THREE.WebGLRenderTarget(size, size, { depthBuffer: false }));
}
renderer.setClearColor(0x000000, 0);
renderer.setRenderTarget(target);
renderer.render(object, new THREE.OrthographicCamera());
if (infill) {
var infillQuad = new THREE.Mesh(new THREE.BufferGeometry(), infillShader);
infillQuad.geometry.setDrawRange(0, 3);
infillQuad.frustumCulled = false;
for (var i = 0; i < count; ++i) {
infillShader.uniforms.map.value = target.textures[i];
renderer.setRenderTarget(infilled[i]);
renderer.render(infillQuad, new THREE.OrthographicCamera());
}
}
renderer.setRenderTarget(null);
var data = [];
for (var i = 0; i < count; ++i) {
data.push(new Uint8Array(size * size * 4));
renderer.readRenderTargetPixels(infill ? infilled[i] : target, 0, 0, size, size, data[i], undefined, infill ? 0 : i);
}
target.dispose();
for (var i = 0; i < infilled.length; ++i) infilled[i].dispose();
return data;
}
// data comes from render texture, but we need it as DataTexture to be compatible with canvas/GLB export
function createTexture(data, size, srgb) {
var texture = new THREE.DataTexture(data, size, size, THREE.RGBAFormat);
if (srgb) texture.colorSpace = THREE.SRGBColorSpace;
texture.magFilter = THREE.LinearFilter;
texture.minFilter = THREE.LinearFilter;
texture.needsUpdate = true;
return texture;
}
export function buildCache(geometry, materials) {
var bvh = new MeshBVH(geometry, { indirect: true });
var bvhgpu = new MeshBVHUniformStruct();
var attrgpu = {
normal: new FloatVertexAttributeTexture(),
uv: new FloatVertexAttributeTexture(),
color: new FloatVertexAttributeTexture(),
tangent: new FloatVertexAttributeTexture(),
material: new UIntVertexAttributeTexture(),
};
bvhgpu.updateFrom(bvh);
attrgpu.normal.updateFrom(geometry.attributes.normal);
attrgpu.uv.updateFrom(geometry.attributes.uv);
attrgpu.color.updateFrom(geometry.attributes.color);
attrgpu.tangent.updateFrom(geometry.attributes.tangent);
attrgpu.material.updateFrom(geometry.attributes.material);
var textures = createTextureArray(materials);
var matgpu = createMaterialTexture(materials, textures.layers);
return {
bvh: bvhgpu,
attributes: attrgpu,
materials: matgpu,
textures: textures.array,
dispose: function () {
this.bvh.dispose();
this.materials.dispose();
this.textures.dispose();
for (var k in this.attributes) this.attributes[k].dispose();
},
};
}
export function transferTexture(renderer, geometry, cache, thickness, size, gutter, infill, bakeNormals, bakeMaterial) {
var expanded = expandGeometry(geometry, size, gutter);
var mesh = new THREE.Mesh(expanded, bakeShader);
mesh.frustumCulled = false;
bindCache(cache, thickness, bakeNormals, bakeMaterial);
var targets = bakeMaterial ? 3 : bakeNormals ? 2 : 1;
var data = renderSamples(renderer, mesh, size, targets, infill);
expanded.dispose();
return {
map: createTexture(data[0], size, true),
normalMap: bakeNormals ? createTexture(data[1], size, false) : null,
materialMap: bakeMaterial ? createTexture(data[2], size, false) : null,
};
}
export function transferColors(renderer, geometry, cache, thickness, facets) {
var vertices = geometry.attributes.position.count;
var triangles = geometry.index.array.length / 3;
var size = Math.ceil(Math.sqrt(vertices + triangles));
var samples = sampleGeometry(geometry, size);
var points = new THREE.Points(samples, bakeShader);
points.frustumCulled = false;
bindCache(cache, thickness, false, false);
var data = renderSamples(renderer, points, size, 1)[0];
samples.dispose();
var colors = new Float32Array(vertices * 4);
extractColors(data, colors, geometry.index.array, facets);
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
DisableFormat: true
+7240
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+800
View File
@@ -0,0 +1,800 @@
/*# Small Deflate
`sdefl` is a small bare bone lossless compression library in ANSI C (ISO C90)
which implements the Deflate (RFC 1951) compressed data format specification standard.
It is mainly tuned to get as much speed and compression ratio from as little code
as needed to keep the implementation as concise as possible.
## Features
- Portable single header and source file duo written in ANSI C (ISO C90)
- Dual license with either MIT or public domain
- Small implementation
- Deflate: 525 LoC
- Inflate: 320 LoC
- Webassembly:
- Deflate ~3.7 KB (~2.2KB compressed)
- Inflate ~3.6 KB (~2.2KB compressed)
## Usage:
This file behaves differently depending on what symbols you define
before including it.
Header-File mode:
If you do not define `SDEFL_IMPLEMENTATION` before including this file, it
will operate in header only mode. In this mode it declares all used structs
and the API of the library without including the implementation of the library.
Implementation mode:
If you define `SDEFL_IMPLEMENTATION` before including this file, it will
compile the implementation . Make sure that you only include
this file implementation in *one* C or C++ file to prevent collisions.
### Benchmark
| Compressor name | Compression| Decompress.| Compr. size | Ratio |
| ------------------------| -----------| -----------| ----------- | ----- |
| miniz 1.0 -1 | 122 MB/s | 208 MB/s | 48510028 | 48.51 |
| miniz 1.0 -6 | 27 MB/s | 260 MB/s | 36513697 | 36.51 |
| miniz 1.0 -9 | 23 MB/s | 261 MB/s | 36460101 | 36.46 |
| zlib 1.2.11 -1 | 72 MB/s | 307 MB/s | 42298774 | 42.30 |
| zlib 1.2.11 -6 | 24 MB/s | 313 MB/s | 36548921 | 36.55 |
| zlib 1.2.11 -9 | 20 MB/s | 314 MB/s | 36475792 | 36.48 |
| sdefl 1.0 -0 | 127 MB/s | 355 MB/s | 40004116 | 39.88 |
| sdefl 1.0 -1 | 111 MB/s | 413 MB/s | 38940674 | 38.82 |
| sdefl 1.0 -5 | 45 MB/s | 436 MB/s | 36577183 | 36.46 |
| sdefl 1.0 -7 | 38 MB/s | 432 MB/s | 36523781 | 36.41 |
| libdeflate 1.3 -1 | 147 MB/s | 667 MB/s | 39597378 | 39.60 |
| libdeflate 1.3 -6 | 69 MB/s | 689 MB/s | 36648318 | 36.65 |
| libdeflate 1.3 -9 | 13 MB/s | 672 MB/s | 35197141 | 35.20 |
| libdeflate 1.3 -12 | 8.13 MB/s | 670 MB/s | 35100568 | 35.10 |
### Compression
Results on the [Silesia compression corpus](http://sun.aei.polsl.pl/~sdeor/index.php?page=silesia):
| File | Original | `sdefl 0` | `sdefl 5` | `sdefl 7` |
| --------| -----------| -------------| ---------- | ------------|
| dickens | 10.192.446 | 4,260,187 | 3,845,261 | 3,833,657 |
| mozilla | 51.220.480 | 20,774,706 | 19,607,009 | 19,565,867 |
| mr | 9.970.564 | 3,860,531 | 3,673,460 | 3,665,627 |
| nci | 33.553.445 | 4,030,283 | 3,094,526 | 3,006,075 |
| ooffice | 6.152.192 | 3,320,063 | 3,186,373 | 3,183,815 |
| osdb | 10.085.684 | 3,919,646 | 3,649,510 | 3,649,477 |
| reymont | 6.627.202 | 2,263,378 | 1,857,588 | 1,827,237 |
| samba | 21.606.400 | 6,121,797 | 5,462,670 | 5,450,762 |
| sao | 7.251.944 | 5,612,421 | 5,485,380 | 5,481,765 |
| webster | 41.458.703 | 13,972,648 | 12,059,432 | 11,991,421 |
| xml | 5.345.280 | 886,620 | 674,009 | 662,141 |
| x-ray | 8.474.240 | 6,304,655 | 6,244,779 | 6,244,779 |
## License
```
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2020-2023 Micha Mettke
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
```
*/
#ifndef SDEFL_H_INCLUDED
#define SDEFL_H_INCLUDED
#ifdef __cplusplus
extern "C" {
#endif
#define SDEFL_MAX_OFF (1 << 15)
#define SDEFL_WIN_SIZ SDEFL_MAX_OFF
#define SDEFL_WIN_MSK (SDEFL_WIN_SIZ-1)
#define SDEFL_HASH_BITS 15
#define SDEFL_HASH_SIZ (1 << SDEFL_HASH_BITS)
#define SDEFL_HASH_MSK (SDEFL_HASH_SIZ-1)
#define SDEFL_MIN_MATCH 4
#define SDEFL_BLK_MAX (256*1024)
#define SDEFL_SEQ_SIZ ((SDEFL_BLK_MAX+2)/3)
#define SDEFL_SYM_MAX (288)
#define SDEFL_OFF_MAX (32)
#define SDEFL_PRE_MAX (19)
#define SDEFL_LVL_MIN 0
#define SDEFL_LVL_DEF 5
#define SDEFL_LVL_MAX 8
struct sdefl_freq {
unsigned lit[SDEFL_SYM_MAX];
unsigned off[SDEFL_OFF_MAX];
};
struct sdefl_code_words {
unsigned lit[SDEFL_SYM_MAX];
unsigned off[SDEFL_OFF_MAX];
};
struct sdefl_lens {
unsigned char lit[SDEFL_SYM_MAX];
unsigned char off[SDEFL_OFF_MAX];
};
struct sdefl_codes {
struct sdefl_code_words word;
struct sdefl_lens len;
};
struct sdefl_seqt {
int off, len;
};
struct sdefl {
int bits, bitcnt;
int tbl[SDEFL_HASH_SIZ];
int prv[SDEFL_WIN_SIZ];
int seq_cnt;
struct sdefl_seqt seq[SDEFL_SEQ_SIZ];
struct sdefl_freq freq;
struct sdefl_codes cod;
};
extern int sdefl_bound(int in_len);
extern int sdeflate(struct sdefl *s, void *o, const void *i, int n, int lvl);
extern int zsdeflate(struct sdefl *s, void *o, const void *i, int n, int lvl);
#ifdef __cplusplus
}
#endif
#endif /* SDEFL_H_INCLUDED */
#ifdef SDEFL_IMPLEMENTATION
#include <assert.h> /* assert */
#include <string.h> /* memcpy */
#include <limits.h> /* CHAR_BIT */
#ifdef __cplusplus
extern "C" {
#endif
#define SDEFL_NIL (-1)
#define SDEFL_MAX_MATCH 258
#define SDEFL_MAX_CODE_LEN (15)
#define SDEFL_SYM_BITS (10u)
#define SDEFL_SYM_MSK ((1u << SDEFL_SYM_BITS)-1u)
#define SDEFL_RAW_BLK_SIZE (65535)
#define SDEFL_LIT_LEN_CODES (14)
#define SDEFL_OFF_CODES (15)
#define SDEFL_PRE_CODES (7)
#define SDEFL_CNT_NUM(n) ((((n)+3u/4u)+3u)&~3u)
#define SDEFL_EOB (256)
#define sdefl_npow2(n) (1 << (sdefl_ilog2((n)-1) + 1))
#define sdefl_div_round_up(n,d) (((n)+((d)-1))/(d))
static int
sdefl_ilog2(int n) {
if (!n) return 0;
#ifdef _MSC_VER
unsigned long msbp = 0;
_BitScanReverse(&msbp, (unsigned long)n);
return (int)msbp;
#elif defined(__GNUC__) || defined(__clang__)
return (int)sizeof(unsigned long) * CHAR_BIT - 1 - __builtin_clzl((unsigned long)n);
#else
#define lt(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const char tbl[256] = {
0,0,1,1,2,2,2,2,3,3,3,3,3,3,3,3,lt(4), lt(5), lt(5), lt(6), lt(6), lt(6), lt(6),
lt(7), lt(7), lt(7), lt(7), lt(7), lt(7), lt(7), lt(7)};
int tt, t;
if ((tt = (n >> 16))) {
return (t = (tt >> 8)) ? 24 + tbl[t] : 16 + tbl[tt];
} else {
return (t = (n >> 8)) ? 8 + tbl[t] : tbl[n];
}
#undef lt
#endif
}
static unsigned
sdefl_uload32(const void *p) {
/* hopefully will be optimized to an unaligned read */
unsigned n = 0;
memcpy(&n, p, sizeof(n));
return n;
}
static unsigned
sdefl_hash32(const void *p) {
unsigned n = sdefl_uload32(p);
return (n * 0x9E377989) >> (32 - SDEFL_HASH_BITS);
}
static void
sdefl_put(unsigned char **dst, struct sdefl *s, int code, int bitcnt) {
s->bits |= (code << s->bitcnt);
s->bitcnt += bitcnt;
while (s->bitcnt >= 8) {
unsigned char *tar = *dst;
*tar = (unsigned char)(s->bits & 0xFF);
s->bits >>= 8;
s->bitcnt -= 8;
*dst = *dst + 1;
}
}
static void
sdefl_heap_sub(unsigned A[], unsigned len, unsigned sub) {
unsigned c, p = sub;
unsigned v = A[sub];
while ((c = p << 1) <= len) {
if (c < len && A[c + 1] > A[c]) c++;
if (v >= A[c]) break;
A[p] = A[c], p = c;
}
A[p] = v;
}
static void
sdefl_heap_array(unsigned *A, unsigned len) {
unsigned sub;
for (sub = len >> 1; sub >= 1; sub--)
sdefl_heap_sub(A, len, sub);
}
static void
sdefl_heap_sort(unsigned *A, unsigned n) {
A--;
sdefl_heap_array(A, n);
while (n >= 2) {
unsigned tmp = A[n];
A[n--] = A[1];
A[1] = tmp;
sdefl_heap_sub(A, n, 1);
}
}
static unsigned
sdefl_sort_sym(unsigned sym_cnt, unsigned *freqs,
unsigned char *lens, unsigned *sym_out) {
unsigned cnts[SDEFL_CNT_NUM(SDEFL_SYM_MAX)] = {0};
unsigned cnt_num = SDEFL_CNT_NUM(sym_cnt);
unsigned used_sym = 0;
unsigned sym, i;
for (sym = 0; sym < sym_cnt; sym++)
cnts[freqs[sym] < cnt_num-1 ? freqs[sym]: cnt_num-1]++;
for (i = 1; i < cnt_num; i++) {
unsigned cnt = cnts[i];
cnts[i] = used_sym;
used_sym += cnt;
}
for (sym = 0; sym < sym_cnt; sym++) {
unsigned freq = freqs[sym];
if (freq) {
unsigned idx = freq < cnt_num-1 ? freq : cnt_num-1;
sym_out[cnts[idx]++] = sym | (freq << SDEFL_SYM_BITS);
} else lens[sym] = 0;
}
sdefl_heap_sort(sym_out + cnts[cnt_num-2], cnts[cnt_num-1] - cnts[cnt_num-2]);
return used_sym;
}
static void
sdefl_build_tree(unsigned *A, unsigned sym_cnt) {
unsigned i = 0, b = 0, e = 0;
do {
unsigned m, n, freq_shift;
if (i != sym_cnt && (b == e || (A[i] >> SDEFL_SYM_BITS) <= (A[b] >> SDEFL_SYM_BITS)))
m = i++;
else m = b++;
if (i != sym_cnt && (b == e || (A[i] >> SDEFL_SYM_BITS) <= (A[b] >> SDEFL_SYM_BITS)))
n = i++;
else n = b++;
freq_shift = (A[m] & ~SDEFL_SYM_MSK) + (A[n] & ~SDEFL_SYM_MSK);
A[m] = (A[m] & SDEFL_SYM_MSK) | (e << SDEFL_SYM_BITS);
A[n] = (A[n] & SDEFL_SYM_MSK) | (e << SDEFL_SYM_BITS);
A[e] = (A[e] & SDEFL_SYM_MSK) | freq_shift;
} while (sym_cnt - ++e > 1);
}
static void
sdefl_gen_len_cnt(unsigned *A, unsigned root, unsigned *len_cnt,
unsigned max_code_len) {
int n;
unsigned i;
for (i = 0; i <= max_code_len; i++)
len_cnt[i] = 0;
len_cnt[1] = 2;
A[root] &= SDEFL_SYM_MSK;
for (n = (int)root - 1; n >= 0; n--) {
unsigned p = A[n] >> SDEFL_SYM_BITS;
unsigned pdepth = A[p] >> SDEFL_SYM_BITS;
unsigned depth = pdepth + 1;
unsigned len = depth;
A[n] = (A[n] & SDEFL_SYM_MSK) | (depth << SDEFL_SYM_BITS);
if (len >= max_code_len) {
len = max_code_len;
do len--; while (!len_cnt[len]);
}
len_cnt[len]--;
len_cnt[len+1] += 2;
}
}
static void
sdefl_gen_codes(unsigned *A, unsigned char *lens, const unsigned *len_cnt,
unsigned max_code_word_len, unsigned sym_cnt) {
unsigned i, sym, len, nxt[SDEFL_MAX_CODE_LEN + 1];
for (i = 0, len = max_code_word_len; len >= 1; len--) {
unsigned cnt = len_cnt[len];
while (cnt--) lens[A[i++] & SDEFL_SYM_MSK] = (unsigned char)len;
}
nxt[0] = nxt[1] = 0;
for (len = 2; len <= max_code_word_len; len++)
nxt[len] = (nxt[len-1] + len_cnt[len-1]) << 1;
for (sym = 0; sym < sym_cnt; sym++)
A[sym] = nxt[lens[sym]]++;
}
static unsigned
sdefl_rev(unsigned c, unsigned char n) {
c = ((c & 0x5555) << 1) | ((c & 0xAAAA) >> 1);
c = ((c & 0x3333) << 2) | ((c & 0xCCCC) >> 2);
c = ((c & 0x0F0F) << 4) | ((c & 0xF0F0) >> 4);
c = ((c & 0x00FF) << 8) | ((c & 0xFF00) >> 8);
return c >> (16-n);
}
static void
sdefl_huff(unsigned char *lens, unsigned *codes, unsigned *freqs,
unsigned num_syms, unsigned max_code_len) {
unsigned c, *A = codes;
unsigned len_cnt[SDEFL_MAX_CODE_LEN + 1];
unsigned used_syms = sdefl_sort_sym(num_syms, freqs, lens, A);
if (!used_syms) return;
if (used_syms == 1) {
unsigned s = A[0] & SDEFL_SYM_MSK;
unsigned i = s ? s : 1;
codes[0] = 0, lens[0] = 1;
codes[i] = 1, lens[i] = 1;
return;
}
sdefl_build_tree(A, used_syms);
sdefl_gen_len_cnt(A, used_syms-2, len_cnt, max_code_len);
sdefl_gen_codes(A, lens, len_cnt, max_code_len, num_syms);
for (c = 0; c < num_syms; c++) {
codes[c] = sdefl_rev(codes[c], lens[c]);
}
}
struct sdefl_symcnt {
int items;
int lit;
int off;
};
static void
sdefl_precode(struct sdefl_symcnt *cnt, unsigned *freqs, unsigned *items,
const unsigned char *litlen, const unsigned char *offlen) {
unsigned *at = items;
unsigned run_start = 0;
unsigned total = 0;
unsigned char lens[SDEFL_SYM_MAX + SDEFL_OFF_MAX];
for (cnt->lit = SDEFL_SYM_MAX; cnt->lit > 257; cnt->lit--)
if (litlen[cnt->lit - 1]) break;
for (cnt->off = SDEFL_OFF_MAX; cnt->off > 1; cnt->off--)
if (offlen[cnt->off - 1]) break;
total = (unsigned)(cnt->lit + cnt->off);
memcpy(lens, litlen, sizeof(unsigned char) * (size_t)cnt->lit);
memcpy(lens + cnt->lit, offlen, sizeof(unsigned char) * (size_t)cnt->off);
do {
unsigned len = lens[run_start];
unsigned run_end = run_start;
do run_end++; while (run_end != total && len == lens[run_end]);
if (!len) {
while ((run_end - run_start) >= 11) {
unsigned n = (run_end - run_start) - 11;
unsigned xbits = n < 0x7f ? n : 0x7f;
freqs[18]++;
*at++ = 18u | (xbits << 5u);
run_start += 11 + xbits;
}
if ((run_end - run_start) >= 3) {
unsigned n = (run_end - run_start) - 3;
unsigned xbits = n < 0x7 ? n : 0x7;
freqs[17]++;
*at++ = 17u | (xbits << 5u);
run_start += 3 + xbits;
}
} else if ((run_end - run_start) >= 4) {
freqs[len]++;
*at++ = len;
run_start++;
do {
unsigned xbits = (run_end - run_start) - 3;
xbits = xbits < 0x03 ? xbits : 0x03;
*at++ = 16 | (xbits << 5);
run_start += 3 + xbits;
freqs[16]++;
} while ((run_end - run_start) >= 3);
}
while (run_start != run_end) {
freqs[len]++;
*at++ = len;
run_start++;
}
} while (run_start != total);
cnt->items = (int)(at - items);
}
struct sdefl_match_codest {
int ls, lc;
int dc, dx;
};
static void
sdefl_match_codes(struct sdefl_match_codest *cod, int dist, int len) {
static const short dxmax[] = {0,6,12,24,48,96,192,384,768,1536,3072,6144,12288,24576};
static const unsigned char lslot[258+1] = {
0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 12,
12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 16, 16, 16, 16, 16,
16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18,
18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 20,
20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21,
21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22,
22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26,
26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
27, 27, 28
};
assert(len <= 258);
assert(dist <= 32768);
cod->ls = lslot[len];
cod->lc = 257 + cod->ls;
assert(cod->lc <= 285);
cod->dx = sdefl_ilog2(sdefl_npow2(dist) >> 2);
cod->dc = cod->dx ? ((cod->dx + 1) << 1) + (dist > dxmax[cod->dx]) : dist-1;
}
enum sdefl_blk_type {
SDEFL_BLK_UCOMPR,
SDEFL_BLK_DYN
};
static enum sdefl_blk_type
sdefl_blk_type(const struct sdefl *s, int blk_len, int pre_item_len,
const unsigned *pre_freq, const unsigned char *pre_len) {
static const unsigned char x_pre_bits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7};
static const unsigned char x_len_bits[] = {0,0,0,0,0,0,0,0, 1,1,1,1,2,2,2,2,
3,3,3,3,4,4,4,4, 5,5,5,5,0};
static const unsigned char x_off_bits[] = {0,0,0,0,1,1,2,2, 3,3,4,4,5,5,6,6,
7,7,8,8,9,9,10,10, 11,11,12,12,13,13};
int dyn_cost = 0;
int fix_cost = 0;
int sym = 0;
dyn_cost += 5 + 5 + 4 + (3 * pre_item_len);
for (sym = 0; sym < SDEFL_PRE_MAX; sym++)
dyn_cost += pre_freq[sym] * (x_pre_bits[sym] + pre_len[sym]);
for (sym = 0; sym < 256; sym++)
dyn_cost += s->freq.lit[sym] * s->cod.len.lit[sym];
dyn_cost += s->cod.len.lit[SDEFL_EOB];
for (sym = 257; sym < 286; sym++)
dyn_cost += s->freq.lit[sym] * (x_len_bits[sym - 257] + s->cod.len.lit[sym]);
for (sym = 0; sym < 30; sym++)
dyn_cost += s->freq.off[sym] * (x_off_bits[sym] + s->cod.len.off[sym]);
fix_cost += 8*(5 * sdefl_div_round_up(blk_len, SDEFL_RAW_BLK_SIZE) + blk_len + 1 + 2);
return (dyn_cost < fix_cost) ? SDEFL_BLK_DYN : SDEFL_BLK_UCOMPR;
}
static void
sdefl_put16(unsigned char **dst, unsigned short x) {
unsigned char *val = *dst;
val[0] = (unsigned char)(x & 0xff);
val[1] = (unsigned char)(x >> 8);
*dst = val + 2;
}
static void
sdefl_match(unsigned char **dst, struct sdefl *s, int dist, int len) {
static const char lxn[] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0};
static const short lmin[] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,
51,59,67,83,99,115,131,163,195,227,258};
static const short dmin[] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,
385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577};
struct sdefl_match_codest cod;
sdefl_match_codes(&cod, dist, len);
sdefl_put(dst, s, (int)s->cod.word.lit[cod.lc], s->cod.len.lit[cod.lc]);
sdefl_put(dst, s, len - lmin[cod.ls], lxn[cod.ls]);
sdefl_put(dst, s, (int)s->cod.word.off[cod.dc], s->cod.len.off[cod.dc]);
sdefl_put(dst, s, dist - dmin[cod.dc], cod.dx);
}
static void
sdefl_flush(unsigned char **dst, struct sdefl *s, int is_last,
const unsigned char *in, int blk_begin, int blk_end) {
int blk_len = blk_end - blk_begin;
int j, i = 0, item_cnt = 0;
struct sdefl_symcnt symcnt = {0};
unsigned codes[SDEFL_PRE_MAX];
unsigned char lens[SDEFL_PRE_MAX];
unsigned freqs[SDEFL_PRE_MAX] = {0};
unsigned items[SDEFL_SYM_MAX + SDEFL_OFF_MAX];
static const unsigned char perm[SDEFL_PRE_MAX] = {16,17,18,0,8,7,9,6,10,5,11,
4,12,3,13,2,14,1,15};
/* calculate huffman codes */
s->freq.lit[SDEFL_EOB]++;
sdefl_huff(s->cod.len.lit, s->cod.word.lit, s->freq.lit, SDEFL_SYM_MAX, SDEFL_LIT_LEN_CODES);
sdefl_huff(s->cod.len.off, s->cod.word.off, s->freq.off, SDEFL_OFF_MAX, SDEFL_OFF_CODES);
sdefl_precode(&symcnt, freqs, items, s->cod.len.lit, s->cod.len.off);
sdefl_huff(lens, codes, freqs, SDEFL_PRE_MAX, SDEFL_PRE_CODES);
for (item_cnt = SDEFL_PRE_MAX; item_cnt > 4; item_cnt--) {
if (lens[perm[item_cnt - 1]]){
break;
}
}
/* write block */
switch (sdefl_blk_type(s, blk_len, item_cnt, freqs, lens)) {
case SDEFL_BLK_UCOMPR: {
/* uncompressed blocks */
int n = sdefl_div_round_up(blk_len, SDEFL_RAW_BLK_SIZE);
for (i = 0; i < n; ++i) {
int fin = is_last && (i + 1 == n);
int amount = blk_len < SDEFL_RAW_BLK_SIZE ? blk_len : SDEFL_RAW_BLK_SIZE;
sdefl_put(dst, s, !!fin, 1); /* block */
sdefl_put(dst, s, 0x00, 2); /* stored block */
if (s->bitcnt) {
sdefl_put(dst, s, 0x00, 8 - s->bitcnt);
}
assert(s->bitcnt == 0);
sdefl_put16(dst, (unsigned short)amount);
sdefl_put16(dst, ~(unsigned short)amount);
memcpy(*dst, in + blk_begin + i * SDEFL_RAW_BLK_SIZE, amount);
*dst = *dst + amount;
blk_len -= amount;
}
} break;
case SDEFL_BLK_DYN: {
/* dynamic huffman block */
sdefl_put(dst, s, !!is_last, 1); /* block */
sdefl_put(dst, s, 0x02, 2); /* dynamic huffman */
sdefl_put(dst, s, symcnt.lit - 257, 5);
sdefl_put(dst, s, symcnt.off - 1, 5);
sdefl_put(dst, s, item_cnt - 4, 4);
for (i = 0; i < item_cnt; ++i) {
sdefl_put(dst, s, lens[perm[i]], 3);
}
for (i = 0; i < symcnt.items; ++i) {
unsigned sym = items[i] & 0x1F;
sdefl_put(dst, s, (int)codes[sym], lens[sym]);
if (sym < 16) continue;
if (sym == 16) sdefl_put(dst, s, items[i] >> 5, 2);
else if(sym == 17) sdefl_put(dst, s, items[i] >> 5, 3);
else sdefl_put(dst, s, items[i] >> 5, 7);
}
/* block sequences */
for (i = 0; i < s->seq_cnt; ++i) {
if (s->seq[i].off >= 0) {
for (j = 0; j < s->seq[i].len; ++j) {
int c = in[s->seq[i].off + j];
sdefl_put(dst, s, (int)s->cod.word.lit[c], s->cod.len.lit[c]);
}
} else {
sdefl_match(dst, s, -s->seq[i].off, s->seq[i].len);
}
}
sdefl_put(dst, s, (int)(s)->cod.word.lit[SDEFL_EOB], (s)->cod.len.lit[SDEFL_EOB]);
} break;}
memset(&s->freq, 0, sizeof(s->freq));
s->seq_cnt = 0;
}
static void
sdefl_seq(struct sdefl *s, int off, int len) {
assert(s->seq_cnt + 2 < SDEFL_SEQ_SIZ);
s->seq[s->seq_cnt].off = off;
s->seq[s->seq_cnt].len = len;
s->seq_cnt++;
}
static void
sdefl_reg_match(struct sdefl *s, int off, int len) {
struct sdefl_match_codest cod;
sdefl_match_codes(&cod, off, len);
assert(cod.lc < SDEFL_SYM_MAX);
assert(cod.dc < SDEFL_OFF_MAX);
s->freq.lit[cod.lc]++;
s->freq.off[cod.dc]++;
}
struct sdefl_match {
int off;
int len;
};
static void
sdefl_fnd(struct sdefl_match *m, const struct sdefl *s, int chain_len,
int max_match, const unsigned char *in, int p, int e) {
int i = s->tbl[sdefl_hash32(in + p)];
int limit = ((p - SDEFL_WIN_SIZ) < SDEFL_NIL) ? SDEFL_NIL : (p-SDEFL_WIN_SIZ);
(void)e; /* used in assertions */
assert(p < e);
assert(p + max_match <= e);
while (i > limit) {
assert(i + m->len < e);
assert(p + m->len < e);
assert(i + SDEFL_MIN_MATCH < e);
assert(p + SDEFL_MIN_MATCH < e);
if (in[i + m->len] == in[p + m->len] &&
(sdefl_uload32(&in[i]) == sdefl_uload32(&in[p]))) {
int n = SDEFL_MIN_MATCH;
while (n < max_match && in[i + n] == in[p + n]) {
assert(i + n < e);
assert(p + n < e);
n++;
}
if (n > m->len) {
m->len = n, m->off = p - i;
if (n == max_match)
break;
}
}
if (!(--chain_len)) break;
i = s->prv[i & SDEFL_WIN_MSK];
}
}
static int
sdefl_compr(struct sdefl *s, unsigned char *out, const unsigned char *in,
int in_len, int lvl) {
unsigned char *q = out;
static const unsigned char pref[] = {8,10,14,24,30,48,65,96,130};
int max_chain = (lvl < 8) ? (1 << (lvl + 1)): (1 << 13);
int n, i = 0, litlen = 0;
for (n = 0; n < SDEFL_HASH_SIZ; ++n) {
s->tbl[n] = SDEFL_NIL;
}
do {int blk_begin = i;
int blk_end = ((i + SDEFL_BLK_MAX) < in_len) ? (i + SDEFL_BLK_MAX) : in_len;
while (i < blk_end) {
struct sdefl_match m = {0};
int left = blk_end - i;
int max_match = (left > SDEFL_MAX_MATCH) ? SDEFL_MAX_MATCH : left;
int nice_match = pref[lvl] < max_match ? pref[lvl] : max_match;
int run = 1, inc = 1, run_inc = 0;
if (max_match > SDEFL_MIN_MATCH) {
sdefl_fnd(&m, s, max_chain, max_match, in, i, in_len);
}
if (lvl >= 5 && m.len >= SDEFL_MIN_MATCH && m.len + 1 < nice_match){
struct sdefl_match m2 = {0};
sdefl_fnd(&m2, s, max_chain, m.len + 1, in, i + 1, in_len);
m.len = (m2.len > m.len) ? 0 : m.len;
}
if (m.len >= SDEFL_MIN_MATCH) {
if (litlen) {
sdefl_seq(s, i - litlen, litlen);
litlen = 0;
}
sdefl_seq(s, -m.off, m.len);
sdefl_reg_match(s, m.off, m.len);
if (lvl < 2 && m.len >= nice_match) {
inc = m.len;
} else {
run = m.len;
}
} else {
s->freq.lit[in[i]]++;
litlen++;
}
run_inc = run * inc;
if (in_len - (i + run_inc) > SDEFL_MIN_MATCH) {
while (run-- > 0) {
unsigned h = sdefl_hash32(&in[i]);
s->prv[i&SDEFL_WIN_MSK] = s->tbl[h];
s->tbl[h] = i, i += inc;
assert(i <= blk_end);
}
} else {
i += run_inc;
assert(i <= blk_end);
}
}
if (litlen) {
sdefl_seq(s, i - litlen, litlen);
litlen = 0;
}
sdefl_flush(&q, s, blk_end == in_len, in, blk_begin, blk_end);
} while (i < in_len);
if (s->bitcnt) {
sdefl_put(&q, s, 0x00, 8 - s->bitcnt);
}
assert(s->bitcnt == 0);
return (int)(q - out);
}
extern int
sdeflate(struct sdefl *s, void *out, const void *in, int n, int lvl) {
s->bits = s->bitcnt = 0;
return sdefl_compr(s, (unsigned char*)out, (const unsigned char*)in, n, lvl);
}
static unsigned
sdefl_adler32(unsigned adler32, const unsigned char *in, int in_len) {
#define SDEFL_ADLER_INIT (1)
const unsigned ADLER_MOD = 65521;
unsigned s1 = adler32 & 0xffff;
unsigned s2 = adler32 >> 16;
unsigned blk_len, i;
blk_len = in_len % 5552;
while (in_len) {
for (i = 0; i + 7 < blk_len; i += 8) {
s1 += in[0]; s2 += s1;
s1 += in[1]; s2 += s1;
s1 += in[2]; s2 += s1;
s1 += in[3]; s2 += s1;
s1 += in[4]; s2 += s1;
s1 += in[5]; s2 += s1;
s1 += in[6]; s2 += s1;
s1 += in[7]; s2 += s1;
in += 8;
}
for (; i < blk_len; ++i) {
s1 += *in++, s2 += s1;
}
s1 %= ADLER_MOD;
s2 %= ADLER_MOD;
in_len -= blk_len;
blk_len = 5552;
}
return (unsigned)(s2 << 16) + (unsigned)s1;
}
extern int
zsdeflate(struct sdefl *s, void *out, const void *in, int n, int lvl) {
int p = 0;
unsigned a = 0;
unsigned char *q = (unsigned char*)out;
s->bits = s->bitcnt = 0;
sdefl_put(&q, s, 0x78, 8); /* deflate, 32k window */
sdefl_put(&q, s, 0x01, 8); /* fast compression */
q += sdefl_compr(s, q, (const unsigned char*)in, n, lvl);
/* append adler checksum */
a = sdefl_adler32(SDEFL_ADLER_INIT, (const unsigned char*)in, n);
for (p = 0; p < 4; ++p) {
sdefl_put(&q, s, (a >> 24) & 0xFF, 8);
a <<= 8;
}
return (int)(q - (unsigned char*)out);
}
extern int
sdefl_bound(int len) {
int max_blocks = 1 + sdefl_div_round_up(len, SDEFL_RAW_BLK_SIZE);
int bound = 5 * max_blocks + len + 1 + 4 + 8 + 3;
return bound;
}
#ifdef __cplusplus
}
#endif
#endif /* SDEFL_IMPLEMENTATION */
+133
View File
@@ -0,0 +1,133 @@
# 📦 gltfpack
gltfpack is a tool that can automatically optimize glTF files to reduce the download size and improve loading and rendering speed.
## Installation
You can download a pre-built binary for gltfpack on [Releases page](https://github.com/zeux/meshoptimizer/releases), or install [npm package](https://www.npmjs.com/package/gltfpack). Native binaries are recommended over npm since they can work with larger files, run faster, and support texture compression.
## Usage
To convert a glTF file using gltfpack, run the command-line binary like this on an input `.gltf`/`.glb`/`.obj` file (run it without arguments for a list of options):
```
gltfpack -i scene.gltf -o scene.glb
```
gltfpack substantially changes the glTF data by optimizing the meshes for vertex fetch and transform cache, quantizing the geometry to reduce the memory consumption and size, merging meshes to reduce the draw call count, quantizing and resampling animations to reduce animation size and simplify playback, and pruning the node tree by removing or collapsing redundant nodes. It will also simplify the meshes when requested to do so.
By default gltfpack outputs regular `.glb`/`.gltf` files that have been optimized for GPU consumption using various cache optimizers and quantization. These files can be loaded by GLTF loaders that support `KHR_mesh_quantization` extension such as [three.js](https://threejs.org/) (r111+) and [Babylon.js](https://www.babylonjs.com/) (4.1+).
When using `-c` option, gltfpack outputs compressed `.glb`/`.gltf` files that use meshoptimizer codecs to reduce the download size further. Loading these files requires extending GLTF loaders with support for [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) extension; three.js supports it in r122+ (requires calling `GLTFLoader.setMeshoptDecoder`), Babylon.js supports it in 5.0+ without further setup.
For better compression, you can use `-cc` option which applies additional compression; additionally make sure that your content delivery method is configured to use deflate (gzip) - meshoptimizer codecs are designed to produce output that can be compressed further with general purpose compressors. For maximum compression, consider using [KHR_meshopt_compression](https://github.com/KhronosGroup/glTF/pull/2517) extension with a higher compression level (`-cz` option).
gltfpack can also compress textures using Basis Universal format stored in a KTX2 container (`-tc` flag, requires support for `KHR_texture_basisu`) or using WebP format (`-tw` flag, requires support for `EXT_texture_webp`).
When working with glTF files that contain point clouds, gltfpack automatically processes the point cloud data to reduce the download size to the extent possible. In addition to aforementioned compression options (either `-c` or `-cc` are recommended), gltfpack can also prune point clouds to provide a more uniform density when `-si` option is used.
## Decompression
When using compressed files, [js/meshopt_decoder.mjs](https://github.com/zeux/meshoptimizer/blob/master/js/meshopt_decoder.mjs) needs to be loaded to provide the WebAssembly decoder module like this:
```js
import { MeshoptDecoder } from './meshopt_decoder.mjs';
...
var loader = new GLTFLoader();
loader.setMeshoptDecoder(MeshoptDecoder);
loader.load('pirate.glb', function (gltf) { scene.add(gltf.scene); });
```
When using Three.js, this module can be imported from three.js repository from `examples/jsm/libs/meshopt_decoder.module.js`.
Note that `meshopt_decoder` assumes that WebAssembly is supported. This is the case for all modern browsers; if support for legacy browsers such as Internet Explorer 11 is desired, it's recommended to use `-cf` flag when creating the glTF content. This will create and load fallback uncompressed buffers, but only on browsers that don't support WebAssembly.
## Options
By default gltfpack makes certain assumptions when optimizing the scenes, for example meshes that belong to nodes that aren't animated can be merged together, and has some defaults that represent a tradeoff between precision and size that are picked to fit most use cases. However, in some cases the resulting `.gltf` file needs to retain some way for the application to manipulate individual scene elements, and in other cases precision or size are more important to optimize for. gltfpack has a rich set of command line options to control various aspects of its behavior, with the full list available via `gltfpack -h`.
The following settings are frequently used to reduce the resulting data size:
* `-cc`: produce compressed gltf/glb files (requires `EXT_meshopt_compression`)
* `-tc`: convert all textures to KTX2 with BasisU supercompression (requires `KHR_texture_basisu` and may require `-tp` flag for compatibility with WebGL 1)
* `-tw`: convert all textures to WebP (requires `EXT_texture_webp`)
* `-mi`: use mesh instancing when serializing references to the same meshes (requires `EXT_mesh_gpu_instancing`)
* `-si R`: simplify meshes targeting triangle/point count ratio R (default: 1; R should be between 0 and 1)
The following settings are frequently used to restrict some optimizations:
* `-kn`: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally
* `-km`: keep named materials and disable named material merging
* `-ke`: keep extras data
* `-vpf`: use floating-point position quantization instead of the default fixed-point (this results in larger position data, but does not insert new nodes with dequantization transforms; when using this option, `-cc` is recommended as well)
## Extensions
gltfpack supports most Khronos extensions and some multi-vendor extensions in the input scenes, with newer extensions added regularly. The following extensions are fully supported:
- KHR_lights_punctual
- KHR_materials_anisotropy
- KHR_materials_clearcoat
- KHR_materials_diffuse_transmission
- KHR_materials_dispersion
- KHR_materials_emissive_strength
- KHR_materials_ior
- KHR_materials_iridescence
- KHR_materials_pbrSpecularGlossiness
- KHR_materials_sheen
- KHR_materials_specular
- KHR_materials_transmission
- KHR_materials_unlit
- KHR_materials_variants
- KHR_materials_volume
- KHR_mesh_quantization
- KHR_meshopt_compression
- KHR_texture_basisu
- KHR_texture_transform
- EXT_mesh_gpu_instancing
- EXT_meshopt_compression
- EXT_texture_webp
Even if the source file does not use extensions, gltfpack may use some extensions in the output file either by default or when certain options are used:
- KHR_mesh_quantization (used by default unless disabled via `-noq`)
- KHR_meshopt_compression (used when requested via `-ce khr` or `-cz`)
- KHR_texture_transform (used by default when textures are present, unless disabled via `-noq` or `-vtf`)
- KHR_texture_basisu (used when requested via `-tc` or `-tu`)
- EXT_meshopt_compression (used when requested via `-c` or `-cc`)
- EXT_mesh_gpu_instancing (used when requested via `-mi`)
- EXT_texture_webp (used when requested via `-tw`)
gltfpack does not support vendor-specific extensions or custom extensions, including ones defined in [Khronos glTF repository](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor). Unknown extension nodes are discarded from the output.
Some Khronos extensions, notably Draco compression and extensions for interactive scenes (interactivity, animation pointer, node selectability/hoverability/visibility), are not supported.
## Custom data
glTF files may contain custom application-specific data stored outside of custom extensions. gltfpack has limited support for preserving this data.
Unknown vertex or instance attributes are generally discarded. While it would be possible to preserve them in theory, many scene transformations and analyses require a full understanding of the attribute semantics - some attributes need to be transformed with the mesh transformation matrix, some need to be adjusted when meshes are merged, some influence how meshes are rendered, etc. However, gltfpack supports some specific attributes that are commonly used in the ecosystem:
- Vertex attributes that store integer IDs with names `_ID`, `_BATCHID` or `_FEATURE_ID_n` are preserved as-is. These can be used to identify objects.
- Instance attribute `_COLOR_0` is preserved as-is. This can be used to provide per-instance color tinting.
Additional application-specific data can be stored via the glTF `extras` property. While extras (outside of `asset`) are discarded by default, using the `-ke` option preserves extras in materials, nodes, primitives and scenes.
In addition to `asset` extras, gltfpack unconditionally preserves morph target names specified via the `targetNames` property inside `extras` on meshes.
## Building
gltfpack can be built from source using CMake or Make. To build a full version of gltfpack that supports texture compression, CMake configuration needs to specify the path to https://github.com/zeux/basis_universal fork (branch gltfpack) via `MESHOPT_GLTFPACK_BASISU_PATH` variable, as well as libwebp path via `MESHOPT_GLTFPACK_LIBWEBP_PATH` variable:
```
git clone -b gltfpack https://github.com/zeux/basis_universal
git clone https://github.com/webmproject/libwebp
cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_GLTFPACK_BASISU_PATH=basis_universal -DMESHOPT_GLTFPACK_LIBWEBP_PATH=libwebp -DCMAKE_BUILD_TYPE=Release
cmake --build . --target gltfpack --config Release
```
## License
gltfpack is available to anybody free of charge, under the terms of MIT License (see LICENSE.md).
+356
View File
@@ -0,0 +1,356 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#include "gltfpack.h"
#include <algorithm>
#include <float.h>
#include <math.h>
#include <string.h>
static float getScaleDelta(float l, float r)
{
float d = fabsf(l - r);
float m = std::max(fabsf(l), fabsf(r));
return m == 0.f ? 0.f : d / m;
}
static float getDelta(const Attr& l, const Attr& r, cgltf_animation_path_type type)
{
switch (type)
{
case cgltf_animation_path_type_translation:
return std::max(std::max(fabsf(l.f[0] - r.f[0]), fabsf(l.f[1] - r.f[1])), fabsf(l.f[2] - r.f[2]));
case cgltf_animation_path_type_rotation:
return 2 * acosf(std::min(1.f, fabsf(l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3])));
case cgltf_animation_path_type_scale:
return std::max(std::max(getScaleDelta(l.f[0], r.f[0]), getScaleDelta(l.f[1], r.f[1])), getScaleDelta(l.f[2], r.f[2]));
case cgltf_animation_path_type_weights:
return fabsf(l.f[0] - r.f[0]);
default:
assert(!"Unknown animation path");
return 0;
}
}
static float getDeltaTolerance(cgltf_animation_path_type type)
{
switch (type)
{
case cgltf_animation_path_type_translation:
return 0.0001f; // 0.1mm linear
case cgltf_animation_path_type_rotation:
return 0.1f * (3.1415926f / 180.f); // 0.1 degrees
case cgltf_animation_path_type_scale:
return 0.001f; // 0.1% ratio
case cgltf_animation_path_type_weights:
return 0.001f; // 0.1% linear
default:
assert(!"Unknown animation path");
return 0;
}
}
static Attr interpolateLinear(const Attr& l, const Attr& r, float t, cgltf_animation_path_type type)
{
if (type == cgltf_animation_path_type_rotation)
{
// Approximating slerp, https://zeux.io/2015/07/23/approximating-slerp/
// We also handle quaternion double-cover
float ca = l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3];
float d = fabsf(ca);
float A = 1.0904f + d * (-3.2452f + d * (3.55645f - d * 1.43519f));
float B = 0.848013f + d * (-1.06021f + d * 0.215638f);
float k = A * (t - 0.5f) * (t - 0.5f) + B;
float ot = t + t * (t - 0.5f) * (t - 1) * k;
float t0 = 1 - ot;
float t1 = ca > 0 ? ot : -ot;
Attr lerp = {{
l.f[0] * t0 + r.f[0] * t1,
l.f[1] * t0 + r.f[1] * t1,
l.f[2] * t0 + r.f[2] * t1,
l.f[3] * t0 + r.f[3] * t1,
}};
float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]);
if (len > 0.f)
{
lerp.f[0] /= len;
lerp.f[1] /= len;
lerp.f[2] /= len;
lerp.f[3] /= len;
}
return lerp;
}
else
{
Attr lerp = {{
l.f[0] * (1 - t) + r.f[0] * t,
l.f[1] * (1 - t) + r.f[1] * t,
l.f[2] * (1 - t) + r.f[2] * t,
l.f[3] * (1 - t) + r.f[3] * t,
}};
return lerp;
}
}
static Attr interpolateHermite(const Attr& v0, const Attr& t0, const Attr& v1, const Attr& t1, float t, float dt, cgltf_animation_path_type type)
{
float s0 = 1 + t * t * (2 * t - 3);
float s1 = t + t * t * (t - 2);
float s2 = 1 - s0;
float s3 = t * t * (t - 1);
float ts1 = dt * s1;
float ts3 = dt * s3;
Attr lerp = {{
s0 * v0.f[0] + ts1 * t0.f[0] + s2 * v1.f[0] + ts3 * t1.f[0],
s0 * v0.f[1] + ts1 * t0.f[1] + s2 * v1.f[1] + ts3 * t1.f[1],
s0 * v0.f[2] + ts1 * t0.f[2] + s2 * v1.f[2] + ts3 * t1.f[2],
s0 * v0.f[3] + ts1 * t0.f[3] + s2 * v1.f[3] + ts3 * t1.f[3],
}};
if (type == cgltf_animation_path_type_rotation)
{
float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]);
if (len > 0.f)
{
lerp.f[0] /= len;
lerp.f[1] /= len;
lerp.f[2] /= len;
lerp.f[3] /= len;
}
}
return lerp;
}
static void resampleKeyframes(std::vector<Attr>& data, const std::vector<float>& input, const std::vector<Attr>& output, cgltf_animation_path_type type, cgltf_interpolation_type interpolation, size_t components, int frames, float mint, int freq)
{
size_t cursor = 0;
for (int i = 0; i < frames; ++i)
{
float time = mint + float(i) / freq;
while (cursor + 1 < input.size())
{
float next_time = input[cursor + 1];
if (next_time > time)
break;
cursor++;
}
if (cursor + 1 < input.size())
{
float cursor_time = input[cursor + 0];
float next_time = input[cursor + 1];
float range = next_time - cursor_time;
float inv_range = (range == 0.f) ? 0.f : 1.f / (next_time - cursor_time);
float t = std::max(0.f, std::min(1.f, (time - cursor_time) * inv_range));
for (size_t j = 0; j < components; ++j)
{
switch (interpolation)
{
case cgltf_interpolation_type_linear:
{
const Attr& v0 = output[(cursor + 0) * components + j];
const Attr& v1 = output[(cursor + 1) * components + j];
data.push_back(interpolateLinear(v0, v1, t, type));
}
break;
case cgltf_interpolation_type_step:
{
const Attr& v = output[cursor * components + j];
data.push_back(v);
}
break;
case cgltf_interpolation_type_cubic_spline:
{
const Attr& v0 = output[(cursor * 3 + 1) * components + j];
const Attr& b0 = output[(cursor * 3 + 2) * components + j];
const Attr& a1 = output[(cursor * 3 + 3) * components + j];
const Attr& v1 = output[(cursor * 3 + 4) * components + j];
data.push_back(interpolateHermite(v0, b0, v1, a1, t, range, type));
}
break;
default:
assert(!"Unknown interpolation type");
}
}
}
else
{
size_t offset = (interpolation == cgltf_interpolation_type_cubic_spline) ? cursor * 3 + 1 : cursor;
for (size_t j = 0; j < components; ++j)
{
const Attr& v = output[offset * components + j];
data.push_back(v);
}
}
}
}
static float getMaxDelta(const std::vector<Attr>& data, cgltf_animation_path_type type, const Attr* value, size_t components)
{
assert(data.size() % components == 0);
float result = 0;
for (size_t i = 0; i < data.size(); i += components)
{
for (size_t j = 0; j < components; ++j)
{
float delta = getDelta(value[j], data[i + j], type);
result = (result < delta) ? delta : result;
}
}
return result;
}
static void getBaseTransform(Attr* result, size_t components, cgltf_animation_path_type type, cgltf_node* node)
{
switch (type)
{
case cgltf_animation_path_type_translation:
memcpy(result->f, node->translation, 3 * sizeof(float));
break;
case cgltf_animation_path_type_rotation:
memcpy(result->f, node->rotation, 4 * sizeof(float));
break;
case cgltf_animation_path_type_scale:
memcpy(result->f, node->scale, 3 * sizeof(float));
break;
case cgltf_animation_path_type_weights:
if (node->weights_count)
{
assert(node->weights_count == components);
for (size_t k = 0; k < components; ++k)
result[k].f[0] = node->weights[k];
}
else if (node->mesh && node->mesh->weights_count)
{
assert(node->mesh->weights_count == components);
for (size_t k = 0; k < components; ++k)
result[k].f[0] = node->mesh->weights[k];
}
break;
default:
assert(!"Unknown animation path");
}
}
static float getWorldScale(cgltf_node* node)
{
float transform[16];
cgltf_node_transform_world(node, transform);
// 3x3 determinant computes scale^3
float a0 = transform[5] * transform[10] - transform[6] * transform[9];
float a1 = transform[4] * transform[10] - transform[6] * transform[8];
float a2 = transform[4] * transform[9] - transform[5] * transform[8];
float det = transform[0] * a0 - transform[1] * a1 + transform[2] * a2;
return powf(fabsf(det), 1.f / 3.f);
}
void processAnimation(Animation& animation, const Settings& settings)
{
float mint = FLT_MAX, maxt = 0;
for (size_t i = 0; i < animation.tracks.size(); ++i)
{
const Track& track = animation.tracks[i];
assert(!track.time.empty());
mint = std::min(mint, track.time.front());
maxt = std::max(maxt, track.time.back());
}
animation.start = mint = std::min(mint, maxt);
if (settings.anim_freq)
{
// round the number of frames to nearest but favor the "up" direction
// this means that at 100 Hz resampling, we will try to preserve the last frame <10ms
// but if the last frame is <2ms we favor just removing this data
int frames = 1 + int((maxt - mint) * settings.anim_freq + 0.8f);
animation.frames = frames;
}
std::vector<Attr> base;
for (size_t i = 0; i < animation.tracks.size(); ++i)
{
Track& track = animation.tracks[i];
if (settings.anim_freq)
{
std::vector<Attr> result;
resampleKeyframes(result, track.time, track.data, track.path, track.interpolation, track.components, animation.frames, animation.start, settings.anim_freq);
track.time.clear();
track.data.swap(result);
track.interpolation = track.interpolation == cgltf_interpolation_type_cubic_spline ? cgltf_interpolation_type_linear : track.interpolation;
}
// getMaxDelta assumes linear/step interpolation for now
if (track.interpolation == cgltf_interpolation_type_cubic_spline)
continue;
float tolerance = getDeltaTolerance(track.path);
// translation tracks use world space tolerance; in the future, we should compute all errors as linear using hierarchy
if (track.node && track.node->parent && track.path == cgltf_animation_path_type_translation)
{
float scale = getWorldScale(track.node->parent);
tolerance /= scale == 0.f ? 1.f : scale;
}
float deviation = getMaxDelta(track.data, track.path, &track.data[0], track.components);
if (deviation <= tolerance)
{
// track is constant (equal to first keyframe), we only need the first keyframe
track.constant = true;
track.time.clear();
track.data.resize(track.components);
// track.dummy is true iff track redundantly sets up the value to be equal to default node transform
base.resize(track.components);
getBaseTransform(&base[0], track.components, track.path, track.node);
track.dummy = getMaxDelta(track.data, track.path, &base[0], track.components) <= tolerance;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
// This file is part of gltfpack and is distributed under the terms of MIT License.
import { pack } from './library.js';
import fs from 'fs';
var args = process.argv.slice(2);
var iface = {
read: function (path) {
return fs.readFileSync(path);
},
write: function (path, data) {
fs.writeFileSync(path, data);
},
};
pack(args, iface)
.then(function (log) {
process.stdout.write(log);
process.exit(0);
})
.catch(function (err) {
process.stderr.write(err.message);
process.exit(1);
});
+214
View File
@@ -0,0 +1,214 @@
// This file is part of gltfpack; see gltfpack.h for version/license details
#ifdef WITH_BASISU
#include "gltfpack.h"
#define BASISU_NO_ITERATOR_DEBUG_LEVEL
#ifdef __clang__
#pragma GCC diagnostic ignored "-Wunknown-warning-option"
#pragma GCC diagnostic ignored "-Warray-bounds"
#pragma GCC diagnostic ignored "-Wc++17-extensions"
#pragma GCC diagnostic ignored "-Wdeprecated-builtins"
#endif
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wunused-local-typedefs"
#pragma GCC diagnostic ignored "-Wunused-value"
#endif
#if defined(__GNUC__) && __GNUC__ >= 12
#pragma GCC diagnostic ignored "-Wc++17-extensions"
#endif
#include "encoder/basisu_comp.h"
struct BasisSettings
{
int etc1s_l;
int etc1s_q;
int uastc_l;
float uastc_q;
};
static const BasisSettings kBasisSettings[10] = {
{1, 1, 0, 4.f},
{1, 32, 0, 3.f},
{1, 64, 1, 2.f},
{1, 96, 1, 1.5f},
{1, 128, 1, 1.f}, // quality arguments aligned with basisu defaults
{1, 150, 1, 0.8f},
{1, 170, 1, 0.6f},
{1, 192, 1, 0.4f}, // gltfpack defaults
{1, 224, 2, 0.2f},
{1, 255, 2, 0.f},
};
static void fillParams(basisu::basis_compressor_params& params, const char* input, const char* output, bool uastc, int width, int height, const BasisSettings& bs, const ImageInfo& info, const Settings& settings)
{
if (uastc)
{
static const uint32_t s_level_flags[basisu::TOTAL_PACK_UASTC_LEVELS] = {basisu::cPackUASTCLevelFastest, basisu::cPackUASTCLevelFaster, basisu::cPackUASTCLevelDefault, basisu::cPackUASTCLevelSlower, basisu::cPackUASTCLevelVerySlow};
params.m_uastc = true;
#if BASISU_LIB_VERSION >= 160
params.m_pack_uastc_ldr_4x4_flags &= ~basisu::cPackUASTCLevelMask;
params.m_pack_uastc_ldr_4x4_flags |= s_level_flags[bs.uastc_l];
params.m_rdo_uastc_ldr_4x4 = bs.uastc_q > 0;
params.m_rdo_uastc_ldr_4x4_quality_scalar = bs.uastc_q;
params.m_rdo_uastc_ldr_4x4_dict_size = 1024;
#else
params.m_pack_uastc_flags &= ~basisu::cPackUASTCLevelMask;
params.m_pack_uastc_flags |= s_level_flags[bs.uastc_l];
params.m_rdo_uastc = bs.uastc_q > 0;
params.m_rdo_uastc_quality_scalar = bs.uastc_q;
params.m_rdo_uastc_dict_size = 1024;
#endif
}
else
{
#if BASISU_LIB_VERSION >= 200
params.m_etc1s_compression_level = bs.etc1s_l;
params.m_quality_level = bs.etc1s_q;
params.m_etc1s_max_endpoint_clusters = 0;
params.m_etc1s_max_selector_clusters = 0;
#elif BASISU_LIB_VERSION >= 160
params.m_compression_level = bs.etc1s_l;
params.m_etc1s_quality_level = bs.etc1s_q;
params.m_etc1s_max_endpoint_clusters = 0;
params.m_etc1s_max_selector_clusters = 0;
#else
params.m_compression_level = bs.etc1s_l;
params.m_quality_level = bs.etc1s_q;
params.m_max_endpoint_clusters = 0;
params.m_max_selector_clusters = 0;
#endif
params.m_no_selector_rdo = info.normal_map;
params.m_no_endpoint_rdo = info.normal_map;
}
params.m_perceptual = info.srgb;
params.m_mip_gen = true;
params.m_mip_srgb = info.srgb;
params.m_resample_width = width;
params.m_resample_height = height;
params.m_y_flip = settings.texture_flipy;
params.m_create_ktx2_file = true;
#if BASISU_LIB_VERSION >= 200
params.m_ktx2_and_basis_srgb_transfer_function = info.srgb;
#else
params.m_ktx2_srgb_transfer_func = info.srgb;
#endif
if (uastc)
{
params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD;
params.m_ktx2_zstd_supercompression_level = 9;
}
params.m_read_source_images = true;
#if BASISU_LIB_VERSION >= 150
params.m_write_output_basis_or_ktx2_files = true;
#else
params.m_write_output_basis_files = true;
#endif
params.m_source_filenames.resize(1);
params.m_source_filenames[0] = input;
params.m_out_filename = output;
params.m_status_output = false;
}
static const char* prepareEncode(basisu::basis_compressor_params& params, const cgltf_image& image, const char* input_path, const ImageInfo& info, const Settings& settings, const std::string& temp_prefix, std::string& temp_input, std::string& temp_output)
{
std::string img_data;
std::string mime_type;
if (!readImage(image, input_path, img_data, mime_type))
return "error reading source file";
if (mime_type != "image/png" && mime_type != "image/jpeg")
return NULL;
int width = 0, height = 0;
if (!getDimensions(img_data, mime_type.c_str(), width, height))
return "error parsing image header";
adjustDimensions(width, height, settings.texture_scale[info.kind], settings.texture_limit[info.kind], settings.texture_pow2);
temp_input = temp_prefix + mimeExtension(mime_type.c_str());
temp_output = temp_prefix + ".ktx2";
if (!writeFile(temp_input.c_str(), img_data))
return "error writing temporary file";
int quality = settings.texture_quality[info.kind];
bool uastc = settings.texture_mode[info.kind] == TextureMode_UASTC;
const BasisSettings& bs = kBasisSettings[quality - 1];
fillParams(params, temp_input.c_str(), temp_output.c_str(), uastc, width, height, bs, info, settings);
return NULL;
}
void encodeImagesBasis(std::string* encoded, const cgltf_data* data, const std::vector<ImageInfo>& images, const char* input_path, const Settings& settings)
{
basisu::basisu_encoder_init();
basisu::vector<basisu::basis_compressor_params> params(data->images_count);
basisu::vector<basisu::parallel_results> results(data->images_count);
std::string temp_prefix = getTempPrefix();
std::vector<std::string> temp_inputs(data->images_count);
std::vector<std::string> temp_outputs(data->images_count);
for (size_t i = 0; i < data->images_count; ++i)
{
const cgltf_image& image = data->images[i];
ImageInfo info = images[i];
if (settings.texture_mode[info.kind] == TextureMode_ETC1S || settings.texture_mode[info.kind] == TextureMode_UASTC)
if (const char* error = prepareEncode(params[i], image, input_path, info, settings, temp_prefix + "-" + std::to_string(i), temp_inputs[i], temp_outputs[i]))
encoded[i] = error;
}
uint32_t num_threads = settings.texture_jobs == 0 ? std::thread::hardware_concurrency() : settings.texture_jobs;
basisu::basis_parallel_compress(num_threads, params, results);
for (size_t i = 0; i < data->images_count; ++i)
{
if (params[i].m_source_filenames.empty())
; // encoding was skipped or preparation resulted in an error
else if (results[i].m_error_code == basisu::basis_compressor::cECFailedReadingSourceImages)
encoded[i] = "error decoding source image";
else if (results[i].m_error_code != basisu::basis_compressor::cECSuccess)
encoded[i] = "error encoding image";
else if (!readFile(temp_outputs[i].c_str(), encoded[i]))
encoded[i] = "error reading temporary file";
}
for (size_t i = 0; i < data->images_count; ++i)
{
if (!temp_inputs[i].empty())
removeFile(temp_inputs[i].c_str());
if (!temp_outputs[i].empty())
removeFile(temp_outputs[i].c_str());
}
}
#endif

Some files were not shown because too many files have changed in this diff Show More