LookDev 是一个实时 3D 模型材质预览工具,用于在游戏内调整和预览 PBR 材质参数。
核心功能:
- 实时加载和预览 3D 模型(支持 FBX、OBJ 等格式)
- 调整 PBR 材质参数(Base Color、Metallic、Roughness、Emissive 等)
- 实时预览材质变化效果
- 支持多材质模型
LookDevScreen (父 Fragment)
├── ModelTreeFragment (左侧 20%) - 模型列表和材质选择
├── PreviewFragment (中间 60%) - 3D 预览窗口
└── ParameterPanelFragment (右侧 20%) - 材质参数调整面板
-
MaterialOverrideManager - 材质覆盖管理器
- 管理材质参数的临时覆盖值
- 不修改原始材质,只在预览时应用覆盖
- 位置:
lookdev/material/MaterialOverrideManager.java
-
OffscreenRenderer - 离屏渲染器
- 使用 FBO (Framebuffer Object) 在后台渲染 3D 模型
- 渲染结果通过纹理传递给 UI 线程显示
- 位置:
lookdev/render/OffscreenRenderer.java
-
PreviewView - 预览视图
- 显示 OffscreenRenderer 渲染的纹理
- 处理鼠标交互(旋转、缩放)
- 位置:
lookdev/ui/PreviewView.java
-
OrbitCamera - 轨道相机
- 管理相机位置和视角
- 支持鼠标拖动旋转和滚轮缩放
- 位置:
lookdev/camera/OrbitCamera.java
用户点击模型
→ ModelTreeFragment.onModelSelected()
→ LookDevScreen.onModelSelected()
→ PreviewFragment.setModel()
→ OffscreenRenderer.setModel()
→ 触发渲染
用户拖动滑块
→ SeekBar.onProgressChanged(fromUser=true)
→ ColorSliderCallback.onValueChanged()
→ MaterialOverrideManager.setBaseColorOverride() // 更新覆盖值
→ ParameterPanelFragment.notifyMaterialChanged()
→ LookDevScreen.onMaterialParametersChanged()
→ PreviewFragment.refresh()
→ PreviewFragment.updateOffscreenRenderer()
→ OffscreenRenderer.setMaterial(PBRMaterial) // 传递包含覆盖值的材质
→ OffscreenRenderer.requestRender()
→ [Render Thread] OffscreenRenderer.renderModel()
→ renderMesh() 使用 currentMaterial.getBaseColor() // 关键:使用 PBRMaterial 而非 AssimpMaterial
→ 渲染完成,更新纹理
→ [UI Thread] PreviewView.invalidate()
→ 显示更新后的预览
关键点:
fromUser=true才会触发回调,程序设置初始值时fromUser=false- 必须使用
currentMaterial(PBRMaterial)而不是material(AssimpMaterial) - PBRMaterial 包含 MaterialOverrideManager 的覆盖值
- AssimpMaterial 只包含模型文件中的原始值
ParameterPanelFragment (子)
→ getParentFragment() 获取 LookDevScreen
→ 调用 LookDevScreen.onMaterialParametersChanged()
→ LookDevScreen 转发给 PreviewFragment
注意: 不要使用 EventBus 或其他复杂机制,直接通过父 Fragment 协调即可。
症状: 拖动 Base Color 或 Emissive Color 滑块,预览不更新
原因: OffscreenRenderer.renderMesh() 使用了错误的材质数据源
错误代码:
// ❌ 错误:使用 AssimpMaterial 的原始值
float[] diffuseColor = material.getDiffuseColor();
Vector4f color = new Vector4f(diffuseColor[0], diffuseColor[1], diffuseColor[2], diffuseColor[3]);正确代码:
// ✅ 正确:使用 PBRMaterial 的覆盖值
Vector4f baseColor;
if (currentMaterial != null) {
baseColor = currentMaterial.getBaseColor(); // 包含 MaterialOverrideManager 的覆盖
} else {
float[] diffuseColor = material != null ? material.getDiffuseColor() : new float[]{1.0f, 1.0f, 1.0f, 1.0f};
baseColor = new Vector4f(diffuseColor[0], diffuseColor[1], diffuseColor[2], diffuseColor[3]);
}调试方法:
- 检查日志中是否有
onProgressChanged: progress=X, fromUser=true - 检查是否有
Base Color X changed to: Y - 检查是否有
notifyMaterialChanged called - 检查是否有
LookDevScreen.onMaterialParametersChanged called - 检查是否有
PreviewFragment.refresh() called - 检查是否有
OffscreenRenderer.requestRender called - 检查渲染后的
first pixel值是否变化
症状: ParameterPanel 或 ModelTree 的内容不可见
原因: ScrollView 或 contentLayout 缺少 LayoutParams
解决方案:
scrollView = new ScrollView(context);
scrollView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
));
contentLayout = new LinearLayout(context);
contentLayout.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
));症状: 游戏启动后不久崩溃,日志显示 GL_OUT_OF_MEMORY
原因:
- FBO 和纹理没有正确释放
- 多次创建 OffscreenRenderer 导致资源泄漏
- 游戏整体显存占用过高
解决方案:
- 确保 OffscreenRenderer 在 Fragment.onDestroyView() 中释放资源
- 使用单例模式或在 LookDevScreen 级别管理 OffscreenRenderer
- 降低预览分辨率(默认 512x512)
- 检查是否有纹理缓存未清理
资源释放代码:
@Override
public void onDestroyView() {
super.onDestroyView();
if (offscreenRenderer != null) {
RenderSystem.recordRenderCall(() -> {
offscreenRenderer.cleanup(); // 释放 FBO、纹理、Renderbuffer
});
}
}症状: UI 控件为 null,或者回调没有触发
原因: Fragment 生命周期理解错误
正确的生命周期顺序:
1. onCreateView() - 创建 View 树,初始化 UI 控件
2. onViewCreated() - View 已创建,可以设置监听器和初始化数据
3. [用户交互]
4. onDestroyView() - 清理资源,释放引用
注意事项:
- 在
onCreateView()中创建所有 UI 控件 - 在
onViewCreated()中设置监听器和加载数据 - 不要在
onCreateView()中访问父 Fragment(可能还未关联) - 在
onDestroyView()中清理所有资源引用
症状: 添加了 DebugManager.debug() 但日志不显示
原因: debug 模式默认禁用
解决方案:
// 在需要详细日志的地方启用 debug 模式
DebugManager.setDebugEnabled(true);
// 或者使用 info 级别(总是输出)
DebugManager.info(DebugCategory.LOOKDEV, "Message");日志级别:
debug()- 详细调试信息,默认禁用info()- 一般信息,总是输出warn()- 警告信息error()- 错误信息
在关键路径上添加日志:
// 滑块事件
DebugManager.info(DebugCategory.LOOKDEV, "onProgressChanged: progress={}, fromUser={}", progress, fromUser);
// 材质更新
DebugManager.info(DebugCategory.LOOKDEV, "Base Color R changed to: {}", value);
// Fragment 通信
DebugManager.info(DebugCategory.LOOKDEV, "notifyMaterialChanged called");
DebugManager.info(DebugCategory.LOOKDEV, "LookDevScreen.onMaterialParametersChanged called");
// 渲染
DebugManager.info(DebugCategory.LOOKDEV, "[PreviewFragment] refresh() called");
DebugManager.info(DebugCategory.LOOKDEV, "[OffscreenRenderer] requestRender called");
DebugManager.info(DebugCategory.LOOKDEV, "[OffscreenRenderer] Using PBRMaterial baseColor: ({}, {}, {}, {})",
baseColor.x, baseColor.y, baseColor.z, baseColor.w);常见错误: 修改了代码但游戏运行的是旧版本
检查步骤:
- 确认 JAR 文件时间戳是最新的
- 确认部署路径正确(
versions\Create New Horizon\mods\而不是.minecraft\mods\) - 确认游戏重启后加载了新的 JAR
- 检查日志时间戳是否在 JAR 构建之后
自动化脚本:
# build-and-run.ps1 会自动:
# 1. 构建 mod
# 2. 部署到正确的 mods 目录
# 3. 启动游戏(如果 testgame.ps1 存在)
.\build-and-run.ps1如果功能突然失效:
# 查看最近的提交
git log --oneline -10
# 对比工作版本和当前版本
git diff <working-commit> HEAD -- src/main/java/cn/minerealms/assimploader/lookdev/
# 回滚到工作版本测试
git checkout <working-commit>步骤:
- 在
PBRMaterial中添加字段和 getter/setter - 在
MaterialOverrideManager中添加覆盖方法 - 在
ParameterPanelFragment中添加 UI 控件和回调 - 在
OffscreenRenderer.renderMesh()中使用新参数 - 测试并提交
示例:添加 Roughness 参数
// 1. PBRMaterial.java
private float roughness = 0.5f;
public float getRoughness() {
return roughness;
}
public void setRoughness(float roughness) {
this.roughness = roughness;
}
// 2. MaterialOverrideManager.java
public void setRoughnessOverride(float roughness) {
if (overrideMaterial != null) {
overrideMaterial.setRoughness(roughness);
}
}
// 3. ParameterPanelFragment.java
private SeekBar roughnessSlider;
private TextView roughnessValue;
// 在 onCreateView() 中创建控件
roughnessSlider = new SeekBar(context);
roughnessSlider.setMax(100);
roughnessValue = new TextView(context);
// 在 setupListeners() 中设置回调
setupFloatSlider(roughnessSlider, roughnessValue, (value) -> {
if (overrideManager != null) {
overrideManager.setRoughnessOverride(value / 100f);
notifyMaterialChanged();
}
});
// 4. OffscreenRenderer.java
// 在 renderMesh() 或 shader 中使用 currentMaterial.getRoughness()注意事项:
- 所有 OpenGL 调用必须在 Render Thread 执行
- 使用
RenderSystem.recordRenderCall()从 UI Thread 调度渲染任务 - 不要在 Render Thread 中修改 UI 控件
示例:
// ❌ 错误:在 UI Thread 直接调用 OpenGL
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
// ✅ 正确:通过 RenderSystem 调度
RenderSystem.recordRenderCall(() -> {
GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureId);
});- 修改代码
- 运行
.\build-and-run.ps1(自动构建、部署、启动游戏) - 在游戏中打开 LookDev 界面
- 测试功能
- 检查日志:
G:\MinecraftGames\CTNH-BaopuEdition\.minecraft\versions\Create New Horizon\logs\latest.log - 如果有问题,添加更多日志并重复步骤 1-5
// 使用 needsRender 标志避免重复渲染
private boolean needsRender = true;
public void requestRender() {
if (!needsRender) {
return; // 已经有渲染请求在队列中
}
needsRender = true;
RenderSystem.recordRenderCall(this::render);
}
private void render() {
if (!needsRender) {
return;
}
needsRender = false;
// 执行渲染...
}// 缓存嵌入纹理,避免重复加载
private final Map<String, Map<Integer, ResourceLocation>> textureCache = new HashMap<>();
// 使用前检查缓存
if (!textureCache.containsKey(modelKey)) {
loadEmbeddedTextures(model, modelKey);
}// 默认 512x512,可以根据需要调整
private static final int PREVIEW_WIDTH = 512;
private static final int PREVIEW_HEIGHT = 512;
// 或者提供用户可调节的分辨率选项| 功能 | 文件路径 |
|---|---|
| 主界面 | lookdev/ui/LookDevScreen.java |
| 模型列表 | lookdev/ui/ModelTreeFragment.java |
| 预览窗口 | lookdev/ui/PreviewFragment.java |
| 参数面板 | lookdev/ui/ParameterPanelFragment.java |
| 预览视图 | lookdev/ui/PreviewView.java |
| 离屏渲染 | lookdev/render/OffscreenRenderer.java |
| 材质覆盖 | lookdev/material/MaterialOverrideManager.java |
| 轨道相机 | lookdev/camera/OrbitCamera.java |
| PBR 材质 | material/PBRMaterial.java |
| 调试管理 | debug/DebugManager.java |
-
永远不要直接修改 AssimpMaterial
- AssimpMaterial 是从模型文件加载的原始数据
- 使用 MaterialOverrideManager 管理临时覆盖
- 渲染时使用 PBRMaterial(包含覆盖值)
-
注意线程安全
- UI 操作在 UI Thread
- OpenGL 调用在 Render Thread
- 使用 RenderSystem.recordRenderCall() 跨线程调度
-
资源管理
- Fragment 销毁时释放 OpenGL 资源
- 使用完纹理后调用 glDeleteTextures
- FBO 和 Renderbuffer 也要释放
-
调试优先
- 遇到问题先添加日志
- 确认数据流的每个环节
- 不要猜测,用日志验证
-
构建验证
- 修改代码后必须重新构建
- 确认 JAR 文件时间戳
- 确认游戏加载了新版本
- ModernUI 文档:https://github.com/BloCamLimb/ModernUI
- Assimp 文档:https://assimp.sourceforge.net/
- OpenGL FBO 教程:https://learnopengl.com/Advanced-OpenGL/Framebuffers
- Minecraft Forge 文档:https://docs.minecraftforge.net/
- 修复 OffscreenRenderer 使用错误的材质数据源
- 添加完整的调试日志链
- 修复 ScrollView 缺少 LayoutParams
- 修复构建脚本部署路径
如果遇到本文档未涵盖的问题,请:
- 检查日志中的错误信息
- 搜索代码中的相关日志输出
- 对比最近的 Git 提交
- 添加更多日志来定位问题