SceneView 有两个编辑器版本:精简版(3d-editor-lite)用于快速迭代新功能,完整版(3d-editor)承载全部功能模块。本次任务是将精简版中已完成的材质列表 + 材质覆盖编辑功能完整同步到完整版,并确保构建通过、UI 样式一致。
这篇文章复盘整个同步过程,涵盖数据模型设计、Viewport 材质应用链路、右侧面板 UI 实现,以及同步过程中遇到的构建错误和样式问题的修复。
一、功能概览:材质列表与覆盖编辑
同步的功能包含两个部分:
1.1 材质列表
在编辑器右侧面板中,选中模型后展示该模型所有材质的列表,每个条目显示:
- 材质预览色块:实时反映材质的颜色/金属度/粗糙度组合
- 材质名称:来自
material.name,未命名时自动生成Material_N - 所属网格名:帮助用户在多个同名材质中区分来源
- 自定义标识:若该材质已被用户覆盖,显示黄色编辑图标
数据来源是模型加载时自动收集的 materialsMeta 字段:
// model.materialsMeta 结构
{
"CarPaint_Red": { name: "CarPaint_Red", meshName: "Body_Mesh" },
"Wheel_Material": { name: "Wheel_Material", meshName: "Wheel_01" }
}
1.2 材质覆盖编辑
点击材质列表中的某一项,右侧面板展开该材质的覆盖编辑区域,可单独调整:
- 颜色(baseColor)
- 金属度(metalness,0~1)
- 粗糙度(roughness,0~1)
- 透明度(opacity,0~1)
- 反射强度(envMapIntensity,0~4)
覆盖配置存储在 model.materialOverrides 中:
// model.materialOverrides 结构
{
"CarPaint_Red": {
baseColor: "#ff0000",
metalness: 0.9,
roughness: 0.2,
opacity: 1.0,
envMapIntensity: 1.5
}
}
二、数据模型:modelStore 的改动
在 modelStore.js 中,addModel 和 addModelToScene 两个函数都需要初始化两个新字段:
// 模型数据结构中新增字段
const newModel = {
id: modelId,
name: modelName,
// ... 原有字段 ...
materialOverrides: {}, // 材质覆盖配置
materialsMeta: {}, // 材质元信息(名称 → { name, meshName })
}
同时新增 updateModelMaterialOverride 函数,用于更新单个材质的覆盖配置并触发视口刷新:
function updateModelMaterialOverride(modelId, materialName, overridePatch) {
const model = models.value.find(m => m.id === modelId)
if (!model) return
if (!model.materialOverrides) model.materialOverrides = {}
// 合并覆盖配置
model.materialOverrides[materialName] = {
...model.materialOverrides[materialName],
...overridePatch
}
// 触发视口重新应用材质
window.dispatchEvent(new CustomEvent('sync-model-materials', {
detail: { modelId }
}))
}
三、Viewport 材质应用链路
材质从配置到最终渲染,经过以下链路:
| 步骤 | 函数/事件 | 作用 |
|---|---|---|
| 1 | applyModelMaterials(model) | 读取 model.materialOverrides,应用到模型所有 mesh 的材质上 |
| 2 | sync-model-materials 事件 | 通知 Viewport 重新调用 applyModelMaterials |
| 3 | preview-model-material 事件 | 实时预览材质调整(不写入 materialOverrides) |
| 4 | applyPreviewMaterialPatch | 支持按 materialName 过滤,只预览指定材质 |
3.1 Phong → Standard 材质转换
部分旧模型使用 MeshPhongMaterial,但金属度、粗糙度是 PBR 专属属性。当材质需要应用 PBR 覆盖时,需要自动将 Phong 转换为 Standard:
// 检查材质是否需要 PBR 属性
const needsPbrConfig = (mat) =>
mat && (mat.metalness !== undefined || mat.roughness !== undefined)
// Phong → Standard 转换
if (child.material.isMeshPhongMaterial && needsPbrConfig(child.material)) {
const oldMat = child.material
const newMat = new THREE.MeshStandardMaterial({
color: oldMat.color,
map: oldMat.map,
transparent: oldMat.transparent,
opacity: oldMat.opacity,
// 从 Phong 属性推算 PBR 默认值
metalness: oldMat.shininess ? 0.3 : 0.0,
roughness: 1.0 - (oldMat.shininess || 30) / 100
})
child.material = newMat
// 标记已克隆,避免重复转换
child.userData._materialCloned = true
}
3.2 materialsMeta 自动收集
在 applyModelMaterials 函数末尾,遍历模型所有 mesh 的材质,按名称去重后填充 model.materialsMeta:
// 收集模型中所有 unique 材质的元数据
model.materialsMeta = {}
group.traverse((child) => {
if (!child.isMesh || !child.material) return
const materials = Array.isArray(child.material) ? child.material : [child.material]
for (let i = 0; i < materials.length; i++) {
const mat = materials[i]
if (!mat) continue
const name = mat.name || `Material_${i}`
if (!model.materialsMeta[name]) {
model.materialsMeta[name] = { name, meshName: child.name || 'Mesh' }
}
}
})
四、右侧面板 UI 实现
材质列表和覆盖编辑 UI 位于 EditorRightPanel.vue 中,选中模型后自动展示。
4.1 材质列表模板
<div v-if="modelMaterials.length > 0" class="config-section">
<div class="section-title">材质列表 ({{ modelMaterials.length }})</div>
<div class="material-list">
<div
v-for="mat in modelMaterials"
:key="mat.matName"
class="material-item"
:class="{ selected: selectedMaterialName === mat.matName, overridden: hasMaterialOverride(mat.matName) }"
@click="selectMaterial(mat.matName)"
>
<div class="material-item-preview" :style="{ background: getMaterialPreviewColor(mat.matName) }" />
<div class="material-item-info">
<span class="material-name">{{ mat.name }}</span>
<span class="material-mesh">{{ mat.meshName }}</span>
</div>
<el-icon v-if="hasMaterialOverride(mat.matName)" class="override-badge" title="已自定义">
<EditPen />
</el-icon>
</div>
</div>
</div>
4.2 材质覆盖编辑模板
选中材质后,面板展开覆盖编辑区域,包含颜色选择器、滑块(金属度/粗糙度/透明度/反射强度)以及「重置」按钮。
4.3 CSS 样式
材质列表的样式定义在 <style scoped> 中,关键类包括:
.material-list {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 220px;
overflow-y: auto;
}
.material-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
color: #d1d5db;
transition: background 0.15s;
border: 1px solid transparent;
}
.material-item.selected {
background: rgba(59, 130, 246, 0.18);
border-color: #3b82f6;
color: #93c5fd;
}
.material-item.overridden {
border-left: 3px solid #fbbf24;
}
五、同步过程中的问题与修复
5.1 构建错误:Identifier 'hasOwn' has already been declared
同步完成后首次构建报错:
[vue/compiler-sfc] SyntaxError: Identifier 'hasOwn' has already been declared. (3559:8)
根因:在 applyModelMaterials 函数中,const hasOwn = (obj, key) => !!obj && Object.prototype.hasOwnProperty.call(obj, key) 被声明了两次(第 3586 行和第 3616 行)。
修复:删除第 3616 行的重复声明。
5.2 材质列表不显示
构建通过后运行应用,发现材质列表始终不显示。
根因:materialsMeta 字段虽然初始化为空 {},但完整版的 applyModelMaterials 末尾缺少收集逻辑(精简版有,但同步时遗漏了)。
修复:在完整版 applyModelMaterials 末尾添加遍历 mesh 材质并填充 materialsMeta 的逻辑(见第三节 3.2)。
5.3 材质列表样式不一致
样式修复后,材质列表能显示了,但外观与精简版不同。
根因:完整版 EditorRightPanel.vue 的 <style scoped> 中缺少 .material-list、.material-item 等全套 CSS 类的定义。精简版有完整定义,同步时只同步了模板和脚本,遗漏了样式部分。
修复:将精简版中 2853~2935 行的材质列表 CSS 完整复制到完整版对应的 <style> 区块中。
5.4 onSyncModelMaterials 调用不存在的函数
根因:onSyncModelMaterials 中调用了 applyMaterials(model),但实际函数名是 applyModelMaterials。
修复:改为 applyModelMaterials(model)。
六、总结
本次同步涉及 3 个核心文件、4 个 Bug 修复,最终实现:
| 文件 | 改动内容 |
|---|---|
modelStore.js | 新增 materialOverrides、materialsMeta 字段;新增 updateModelMaterialOverride 函数 |
EditorRightPanel.vue | 材质列表模板 + 覆盖编辑模板 + 全套 CSS 样式 + script 逻辑 |
EditorViewport.vue | applyModelMaterials 增强(materialOverrides 应用 + Phong→Standard 转换 + materialsMeta 收集);preview-model-material 支持 materialName 过滤;修复 onSyncModelMaterials 函数名 |
几个值得记住的工程教训:
- 同步功能时不要只同步模板/脚本,CSS 也容易被遗漏:本次样式问题就是典型例子——HTML 和 JS 都同步了,但
<style>块里的 CSS 没有一并复制。 - 数据收集逻辑要跟着数据流走:
materialsMeta的收集逻辑放在applyModelMaterials末尾,确保每次材质应用后数据都是最新的。 - Phong → Standard 转换需要谨慎处理:转换后原材质的引用关系会改变,需要用
child.userData._materialCloned标记避免重复转换。 - 构建错误要第一时间修:
hasOwn重复声明是典型的复制粘贴导致的错误,在同步代码时尤其要注意变量/函数名冲突。