|
@@ -9,6 +9,7 @@
|
|
|
:size="isFullScreen ? '32px' : '16px'"
|
|
:size="isFullScreen ? '32px' : '16px'"
|
|
|
@click="handleToggleFullScreen"
|
|
@click="handleToggleFullScreen"
|
|
|
/>
|
|
/>
|
|
|
|
|
+ <span v-if="!loaded" class="progress">{{ progress }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
@@ -36,10 +37,12 @@ export default {
|
|
|
camera: null, // 相机
|
|
camera: null, // 相机
|
|
|
mixer: null, // 动画混合器
|
|
mixer: null, // 动画混合器
|
|
|
renderer: null, // 渲染器
|
|
renderer: null, // 渲染器
|
|
|
|
|
+ pmremGenerator: null, // PMREM 生成器,用于环境光照(PBR)
|
|
|
controls: null, // 轨道控制器
|
|
controls: null, // 轨道控制器
|
|
|
animationId: null, // 动画帧ID
|
|
animationId: null, // 动画帧ID
|
|
|
isFullScreen: false, // 是否全屏
|
|
isFullScreen: false, // 是否全屏
|
|
|
loaded: false, // 是否加载完成
|
|
loaded: false, // 是否加载完成
|
|
|
|
|
+ progress: '0%', // 加载进度
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
watch: {
|
|
watch: {
|
|
@@ -81,20 +84,30 @@ export default {
|
|
|
// 创建相机
|
|
// 创建相机
|
|
|
const width = container.clientWidth;
|
|
const width = container.clientWidth;
|
|
|
const height = container.clientHeight;
|
|
const height = container.clientHeight;
|
|
|
- this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); // 视角75度
|
|
|
|
|
|
|
+ this.camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000); // 视角75度
|
|
|
this.camera.position.set(5, 5, 5); // 设置相机位置
|
|
this.camera.position.set(5, 5, 5); // 设置相机位置
|
|
|
|
|
|
|
|
// 创建渲染器
|
|
// 创建渲染器
|
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true }); // 开启抗锯齿
|
|
|
|
|
+ // 在高 DPI 屏幕上限制像素比以平衡清晰度与性能
|
|
|
|
|
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
|
this.renderer.setSize(width, height);
|
|
this.renderer.setSize(width, height);
|
|
|
this.renderer.shadowMap.enabled = true; // 启用阴影映射
|
|
this.renderer.shadowMap.enabled = true; // 启用阴影映射
|
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 使用柔和阴影
|
|
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 使用柔和阴影
|
|
|
|
|
|
|
|
// 启用颜色管理和正确的色彩空间
|
|
// 启用颜色管理和正确的色彩空间
|
|
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace; // 设置输出色彩空间
|
|
this.renderer.outputColorSpace = THREE.SRGBColorSpace; // 设置输出色彩空间
|
|
|
|
|
+ // 启用物理光照计算,使光照强度与光源单位保持一致
|
|
|
|
|
+ this.renderer.physicallyCorrectLights = true;
|
|
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping; // 使用ACES电影色调映射
|
|
this.renderer.toneMapping = THREE.ACESFilmicToneMapping; // 使用ACES电影色调映射
|
|
|
this.renderer.toneMappingExposure = 1.0; // 曝光度
|
|
this.renderer.toneMappingExposure = 1.0; // 曝光度
|
|
|
|
|
|
|
|
|
|
+ // 创建 PMREMGenerator,用于从 equirectangular/HDR 贴图生成环境贴图(用于 PBR 材质)
|
|
|
|
|
+ if (THREE.PMREMGenerator) {
|
|
|
|
|
+ this.pmremGenerator = new THREE.PMREMGenerator(this.renderer); // 创建 PMREM 生成器
|
|
|
|
|
+ this.pmremGenerator.compileEquirectangularShader(); // 预编译着色器
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
container.appendChild(this.renderer.domElement); // 将渲染器添加到DOM
|
|
container.appendChild(this.renderer.domElement); // 将渲染器添加到DOM
|
|
|
|
|
|
|
|
// 添加光源
|
|
// 添加光源
|
|
@@ -116,6 +129,8 @@ export default {
|
|
|
this.controls.enableDamping = true; // 启用阻尼效果
|
|
this.controls.enableDamping = true; // 启用阻尼效果
|
|
|
this.controls.dampingFactor = 0.25; // 阻尼系数
|
|
this.controls.dampingFactor = 0.25; // 阻尼系数
|
|
|
|
|
|
|
|
|
|
+ // 绑定 animate 的 this,避免在 requestAnimationFrame 中丢失上下文
|
|
|
|
|
+ this.animate = this.animate.bind(this);
|
|
|
// 开始渲染循环
|
|
// 开始渲染循环
|
|
|
this.animate();
|
|
this.animate();
|
|
|
|
|
|
|
@@ -128,7 +143,7 @@ export default {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const modelUrl = this.data.file_list[0].file_url;
|
|
|
|
|
|
|
+ const modelUrl = this.data.model_list[0].file_url;
|
|
|
if (!modelUrl) {
|
|
if (!modelUrl) {
|
|
|
console.error('模型文件URL不存在');
|
|
console.error('模型文件URL不存在');
|
|
|
return;
|
|
return;
|
|
@@ -190,7 +205,7 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
(progress) => {
|
|
(progress) => {
|
|
|
- console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
|
|
|
|
|
|
|
+ this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
|
|
|
if (progress.loaded >= progress.total) {
|
|
if (progress.loaded >= progress.total) {
|
|
|
this.loaded = true;
|
|
this.loaded = true;
|
|
|
}
|
|
}
|
|
@@ -226,7 +241,7 @@ export default {
|
|
|
this.addModelToScene(fbx);
|
|
this.addModelToScene(fbx);
|
|
|
},
|
|
},
|
|
|
(progress) => {
|
|
(progress) => {
|
|
|
- console.log('加载进度:', `${(progress.loaded / progress.total) * 100}%`);
|
|
|
|
|
|
|
+ this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
|
|
|
if (progress.loaded >= progress.total) {
|
|
if (progress.loaded >= progress.total) {
|
|
|
this.loaded = true;
|
|
this.loaded = true;
|
|
|
}
|
|
}
|
|
@@ -245,9 +260,10 @@ export default {
|
|
|
this.loadOBJModel(loader, url);
|
|
this.loadOBJModel(loader, url);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
const mtlLoader = new MTLLoader();
|
|
const mtlLoader = new MTLLoader();
|
|
|
|
|
|
|
|
- // 设置材质加载路径
|
|
|
|
|
|
|
+ // 设置材质和纹理加载路径
|
|
|
const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
|
|
const basePath = mtlUrl.substring(0, mtlUrl.lastIndexOf('/') + 1);
|
|
|
mtlLoader.setPath(basePath);
|
|
mtlLoader.setPath(basePath);
|
|
|
mtlLoader.setResourcePath(basePath);
|
|
mtlLoader.setResourcePath(basePath);
|
|
@@ -264,7 +280,6 @@ export default {
|
|
|
},
|
|
},
|
|
|
undefined,
|
|
undefined,
|
|
|
(error) => {
|
|
(error) => {
|
|
|
- // 材质文件加载失败,使用默认材质
|
|
|
|
|
console.warn('MTL材质文件加载失败,使用默认材质:', error);
|
|
console.warn('MTL材质文件加载失败,使用默认材质:', error);
|
|
|
this.loadOBJModel(loader, url);
|
|
this.loadOBJModel(loader, url);
|
|
|
},
|
|
},
|
|
@@ -283,6 +298,7 @@ export default {
|
|
|
this.addModelToScene(obj);
|
|
this.addModelToScene(obj);
|
|
|
},
|
|
},
|
|
|
(progress) => {
|
|
(progress) => {
|
|
|
|
|
+ this.progress = `${((progress.loaded / progress.total) * 100).toFixed(2)}%`;
|
|
|
if (progress.loaded >= progress.total) {
|
|
if (progress.loaded >= progress.total) {
|
|
|
this.loaded = true;
|
|
this.loaded = true;
|
|
|
}
|
|
}
|
|
@@ -316,6 +332,27 @@ export default {
|
|
|
this.camera.position.set(cameraZ, cameraZ, cameraZ);
|
|
this.camera.position.set(cameraZ, cameraZ, cameraZ);
|
|
|
this.camera.lookAt(0, 0, 0);
|
|
this.camera.lookAt(0, 0, 0);
|
|
|
this.controls.target.set(0, 0, 0);
|
|
this.controls.target.set(0, 0, 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算相机到原点的大致距离并根据模型尺寸调整 far
|
|
|
|
|
+ const cameraDistance = Math.sqrt(
|
|
|
|
|
+ this.camera.position.x * this.camera.position.x +
|
|
|
|
|
+ this.camera.position.y * this.camera.position.y +
|
|
|
|
|
+ this.camera.position.z * this.camera.position.z,
|
|
|
|
|
+ );
|
|
|
|
|
+ const desiredFar = cameraDistance + Math.max(size.x, size.y, size.z) * 2;
|
|
|
|
|
+ this.camera.far = Math.max(this.camera.far || 1000, desiredFar);
|
|
|
|
|
+ this.camera.updateProjectionMatrix();
|
|
|
|
|
+
|
|
|
|
|
+ // 为 OrbitControls 设置限制,避免相机飞离或穿透模型
|
|
|
|
|
+ if (this.controls) {
|
|
|
|
|
+ const minDist = Math.max(0.1, Math.min(maxDim * 0.5, cameraDistance * 0.1));
|
|
|
|
|
+ const maxDist = Math.max(cameraDistance * 10, maxDim * 10, cameraZ * 10);
|
|
|
|
|
+ this.controls.minDistance = minDist;
|
|
|
|
|
+ this.controls.maxDistance = maxDist;
|
|
|
|
|
+ // 限制极角,防止视角翻转
|
|
|
|
|
+ this.controls.maxPolarAngle = Math.PI - 0.1;
|
|
|
|
|
+ this.controls.update();
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
animate() {
|
|
animate() {
|
|
@@ -364,6 +401,10 @@ export default {
|
|
|
this.camera.updateProjectionMatrix();
|
|
this.camera.updateProjectionMatrix();
|
|
|
|
|
|
|
|
// 更新渲染器尺寸
|
|
// 更新渲染器尺寸
|
|
|
|
|
+ // 同步像素比以适配当前屏幕密度,避免过高开销
|
|
|
|
|
+ if (this.renderer.setPixelRatio) {
|
|
|
|
|
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
|
|
|
|
|
+ }
|
|
|
this.renderer.setSize(width, height);
|
|
this.renderer.setSize(width, height);
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -388,6 +429,14 @@ export default {
|
|
|
if (this.scene) {
|
|
if (this.scene) {
|
|
|
this.scene.clear();
|
|
this.scene.clear();
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 释放 PMREMGenerator
|
|
|
|
|
+ if (this.pmremGenerator) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.pmremGenerator.dispose();
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ this.pmremGenerator = null;
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -430,6 +479,14 @@ export default {
|
|
|
@include spin;
|
|
@include spin;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ .progress {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ right: 28px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.fullscreen-btn {
|
|
.fullscreen-btn {
|
|
|
position: absolute;
|
|
position: absolute;
|
|
|
top: 10px;
|
|
top: 10px;
|