3d出来后的卡片交互修改

This commit is contained in:
X1627315083@163.com
2026-04-14 13:03:18 +08:00
parent b16b41f44e
commit e3593f9e96
15 changed files with 499 additions and 621 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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)
}

View File

@@ -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(()=>{

View File

@@ -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({
//})

View File

@@ -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');
}
}

View File

@@ -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');
}
}

View File

@@ -258,6 +258,8 @@
nodeManager.createResultNode({
data: {
superiorID_: captureData.nodeId,
superiorNodeType: captureData.nodeType,
superiorGenerateImg: captureData.superiorGenerateImg,
data: {
selectable: false,
imageProcessTasks: [

View File

@@ -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,

View File

@@ -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,