3d出来后的卡片交互修改
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<!-- 高级工具选择 -->
|
||||
<div class="cards-select">
|
||||
<div v-for="v in (node.data?.secondaryMenu?.selectList || list)" :key="v.type" @click="onClickItem(v)" v-show="v.tier === tier">
|
||||
<div
|
||||
v-for="v in (node.data?.secondaryMenu?.selectList || list)"
|
||||
:key="v.type"
|
||||
@click="onClickItem(v)"
|
||||
v-show="
|
||||
(v.tier === tier) &&
|
||||
((v.type == NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL && props.tier == NODE_DATATIER.TO_REAL_VARIANTS))||
|
||||
(v.type != NODE_DATATYPE.TO_REAL_VARIANTS && (superiorNode?.data?.superiorNodeType != NODE_DATATYPE.TO_3D_MODEL || props.tier != NODE_DATATIER.TO_REAL_VARIANTS)))
|
||||
"
|
||||
>
|
||||
<span class="icon">
|
||||
<svg-icon :name="v.type + '-2'" size="15" size-unit="px" />
|
||||
</span>
|
||||
@@ -19,12 +28,20 @@
|
||||
node: { required: true, type: Object },
|
||||
tier: { default: 1, type: Number }
|
||||
})
|
||||
const superiorNode = computed(() => {
|
||||
return stateManager.getNodeById(props.node.data.superiorID)
|
||||
})
|
||||
const list = ref([
|
||||
{
|
||||
tier: NODE_DATATIER.TO_REAL_STYLE,
|
||||
type: NODE_DATATYPE.TO_REAL_STYLE,
|
||||
title: 'To Real Style'
|
||||
},
|
||||
{
|
||||
tier: NODE_DATATIER.TO_REAL_VARIANTS,
|
||||
type: NODE_DATATYPE.TO_REAL_VARIANTS,
|
||||
title: 'To Real Variants'
|
||||
},
|
||||
{
|
||||
tier: NODE_DATATIER.SURFACE_EDIT,
|
||||
type: NODE_DATATYPE.SURFACE_EDIT,
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
import { computed, ref, useAttrs, onMounted, inject, watch } from 'vue'
|
||||
import CardsSelect from './cards-select.vue'
|
||||
import ToRealStyle from './to-real-style.vue'
|
||||
import ToRealVariants from './to-real-variants.vue'
|
||||
import SurfaceEdit from './surface-edit.vue'
|
||||
import FastMode from './fast-mode.vue'
|
||||
import SceneComposition from './scene-composition.vue'
|
||||
@@ -69,6 +70,13 @@
|
||||
component: ToRealStyle,
|
||||
api: toRealStyleApi
|
||||
},
|
||||
{
|
||||
tier: NODE_DATATIER.TO_REAL_VARIANTS,
|
||||
type: NODE_DATATYPE.TO_REAL_VARIANTS,
|
||||
title: 'To Real Variants',
|
||||
component: ToRealVariants,
|
||||
api: toRealStyleApi
|
||||
},
|
||||
{
|
||||
tier: NODE_DATATIER.Fast_MODE,
|
||||
type: NODE_DATATYPE.Fast_MODE,
|
||||
@@ -197,6 +205,7 @@
|
||||
data: {
|
||||
superiorID: attrs.node.id,
|
||||
superiorNodeType: attrs.node?.data?.type,
|
||||
superiorGenerateImg: superiorNodeUrl || null,
|
||||
createIndexPosition: index + subordNodes.length,
|
||||
tier: tier,
|
||||
isActive: index == 0 && subordNodes.length == 0,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<!-- 转换为真实图 -->
|
||||
<div class="to-real-style">
|
||||
<p class="label">Prompt</p>
|
||||
<my-textarea v-model="data.prompt" :placeholder="$t('flowCanvas.toRealVariantsPlaceholder')" />
|
||||
<div class="shortcut-list">
|
||||
<div
|
||||
class="item"
|
||||
v-for="v in shortcutList"
|
||||
:key="v.value"
|
||||
@click="data.prompt = v.value"
|
||||
>
|
||||
{{ v.label }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="label">Mode</p>
|
||||
<my-select v-model="data.mode" :list="modeList" />
|
||||
<p class="label">Size</p>
|
||||
<pixel-ratio-selection v-model="data.pixelRatio" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, reactive, useAttrs } from 'vue'
|
||||
import myTextarea from '../../tools/my-textarea.vue'
|
||||
import mySelect from '../../tools/my-select.vue'
|
||||
import pixelRatioSelection from '../../tools/pixel-ratio-selection.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const shortcutList = ref([
|
||||
{
|
||||
label: t('flowCanvas.toRealVariantsShortcut1Label'),
|
||||
value: t('flowCanvas.toRealVariantsShortcut1Value')
|
||||
},
|
||||
{
|
||||
label: t('flowCanvas.toRealVariantsShortcut2Label'),
|
||||
value: t('flowCanvas.toRealVariantsShortcut2Value')
|
||||
},
|
||||
{
|
||||
label: t('flowCanvas.toRealVariantsShortcut3Label'),
|
||||
value: t('flowCanvas.toRealVariantsShortcut3Value')
|
||||
},
|
||||
{
|
||||
label: t('flowCanvas.toRealVariantsShortcut4Label'),
|
||||
value: t('flowCanvas.toRealVariantsShortcut4Value')
|
||||
},
|
||||
{
|
||||
label: t('flowCanvas.toRealVariantsShortcut5Label'),
|
||||
value: t('flowCanvas.toRealVariantsShortcut5Value')
|
||||
}
|
||||
])
|
||||
const stateManager = inject('stateManager') as any
|
||||
const attrs = useAttrs()
|
||||
const modeList = ref([
|
||||
{ value: 'Advanced', label: 'Advanced' },
|
||||
{ value: 'Normal', label: 'Normal' }
|
||||
])
|
||||
const data = reactive({
|
||||
prompt: '',
|
||||
pixelRatio: '1:1',
|
||||
mode: 'Advanced',
|
||||
})
|
||||
|
||||
const getApiData = ()=>{
|
||||
let superior = stateManager.getNodeById(attrs.node?.data?.superiorID)
|
||||
// let {superiorGenerateImg} = attrs
|
||||
return {
|
||||
mode: data.mode,
|
||||
size: data.pixelRatio,
|
||||
userPrompt: data.prompt,
|
||||
aaa: superior?.data?.superiorGenerateImg,
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ data, getApiData })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.to-real-style {
|
||||
> .shortcut-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 4px;
|
||||
user-select: none;
|
||||
> .item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 3px;
|
||||
font-family: Medium;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
border: 1px solid #e4e4e7;
|
||||
background: #f0f0f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
<span class="icon" @click="onDownload(item)">
|
||||
<svg-icon name="download" size="20" size-unit="px" />
|
||||
</span>
|
||||
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL">
|
||||
<button class="edit" @click="onEdit(item)" v-if="node.data.superiorNodeType !== NODE_DATATYPE.TO_3D_MODEL || node.data.tier == 0">
|
||||
<span class="icon"><svg-icon name="edit" size="13" /></span>
|
||||
<span class="text">Edit</span>
|
||||
</button>
|
||||
@@ -185,7 +185,13 @@
|
||||
])
|
||||
const onPreview = (item: any) => {
|
||||
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){
|
||||
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj,nodeId:props.node.id})
|
||||
openThreeModelPreview({
|
||||
glbPath:item?.glbPath,
|
||||
glbInfoObj:item?.glbInfoObj,
|
||||
nodeId:props.node?.id,
|
||||
nodeType:props.node.data?.superiorNodeType,
|
||||
superiorGenerateImg:props.node.data?.superiorGenerateImg,
|
||||
})
|
||||
}else{
|
||||
openImagePreview(item.url)
|
||||
}
|
||||
|
||||
@@ -29,10 +29,11 @@ const captureView = async ()=>{
|
||||
formData.append('file', file)
|
||||
const minioUrl = await uploadImage(formData, true)
|
||||
ElMessage.warning('Your new view has been captured.')
|
||||
|
||||
emit('captureView', {
|
||||
minioUrl,
|
||||
nodeId: props?.currentData?.nodeId
|
||||
nodeId: props?.currentData?.nodeId,
|
||||
nodeType: props?.currentData?.nodeType,
|
||||
superiorGenerateImg: props?.currentData?.superiorGenerateImg,
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import gsap from 'gsap';
|
||||
import * as THREE from 'three';
|
||||
import { ThreeManager } from './threeTool copy'
|
||||
import { ThreeManager } from './threeTool'
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
|
||||
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
|
||||
import hdri from '@/assets/images/three/hdri.hdr'
|
||||
|
||||
interface ModelInfo {
|
||||
box: THREE.Box3;
|
||||
center: THREE.Vector3;
|
||||
size: THREE.Vector3;
|
||||
maxSize: number;
|
||||
}
|
||||
const CONFIG = {
|
||||
hdriIntensity: 7.4,
|
||||
exposureBase: 0.92,
|
||||
backlightBoost: { min: 2.1, max: 4.8 }, // 背光增强系数
|
||||
hdriUrl: hdri,
|
||||
};
|
||||
export class ThreeManager {
|
||||
threeDom: HTMLElement;
|
||||
scene: THREE.Scene;//场景对象
|
||||
camera: THREE.PerspectiveCamera;//相机对象
|
||||
renderer: THREE.WebGLRenderer;//渲染器对象
|
||||
controls: OrbitControls;//轨道控制器对象
|
||||
|
||||
|
||||
pointLight: THREE.AmbientLight;//环境光对象
|
||||
studioLights: any;//工作室光对象数组
|
||||
v1: THREE.Vector3;//相机前向向量
|
||||
camDir: THREE.Vector3;//相机前向向量
|
||||
camForward: THREE.Vector3;//相机前向向量
|
||||
camToTarget: THREE.Vector3;//相机目标向量
|
||||
|
||||
currentModel: any;//当前模型对象
|
||||
animate: any;//动画对象
|
||||
|
||||
modelInfo: ModelInfo;//模型信息对象
|
||||
defaultSoftboxPositions: Array<{ x: number; y: number; z: number; intensity: number; w: number; h: number }> = [
|
||||
{ x: 0, y: 5.8, z: 3.5, intensity: 3.5, w: 6, h: 4.8 }, // 主光
|
||||
{ x: 0, y: 2.8, z: 7.5, intensity: 2.2, w: 5, h: 4 }, // 前光
|
||||
{ x: 0, y: 2.8, z: -7.5, intensity: 6.5, w: 5, h: 4 }, // 背光
|
||||
{ x: -7.5, y: 2.8, z: 0, intensity: 10.8, w: 5, h: 4 }, // 侧光
|
||||
{ x: 7.5, y: 2.8, z: 0, intensity: 2.0, w: 5, h: 4 }, // 侧光
|
||||
{ x: 0, y: -2.2, z: 3, intensity: 0.8, w: 9, h: 4 } // 地面反光板
|
||||
];
|
||||
constructor(threeDom: HTMLElement,config:any = {}){
|
||||
|
||||
this.threeDom = threeDom;
|
||||
this.studioLights = []
|
||||
//创建场景
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xffffff);
|
||||
//创建相机
|
||||
this.camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
|
||||
this.camera.position.set(0, 1.5, 6); //设置相机位置
|
||||
|
||||
this.v1 = new THREE.Vector3();
|
||||
this.camDir = new THREE.Vector3();
|
||||
this.camForward = new THREE.Vector3();
|
||||
this.camToTarget = new THREE.Vector3();
|
||||
//设置渲染器
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance", preserveDrawingBuffer: true });
|
||||
//设置环境光
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.15));
|
||||
|
||||
// 关键优化:物理光照与色彩管理
|
||||
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = CONFIG.exposureBase;
|
||||
this.renderer.physicallyCorrectLights = true; // 开启物理光照模式,光衰减更真实
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight); //设置渲染区域尺寸
|
||||
this.renderer.setClearColor(0xffffff, 1); //设置背景颜色
|
||||
threeDom.innerHTML = '';
|
||||
threeDom.appendChild(this.renderer.domElement);
|
||||
|
||||
RectAreaLightUniformsLib.init();
|
||||
//设置轨道控制器
|
||||
this.controls = new OrbitControls(this.camera,this.renderer.domElement)//监听鼠标、键盘事件;
|
||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||
this.controls.mouseButtons = {
|
||||
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转:ROTATE)
|
||||
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转:ROTATE)
|
||||
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
|
||||
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
||||
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
||||
}
|
||||
this.controls.enableDamping = true;
|
||||
|
||||
}
|
||||
/**
|
||||
* 根据模型大小计算自适应配置
|
||||
*/
|
||||
calculateAdaptiveConfig(modelInfo: ModelInfo) {
|
||||
const { size, maxSize, center } = modelInfo;
|
||||
// 基础距离系数(可根据需要调整)
|
||||
let distanceFactor = 1.5;
|
||||
// 根据模型形状调整距离系数
|
||||
const aspectRatio = size.x / size.y;
|
||||
if (aspectRatio > 2) {
|
||||
// 扁平模型,拉远一点
|
||||
distanceFactor = 3.0;
|
||||
} else if (aspectRatio < 0.5) {
|
||||
// 高瘦模型,稍微拉近
|
||||
distanceFactor = 2.0;
|
||||
}
|
||||
// 计算相机距离
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
const cameraDistance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
|
||||
// 灯光缩放系数
|
||||
const lightScale = Math.max(0.5, Math.min(2.0, maxSize / 2.5));
|
||||
// 强度缩放系数(模型越大,灯光需要越强)
|
||||
const intensityScale = Math.max(0.6, Math.min(2.5, maxSize / 2));
|
||||
|
||||
console.log('自适应配置:', {
|
||||
相机距离: cameraDistance,
|
||||
灯光缩放: lightScale,
|
||||
强度缩放: intensityScale,
|
||||
目标中心: center
|
||||
});
|
||||
|
||||
return {
|
||||
cameraDistance,
|
||||
lightScale,
|
||||
intensityScale,
|
||||
targetCenter: center.clone()
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 获取模型信息(包围盒、中心点、尺寸等)
|
||||
*/
|
||||
getModelInfo(model: THREE.Object3D): ModelInfo {
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxSize = Math.max(size.x, size.y, size.z);
|
||||
|
||||
console.log('模型信息:', {
|
||||
中心点: center,
|
||||
尺寸: { x: size.x, y: size.y, z: size.z },
|
||||
最大尺寸: maxSize
|
||||
});
|
||||
|
||||
return { box, center, size, maxSize };
|
||||
}
|
||||
/**
|
||||
* 根据模型大小自适应创建柔光箱灯光
|
||||
*/
|
||||
createAdaptiveSoftboxes(modelInfo: ModelInfo) {
|
||||
const { lightScale, intensityScale, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||
// 清除现有灯光
|
||||
this.studioLights.forEach(item => {
|
||||
this.scene.remove(item.light);
|
||||
});
|
||||
this.studioLights = [];
|
||||
|
||||
// 根据模型大小创建自适应灯光
|
||||
this.defaultSoftboxPositions.forEach(pos => {
|
||||
const scaledX = pos.x * lightScale;
|
||||
const scaledY = pos.y * lightScale;
|
||||
const scaledZ = pos.z * lightScale;
|
||||
const scaledW = pos.w * lightScale;
|
||||
const scaledH = pos.h * lightScale;
|
||||
const scaledIntensity = pos.intensity * intensityScale;
|
||||
|
||||
const light = new THREE.RectAreaLight(0xffffff, scaledIntensity, scaledW, scaledH);
|
||||
light.position.set(
|
||||
targetCenter.x + scaledX,
|
||||
targetCenter.y + scaledY,
|
||||
targetCenter.z + scaledZ
|
||||
);
|
||||
light.lookAt(targetCenter);
|
||||
this.scene.add(light);
|
||||
|
||||
this.studioLights.push({
|
||||
light,
|
||||
offset: new THREE.Vector3(scaledX, scaledY, scaledZ),
|
||||
baseIntensity: scaledIntensity
|
||||
});
|
||||
});
|
||||
console.log('自适应灯光已创建,缩放系数:', lightScale, '强度系数:', intensityScale);
|
||||
}
|
||||
/**
|
||||
* 根据模型大小调整相机位置
|
||||
*/
|
||||
adjustCameraToModel(modelInfo: ModelInfo) {
|
||||
const { cameraDistance, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||
// 正面俯视角度:相机在模型正前方,稍高于模型中心
|
||||
// X 轴:模型中心(正面视角,不左右偏移)
|
||||
// Y 轴:相机高度 = 模型中心高度 + 模型高度的 0.3 倍(俯视效果)
|
||||
// Z 轴:相机距离 = 根据模型大小计算的距离
|
||||
const x = targetCenter.x;
|
||||
const y = targetCenter.y + modelInfo.size.y * 0.3; // 相机高度为模型中心的 0.3 倍
|
||||
const z = targetCenter.z + cameraDistance;
|
||||
|
||||
this.camera.position.set(x, y, z);
|
||||
this.controls.target.copy(targetCenter);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
|
||||
createSoftbox(x, y, z, intensity, w = 5, h = 4) {
|
||||
const light = new THREE.RectAreaLight(0xffffff, intensity, w, h);
|
||||
light.position.set(x, y, z);
|
||||
light.lookAt(0, 0, 0);
|
||||
this.scene.add(light);
|
||||
// 存储 offset 向量副本,避免后续重复创建
|
||||
this.studioLights.push({
|
||||
light,
|
||||
offset: new THREE.Vector3(x, y, z),
|
||||
baseIntensity: intensity
|
||||
});
|
||||
}
|
||||
async setHDRI(){
|
||||
const rgbeLoader = new RGBELoader()
|
||||
await rgbeLoader.load(CONFIG.hdriUrl, (texture)=>{
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
||||
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||
|
||||
this.scene.environment = envMap;
|
||||
this.scene.environmentIntensity = CONFIG.hdriIntensity;
|
||||
|
||||
pmremGenerator.dispose();
|
||||
texture.dispose();
|
||||
console.log('✅ HDRI Loaded');
|
||||
});
|
||||
this.createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
|
||||
this.createSoftbox(0, 2.8, 7.5, 2.2); // 前光
|
||||
this.createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
|
||||
this.createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
|
||||
this.createSoftbox(7.5, 2.8, 0, 2.0);
|
||||
this.createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
|
||||
}
|
||||
// 更新工作室光位置
|
||||
updateStudioLighting(){
|
||||
this.camera.getWorldDirection(this.camDir);
|
||||
this.studioLights.forEach(item => {
|
||||
// 使用 applyQuaternion 同步灯光位置
|
||||
this.v1.copy(item.offset).applyQuaternion(this.camera.quaternion);
|
||||
item.light.position.copy(this.controls.target).add(this.v1);
|
||||
item.light.lookAt(this.controls.target);
|
||||
|
||||
// 动态背光逻辑
|
||||
if (item.offset.z < -2) {
|
||||
this.v1.copy(item.light.position).sub(this.controls.target).normalize();
|
||||
const dot = this.v1.dot(this.camDir);
|
||||
const factor = THREE.MathUtils.lerp(CONFIG.backlightBoost.min, CONFIG.backlightBoost.max, Math.max(0, 1 - dot));
|
||||
item.light.intensity = item.baseIntensity * factor;
|
||||
}
|
||||
});
|
||||
}
|
||||
//动态设置曝光
|
||||
updateImagePipeline() {
|
||||
let exposure = 1.0;
|
||||
this.camera.getWorldDirection(this.camForward);
|
||||
this.camToTarget.copy(this.controls.target).sub(this.camera.position).normalize();
|
||||
const facing = Math.abs(this.camForward.dot(this.camToTarget));
|
||||
const targetExposure = THREE.MathUtils.lerp(1.25, 0.90, facing);
|
||||
exposure = THREE.MathUtils.lerp(exposure, targetExposure, 0.08);
|
||||
this.renderer.toneMappingExposure = CONFIG.exposureBase * exposure;
|
||||
}
|
||||
|
||||
async setModel(modelUrl,load){
|
||||
await new Promise((resolve, reject) => {
|
||||
const drac = new DRACOLoader()
|
||||
drac.setDecoderPath('/draco/')
|
||||
const loader = new GLTFLoader().setDRACOLoader(drac);
|
||||
|
||||
if (this.currentModel) {
|
||||
this.scene.remove(this.currentModel);
|
||||
this.dispose(this.currentModel); // 核心优化:释放显存
|
||||
}
|
||||
|
||||
loader.load(modelUrl,
|
||||
(gltf) => {
|
||||
this.currentModel = gltf.scene;
|
||||
|
||||
// 遍历模型:增强材质表现
|
||||
this.currentModel.traverse(child => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
if (child.material) {
|
||||
child.material.envMapIntensity = 1.2; // 增强 HDRI 反射强度
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const box = new THREE.Box3().setFromObject(this.currentModel);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
// this.currentModel.position.sub(center);
|
||||
this.scene.add(this.currentModel);
|
||||
this.modelInfo = this.getModelInfo(this.currentModel)
|
||||
// 根据模型大小调整相机位置
|
||||
this.adjustCameraToModel(this.modelInfo);
|
||||
|
||||
// 根据模型大小创建自适应灯光
|
||||
this.createAdaptiveSoftboxes(this.modelInfo);
|
||||
resolve('')
|
||||
},
|
||||
(xhr: any) => { // 加载进度回调
|
||||
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
|
||||
load.value.progress = percent
|
||||
console.log('模型加载进度:', percent);
|
||||
},
|
||||
(error: any) => { // 加载失败回调
|
||||
console.error('模型加载失败:', error);
|
||||
resolve('')
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
operation(){
|
||||
let this_ = this
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
this.controls.update();
|
||||
this.updateStudioLighting();
|
||||
this.updateImagePipeline();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
animate();
|
||||
}
|
||||
|
||||
// 释放模型资源
|
||||
dispose(obj){
|
||||
if (!obj) return;
|
||||
obj.traverse(node => {
|
||||
if (node.isMesh) {
|
||||
if (node.geometry) node.geometry.dispose();
|
||||
if (node.material) {
|
||||
if (Array.isArray(node.material)) {
|
||||
node.material.forEach(m => m.dispose());
|
||||
} else {
|
||||
node.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
exportAsImage(){
|
||||
return this.renderer.domElement.toDataURL('image/png');
|
||||
}
|
||||
}
|
||||
@@ -3,291 +3,349 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
||||
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
|
||||
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
|
||||
import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib.js';
|
||||
import hdri from '@/assets/images/three/hdri.hdr'
|
||||
|
||||
export const initThree = async (threeDom)=>{
|
||||
const scene = new THREE.Scene();
|
||||
const group = new THREE.Group()
|
||||
scene.add(group)
|
||||
const studioLights = []
|
||||
//创建相机对象
|
||||
// this.camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
|
||||
const camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 1000);
|
||||
camera.position.set(0, 1.5, 5);
|
||||
camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
|
||||
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
|
||||
/**
|
||||
* 创建渲染器对象
|
||||
*/
|
||||
interface ModelInfo {
|
||||
box: THREE.Box3;
|
||||
center: THREE.Vector3;
|
||||
size: THREE.Vector3;
|
||||
maxSize: number;
|
||||
}
|
||||
const CONFIG = {
|
||||
hdriIntensity: 7.4,
|
||||
exposureBase: 0.92,
|
||||
backlightBoost: { min: 2.1, max: 4.8 }, // 背光增强系数
|
||||
hdriUrl: hdri,
|
||||
};
|
||||
export class ThreeManager {
|
||||
threeDom: HTMLElement;
|
||||
scene: THREE.Scene;//场景对象
|
||||
camera: THREE.PerspectiveCamera;//相机对象
|
||||
renderer: THREE.WebGLRenderer;//渲染器对象
|
||||
controls: OrbitControls;//轨道控制器对象
|
||||
|
||||
|
||||
const width = threeDom.offsetWidth; //窗口宽度
|
||||
const height = threeDom.offsetHeight; //窗口高度
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
logarithmicDepthBuffer: true,//深度缓存 防止模型闪烁重影
|
||||
});
|
||||
pointLight: THREE.AmbientLight;//环境光对象
|
||||
studioLights: any;//工作室光对象数组
|
||||
v1: THREE.Vector3;//相机前向向量
|
||||
camDir: THREE.Vector3;//相机前向向量
|
||||
camForward: THREE.Vector3;//相机前向向量
|
||||
camToTarget: THREE.Vector3;//相机目标向量
|
||||
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;//设置色调
|
||||
renderer.toneMappingExposure = 1.3;
|
||||
currentModel: any;//当前模型对象
|
||||
animate: any;//动画对象
|
||||
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
renderer.setSize(width, height); //设置渲染区域尺寸
|
||||
renderer.setClearColor(0xffffff, 1); //设置背景颜色
|
||||
threeDom.innerHTML = '';
|
||||
threeDom.appendChild(renderer.domElement);
|
||||
modelInfo: ModelInfo;//模型信息对象
|
||||
defaultSoftboxPositions: Array<{ x: number; y: number; z: number; intensity: number; w: number; h: number }> = [
|
||||
{ x: 0, y: 5.8, z: 3.5, intensity: 3.5, w: 6, h: 4.8 }, // 主光
|
||||
{ x: 0, y: 2.8, z: 7.5, intensity: 2.2, w: 5, h: 4 }, // 前光
|
||||
{ x: 0, y: 2.8, z: -7.5, intensity: 6.5, w: 5, h: 4 }, // 背光
|
||||
{ x: -7.5, y: 2.8, z: 0, intensity: 10.8, w: 5, h: 4 }, // 侧光
|
||||
{ x: 7.5, y: 2.8, z: 0, intensity: 2.0, w: 5, h: 4 }, // 侧光
|
||||
{ x: 0, y: -2.2, z: 3, intensity: 0.8, w: 9, h: 4 } // 地面反光板
|
||||
];
|
||||
constructor(threeDom: HTMLElement,config:any = {}){
|
||||
|
||||
// 设置渲染器大小
|
||||
this.threeDom = threeDom;
|
||||
this.studioLights = []
|
||||
//创建场景
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.background = new THREE.Color(0xffffff);
|
||||
//创建相机
|
||||
this.camera = new THREE.PerspectiveCamera(45, threeDom.offsetWidth / threeDom.offsetHeight, 0.1, 10000);
|
||||
this.camera.position.set(0, 1.5, 6); //设置相机位置
|
||||
|
||||
this.v1 = new THREE.Vector3();
|
||||
this.camDir = new THREE.Vector3();
|
||||
this.camForward = new THREE.Vector3();
|
||||
this.camToTarget = new THREE.Vector3();
|
||||
//设置渲染器
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance", preserveDrawingBuffer: true });
|
||||
//设置环境光
|
||||
this.scene.add(new THREE.AmbientLight(0xffffff, 0.15));
|
||||
|
||||
// 关键优化:物理光照与色彩管理
|
||||
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = CONFIG.exposureBase;
|
||||
this.renderer.physicallyCorrectLights = true; // 开启物理光照模式,光衰减更真实
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setSize(threeDom.offsetWidth, threeDom.offsetHeight); //设置渲染区域尺寸
|
||||
this.renderer.setClearColor(0xffffff, 1); //设置背景颜色
|
||||
threeDom.innerHTML = '';
|
||||
threeDom.appendChild(this.renderer.domElement);
|
||||
|
||||
RectAreaLightUniformsLib.init();
|
||||
//设置轨道控制器
|
||||
this.controls = new OrbitControls(this.camera,this.renderer.domElement)//监听鼠标、键盘事件;
|
||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||
this.controls.mouseButtons = {
|
||||
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转:ROTATE)
|
||||
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转:ROTATE)
|
||||
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
|
||||
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
||||
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
||||
}
|
||||
this.controls.enableDamping = true;
|
||||
|
||||
const controls = new OrbitControls(camera,renderer.domElement)//监听鼠标、键盘事件;
|
||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||
controls.mouseButtons = {
|
||||
// LEFT:THREE.MOUSE.PAN, // 左键 拖动(默认旋转:ROTATE)
|
||||
LEFT:THREE.MOUSE.ROTATE, // 左键 拖动(默认旋转:ROTATE)
|
||||
MIDDLE:THREE.MOUSE.DOLLY, // 滑轮 缩放
|
||||
RIGHT:THREE.MOUSE.PAN // 右键 旋转(默认拖动:PAN)
|
||||
// RIGHT:THREE.MOUSE.ROTAafTE // 右键 旋转(默认拖动:PAN)
|
||||
}
|
||||
|
||||
//使用hdri文件
|
||||
try {
|
||||
const rgbeLoader = new RGBELoader()
|
||||
const hdrTexture = await rgbeLoader.loadAsync(hdri)
|
||||
hdrTexture.mapping = THREE.EquirectangularMapping
|
||||
/**
|
||||
* 根据模型大小计算自适应配置
|
||||
*/
|
||||
calculateAdaptiveConfig(modelInfo: ModelInfo) {
|
||||
const { size, maxSize, center } = modelInfo;
|
||||
// 基础距离系数(可根据需要调整)
|
||||
let distanceFactor = 1.5;
|
||||
// 根据模型形状调整距离系数
|
||||
const aspectRatio = size.x / size.y;
|
||||
if (aspectRatio > 2) {
|
||||
// 扁平模型,拉远一点
|
||||
distanceFactor = 3.0;
|
||||
} else if (aspectRatio < 0.5) {
|
||||
// 高瘦模型,稍微拉近
|
||||
distanceFactor = 2.0;
|
||||
}
|
||||
// 计算相机距离
|
||||
const fov = this.camera.fov * (Math.PI / 180);
|
||||
const cameraDistance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
|
||||
// 灯光缩放系数
|
||||
const lightScale = Math.max(0.5, Math.min(2.0, maxSize / 2.5));
|
||||
// 强度缩放系数(模型越大,灯光需要越强)
|
||||
const intensityScale = Math.max(0.6, Math.min(2.5, maxSize / 2));
|
||||
|
||||
console.log('自适应配置:', {
|
||||
相机距离: cameraDistance,
|
||||
灯光缩放: lightScale,
|
||||
强度缩放: intensityScale,
|
||||
目标中心: center
|
||||
});
|
||||
|
||||
return {
|
||||
cameraDistance,
|
||||
lightScale,
|
||||
intensityScale,
|
||||
targetCenter: center.clone()
|
||||
};
|
||||
}
|
||||
/**
|
||||
* 获取模型信息(包围盒、中心点、尺寸等)
|
||||
*/
|
||||
getModelInfo(model: THREE.Object3D): ModelInfo {
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxSize = Math.max(size.x, size.y, size.z);
|
||||
|
||||
console.log('模型信息:', {
|
||||
中心点: center,
|
||||
尺寸: { x: size.x, y: size.y, z: size.z },
|
||||
最大尺寸: maxSize
|
||||
});
|
||||
|
||||
return { box, center, size, maxSize };
|
||||
}
|
||||
/**
|
||||
* 根据模型大小自适应创建柔光箱灯光
|
||||
*/
|
||||
createAdaptiveSoftboxes(modelInfo: ModelInfo) {
|
||||
const { lightScale, intensityScale, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||
// 清除现有灯光
|
||||
this.studioLights.forEach(item => {
|
||||
this.scene.remove(item.light);
|
||||
});
|
||||
this.studioLights = [];
|
||||
|
||||
// 设置环境贴图(影响材质反射)
|
||||
scene.environment = hdrTexture
|
||||
// 可选:同时设置为背景
|
||||
scene.background = hdrTexture
|
||||
// 根据模型大小创建自适应灯光
|
||||
this.defaultSoftboxPositions.forEach(pos => {
|
||||
const scaledX = pos.x * lightScale;
|
||||
const scaledY = pos.y * lightScale;
|
||||
const scaledZ = pos.z * lightScale;
|
||||
const scaledW = pos.w * lightScale;
|
||||
const scaledH = pos.h * lightScale;
|
||||
const scaledIntensity = pos.intensity * intensityScale;
|
||||
|
||||
const light = new THREE.RectAreaLight(0xffffff, scaledIntensity, scaledW, scaledH);
|
||||
light.position.set(
|
||||
targetCenter.x + scaledX,
|
||||
targetCenter.y + scaledY,
|
||||
targetCenter.z + scaledZ
|
||||
);
|
||||
light.lookAt(targetCenter);
|
||||
this.scene.add(light);
|
||||
|
||||
this.studioLights.push({
|
||||
light,
|
||||
offset: new THREE.Vector3(scaledX, scaledY, scaledZ),
|
||||
baseIntensity: scaledIntensity
|
||||
});
|
||||
});
|
||||
console.log('自适应灯光已创建,缩放系数:', lightScale, '强度系数:', intensityScale);
|
||||
}
|
||||
/**
|
||||
* 根据模型大小调整相机位置
|
||||
*/
|
||||
adjustCameraToModel(modelInfo: ModelInfo) {
|
||||
const { cameraDistance, targetCenter } = this.calculateAdaptiveConfig(modelInfo);
|
||||
// 正面俯视角度:相机在模型正前方,稍高于模型中心
|
||||
// X 轴:模型中心(正面视角,不左右偏移)
|
||||
// Y 轴:相机高度 = 模型中心高度 + 模型高度的 0.3 倍(俯视效果)
|
||||
// Z 轴:相机距离 = 根据模型大小计算的距离
|
||||
const x = targetCenter.x;
|
||||
const y = targetCenter.y + modelInfo.size.y * 0.3; // 相机高度为模型中心的 0.3 倍
|
||||
const z = targetCenter.z + cameraDistance;
|
||||
|
||||
console.log('HDR 环境贴图加载成功:', hdri)
|
||||
} catch (error) {
|
||||
console.error('HDR 加载失败:', error)
|
||||
// 降级方案:使用环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
||||
scene.add(ambientLight)
|
||||
this.camera.position.set(x, y, z);
|
||||
this.controls.target.copy(targetCenter);
|
||||
this.controls.update();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 光源设置
|
||||
*/
|
||||
//点光源
|
||||
/**
|
||||
* AmbientLight 环境光
|
||||
PointLight 点光源
|
||||
DirectionalLight 平行光,比如太阳光
|
||||
SpotLight 聚光源
|
||||
*/
|
||||
|
||||
|
||||
//设置环境光全亮
|
||||
//环境光
|
||||
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
||||
// scene.add(pointLight);
|
||||
|
||||
function createSoftbox(x, y, z, intensity, w = 5, h = 4) {
|
||||
createSoftbox(x, y, z, intensity, w = 5, h = 4) {
|
||||
const light = new THREE.RectAreaLight(0xffffff, intensity, w, h);
|
||||
light.position.set(x, y, z);
|
||||
light.lookAt(0, 0, 0);
|
||||
scene.add(light);
|
||||
this.scene.add(light);
|
||||
// 存储 offset 向量副本,避免后续重复创建
|
||||
studioLights.push({
|
||||
this.studioLights.push({
|
||||
light,
|
||||
offset: new THREE.Vector3(x, y, z),
|
||||
baseIntensity: intensity
|
||||
});
|
||||
}
|
||||
// 灯光布局优化
|
||||
createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
|
||||
createSoftbox(0, 2.8, 7.5, 2.2); // 前光
|
||||
createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
|
||||
createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
|
||||
createSoftbox(7.5, 2.8, 0, 2.0);
|
||||
createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
|
||||
|
||||
// const pointLight = new THREE.AmbientLight(0xffffff,1.0);
|
||||
// pointLight.intensity = 1.2//光源强度
|
||||
// pointLight.castShadow = true//开启阴影
|
||||
// pointLight.shadow.mapSize = new THREE.Vector2(width, height)
|
||||
// scene.add(pointLight); //点光源添加到场景中
|
||||
// pointLight.position.set(400, 200, 300); //点光源位置
|
||||
// pointLight.position.y = 100;
|
||||
// pointLight.position.z = 50;
|
||||
// pointLight.position.x = 100;
|
||||
|
||||
// let floorGeometry = new THREE.PlaneGeometry(5000, 3000)//地板大小
|
||||
// let floorMaterial = new THREE.MeshPhongMaterial({ color: "#7e7ab0", })
|
||||
// let floorMesh = new THREE.Mesh(floorGeometry, floorMaterial);
|
||||
// floorMesh.rotation.x = -0.5 * Math.PI;
|
||||
// floorMesh.receiveShadow = true;
|
||||
// floorMesh.position.y = -0.001;
|
||||
// scene.add(floorMesh);
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
|
||||
scene.background = new THREE.Color("#fff");
|
||||
return {scene,group,camera,renderer,controls,pointLight,studioLights}
|
||||
}
|
||||
export const clearModel = (group,scene)=>{
|
||||
const oldGroup:any = group.value;
|
||||
group.value = new THREE.Group();
|
||||
scene.value.add(group.value);
|
||||
scene.value.remove(oldGroup);
|
||||
}
|
||||
|
||||
// 计算模型包围盒
|
||||
export const getModelInfo = (model: THREE.Object3D) => {
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
const size = box.getSize(new THREE.Vector3());
|
||||
const maxSize = Math.max(size.x, size.y, size.z);
|
||||
return {
|
||||
box,
|
||||
center,
|
||||
size,
|
||||
maxSize
|
||||
};
|
||||
};
|
||||
// 根据模型信息计算相机位置
|
||||
export const calculateCameraPosition = (
|
||||
modelInfo: ReturnType<typeof getModelInfo>,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
options?: {
|
||||
distanceFactor?: number; // 距离系数,默认1.5
|
||||
heightFactor?: number; // 高度偏移系数,默认0.3
|
||||
angle?: number; // 观察角度,默认正前方
|
||||
}
|
||||
) => {
|
||||
const { center, size, maxSize } = modelInfo;
|
||||
const fov = camera.fov * (Math.PI / 180);
|
||||
|
||||
const distanceFactor = options?.distanceFactor ?? 1.5;
|
||||
const heightFactor = options?.heightFactor ?? 0.3;
|
||||
|
||||
// 计算合适的相机距离
|
||||
const distance = (maxSize / 2) / Math.tan(fov / 2) * distanceFactor;
|
||||
|
||||
// 根据角度计算相机位置
|
||||
const angle = options?.angle ?? 0; // 0表示正前方
|
||||
|
||||
return {
|
||||
position: new THREE.Vector3(
|
||||
center.x + distance * Math.sin(angle),
|
||||
center.y + size.y * heightFactor,
|
||||
center.z + distance * Math.cos(angle)
|
||||
),
|
||||
target: center.clone(),
|
||||
distance,
|
||||
center,
|
||||
size
|
||||
};
|
||||
};
|
||||
// 根据模型信息计算光源位置
|
||||
export const calculateLightPosition = (
|
||||
modelInfo: ReturnType<typeof getModelInfo>,
|
||||
options?: {
|
||||
xFactor?: number; // X轴偏移系数,默认0.5
|
||||
yFactor?: number; // Y轴偏移系数,默认0.8
|
||||
zFactor?: number; // Z轴偏移系数,默认0.5
|
||||
}
|
||||
) => {
|
||||
const { center, size } = modelInfo;
|
||||
|
||||
const xFactor = options?.xFactor ?? 0.5;
|
||||
const yFactor = options?.yFactor ?? 0.8;
|
||||
const zFactor = options?.zFactor ?? 0.5;
|
||||
|
||||
return new THREE.Vector3(
|
||||
center.x + size.x * xFactor,
|
||||
center.y + size.y * yFactor,
|
||||
center.z + size.z * zFactor
|
||||
);
|
||||
};
|
||||
export const addModel = async (
|
||||
url: any,
|
||||
controls: OrbitControls,
|
||||
camera: THREE.PerspectiveCamera,
|
||||
pointLight: THREE.DirectionalLight,
|
||||
group: THREE.Group,
|
||||
load: any
|
||||
) => {
|
||||
await new Promise((resolve, reject) => {
|
||||
const fbxLoader = new GLTFLoader();
|
||||
const drac = new DRACOLoader()
|
||||
drac.setDecoderPath('/draco/')
|
||||
fbxLoader.setDRACOLoader(drac)
|
||||
|
||||
fbxLoader.load(url,
|
||||
(obj: any) => {
|
||||
const scene = obj.scene;
|
||||
scene.traverse((child: any) => {
|
||||
if (child.isMesh) {
|
||||
// 如果是基础材质,转换为标准材质
|
||||
if (child.material instanceof THREE.MeshBasicMaterial) {
|
||||
const oldMat = child.material;
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
map: oldMat.map,
|
||||
color: oldMat.color,
|
||||
roughness: 0.4,
|
||||
metalness: 0
|
||||
});
|
||||
}
|
||||
// 如果是标准材质,调整粗糙度
|
||||
else if (child.material instanceof THREE.MeshStandardMaterial) {
|
||||
child.material.roughness = 0.4;
|
||||
child.material.metalness = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
// 获取模型信息
|
||||
const modelInfo = getModelInfo(scene);
|
||||
const { position: cameraPos, target } = calculateCameraPosition(
|
||||
modelInfo,
|
||||
camera.value,
|
||||
{
|
||||
distanceFactor: 1.5,
|
||||
heightFactor: 0.3,
|
||||
angle: 0 // 正前方
|
||||
}
|
||||
);
|
||||
// 设置相机位置
|
||||
camera.value.position.copy(cameraPos);
|
||||
// 设置控制器目标点
|
||||
controls.value.target.copy(target);
|
||||
// 计算并设置光源位置
|
||||
const lightPos = calculateLightPosition(modelInfo);
|
||||
pointLight.value.position.copy(lightPos);
|
||||
// 将模型添加到场景
|
||||
group.value.add(scene);
|
||||
// 可选:将模型信息存储在模型上,方便后续使用
|
||||
(scene as any).userData.modelInfo = modelInfo;
|
||||
|
||||
resolve('')
|
||||
},
|
||||
(xhr: any) => { // 加载进度回调
|
||||
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
|
||||
load.value.progress = percent
|
||||
console.log('模型加载进度:', percent);
|
||||
},
|
||||
(error: any) => { // 加载失败回调
|
||||
console.error('模型加载失败:', error);
|
||||
reject('')
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
// 导出当前视图为图片
|
||||
export const exportAsImage = (renderer, camera, scene, filename = 'model.png')=>{
|
||||
// 渲染当前场景
|
||||
renderer.render(scene, camera)
|
||||
async setHDRI(){
|
||||
const rgbeLoader = new RGBELoader()
|
||||
await rgbeLoader.load(CONFIG.hdriUrl, (texture)=>{
|
||||
texture.mapping = THREE.EquirectangularReflectionMapping;
|
||||
const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
|
||||
const envMap = pmremGenerator.fromEquirectangular(texture).texture;
|
||||
|
||||
// 获取 canvas 数据
|
||||
const canvas = renderer.domElement
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
return dataURL
|
||||
this.scene.environment = envMap;
|
||||
this.scene.environmentIntensity = CONFIG.hdriIntensity;
|
||||
|
||||
pmremGenerator.dispose();
|
||||
texture.dispose();
|
||||
console.log('✅ HDRI Loaded');
|
||||
});
|
||||
this.createSoftbox(0, 5.8, 3.5, 3.5, 6, 4.8); // 主光
|
||||
this.createSoftbox(0, 2.8, 7.5, 2.2); // 前光
|
||||
this.createSoftbox(0, 2.8, -7.5, 6.5); // 背光 (增强)
|
||||
this.createSoftbox(-7.5, 2.8, 0, 10.8); // 侧光
|
||||
this.createSoftbox(7.5, 2.8, 0, 2.0);
|
||||
this.createSoftbox(0, -2.2, 3, 0.8, 9, 4); // 地面反光板
|
||||
}
|
||||
// 更新工作室光位置
|
||||
updateStudioLighting(){
|
||||
this.camera.getWorldDirection(this.camDir);
|
||||
this.studioLights.forEach(item => {
|
||||
// 使用 applyQuaternion 同步灯光位置
|
||||
this.v1.copy(item.offset).applyQuaternion(this.camera.quaternion);
|
||||
item.light.position.copy(this.controls.target).add(this.v1);
|
||||
item.light.lookAt(this.controls.target);
|
||||
|
||||
// 动态背光逻辑
|
||||
if (item.offset.z < -2) {
|
||||
this.v1.copy(item.light.position).sub(this.controls.target).normalize();
|
||||
const dot = this.v1.dot(this.camDir);
|
||||
const factor = THREE.MathUtils.lerp(CONFIG.backlightBoost.min, CONFIG.backlightBoost.max, Math.max(0, 1 - dot));
|
||||
item.light.intensity = item.baseIntensity * factor;
|
||||
}
|
||||
});
|
||||
}
|
||||
//动态设置曝光
|
||||
updateImagePipeline() {
|
||||
let exposure = 1.0;
|
||||
this.camera.getWorldDirection(this.camForward);
|
||||
this.camToTarget.copy(this.controls.target).sub(this.camera.position).normalize();
|
||||
const facing = Math.abs(this.camForward.dot(this.camToTarget));
|
||||
const targetExposure = THREE.MathUtils.lerp(1.25, 0.90, facing);
|
||||
exposure = THREE.MathUtils.lerp(exposure, targetExposure, 0.08);
|
||||
this.renderer.toneMappingExposure = CONFIG.exposureBase * exposure;
|
||||
}
|
||||
|
||||
async setModel(modelUrl,load){
|
||||
await new Promise((resolve, reject) => {
|
||||
const drac = new DRACOLoader()
|
||||
drac.setDecoderPath('/draco/')
|
||||
const loader = new GLTFLoader().setDRACOLoader(drac);
|
||||
|
||||
if (this.currentModel) {
|
||||
this.scene.remove(this.currentModel);
|
||||
this.dispose(this.currentModel); // 核心优化:释放显存
|
||||
}
|
||||
|
||||
loader.load(modelUrl,
|
||||
(gltf) => {
|
||||
this.currentModel = gltf.scene;
|
||||
|
||||
// 遍历模型:增强材质表现
|
||||
this.currentModel.traverse(child => {
|
||||
if (child.isMesh) {
|
||||
child.castShadow = true;
|
||||
child.receiveShadow = true;
|
||||
if (child.material) {
|
||||
child.material.envMapIntensity = 1.2; // 增强 HDRI 反射强度
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const box = new THREE.Box3().setFromObject(this.currentModel);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
// this.currentModel.position.sub(center);
|
||||
this.scene.add(this.currentModel);
|
||||
this.modelInfo = this.getModelInfo(this.currentModel)
|
||||
// 根据模型大小调整相机位置
|
||||
this.adjustCameraToModel(this.modelInfo);
|
||||
|
||||
// 根据模型大小创建自适应灯光
|
||||
this.createAdaptiveSoftboxes(this.modelInfo);
|
||||
resolve('')
|
||||
},
|
||||
(xhr: any) => { // 加载进度回调
|
||||
const percent = xhr.total == 0 ? 100 : (xhr.loaded / xhr.total * 100).toFixed(2);
|
||||
load.value.progress = percent
|
||||
console.log('模型加载进度:', percent);
|
||||
},
|
||||
(error: any) => { // 加载失败回调
|
||||
console.error('模型加载失败:', error);
|
||||
resolve('')
|
||||
}
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
operation(){
|
||||
let this_ = this
|
||||
const animate = () => {
|
||||
requestAnimationFrame(animate);
|
||||
this.controls.update();
|
||||
this.updateStudioLighting();
|
||||
this.updateImagePipeline();
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
animate();
|
||||
}
|
||||
|
||||
// 释放模型资源
|
||||
dispose(obj){
|
||||
if (!obj) return;
|
||||
obj.traverse(node => {
|
||||
if (node.isMesh) {
|
||||
if (node.geometry) node.geometry.dispose();
|
||||
if (node.material) {
|
||||
if (Array.isArray(node.material)) {
|
||||
node.material.forEach(m => m.dispose());
|
||||
} else {
|
||||
node.material.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
exportAsImage(){
|
||||
return this.renderer.domElement.toDataURL('image/png');
|
||||
}
|
||||
}
|
||||
@@ -258,6 +258,8 @@
|
||||
nodeManager.createResultNode({
|
||||
data: {
|
||||
superiorID_: captureData.nodeId,
|
||||
superiorNodeType: captureData.nodeType,
|
||||
superiorGenerateImg: captureData.superiorGenerateImg,
|
||||
data: {
|
||||
selectable: false,
|
||||
imageProcessTasks: [
|
||||
|
||||
@@ -9,6 +9,7 @@ interface NodeData {
|
||||
superiorID?: string// 上级节点ID,有连接线
|
||||
superiorID_?: string// 上级节点ID,没有连接线
|
||||
superiorNodeType?: string// 上级节点类型
|
||||
superiorGenerateImg?: string// 上级生成节点图片
|
||||
disableDelete?: boolean// 是否禁用删除
|
||||
disableCopy?: boolean// 是否禁用复制
|
||||
createIndexPosition?: number// 创建索引位置
|
||||
@@ -97,6 +98,7 @@ export class NodeManager {
|
||||
}
|
||||
/** 创建结果节点 */
|
||||
createResultNode(options?: NodeOptions) {
|
||||
console.log(options)
|
||||
const options_ = {
|
||||
...(options ? options : {}),
|
||||
component: NODE_COMPONENT.RESULT_IMAGE,
|
||||
|
||||
@@ -24,6 +24,7 @@ export const NODE_DATATYPE = {
|
||||
RESULT_IMAGE: 'result-image',
|
||||
CARDS_SELECT: 'cards-select',
|
||||
TO_REAL_STYLE: 'to-real-style',
|
||||
TO_REAL_VARIANTS: 'to-real-variants',
|
||||
SURFACE_EDIT: 'surface-edit',
|
||||
CANVAS_MODE: 'canvas-mode',
|
||||
Fast_MODE: 'fast-mode',
|
||||
@@ -39,6 +40,7 @@ export const NODE_DATATIER = {
|
||||
RESULT_IMAGE: 0,
|
||||
CARDS_SELECT: 0,
|
||||
TO_REAL_STYLE: 1,
|
||||
TO_REAL_VARIANTS: 1,
|
||||
SURFACE_EDIT: 1,
|
||||
CANVAS_MODE: 1,
|
||||
Fast_MODE: 1,
|
||||
|
||||
Reference in New Issue
Block a user