Merge branch 'main' of http://18.167.251.121:10003/aidlab/FiDA_Front
This commit is contained in:
@@ -176,6 +176,7 @@
|
||||
]
|
||||
let tier = (tritList.includes(currentComponent.value.tier) && typeList.includes(currentComponent.value.type))?currentComponent.value.tier - 1:currentComponent.value.tier
|
||||
if(NODE_DATATYPE.TO_REAL_STYLE == currentComponent.value.type && false){
|
||||
//一个结果节点里面多个子节点
|
||||
let imageProcessTasks = taskList
|
||||
nodeManager.createResultNode({
|
||||
data: {
|
||||
|
||||
@@ -5,20 +5,28 @@
|
||||
<div class="image">
|
||||
<img :src="data.url" alt="">
|
||||
</div>
|
||||
<p class="label">Mode</p>
|
||||
<my-select v-model="data.mode" :list="modeList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, inject, useAttrs, computed } from 'vue'
|
||||
import { reactive, inject, useAttrs, computed,ref } from 'vue'
|
||||
import uploadFile from '../../tools/upload-file.vue'
|
||||
const attrs = useAttrs()
|
||||
const stateManager = inject('stateManager') as any
|
||||
const data = reactive({
|
||||
url: computed(()=>stateManager.getSuperiorNodeImage(attrs.node?.data?.superiorID)),
|
||||
mode: 'Advanced',
|
||||
})
|
||||
const modeList = ref([
|
||||
{ value: 'Advanced', label: 'Advanced' },
|
||||
{ value: 'Normal', label: 'Normal' }
|
||||
])
|
||||
const getApiData = ()=>{
|
||||
return {
|
||||
imageUrls: [data.url],
|
||||
mode: data.mode,
|
||||
}
|
||||
}
|
||||
defineExpose({ data,getApiData })
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
])
|
||||
const onPreview = (item: any) => {
|
||||
if(data.superiorNodeType == NODE_DATATYPE.TO_3D_MODEL){
|
||||
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj})
|
||||
openThreeModelPreview({glbPath:item?.glbPath,glbInfoObj:item?.glbInfoObj,nodeId:props.node.id})
|
||||
}else{
|
||||
openImagePreview(item.url)
|
||||
}
|
||||
@@ -379,6 +379,7 @@
|
||||
width: 100%;
|
||||
// height: 140px;
|
||||
height: 100%;
|
||||
min-height: 140px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -7,13 +7,18 @@ const props = defineProps({
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const emit = defineEmits([
|
||||
'captureView'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
const onDownload = () => {
|
||||
if(props?.config?.glbPath)downloadImage(props?.config?.glbPath, 'model.glb')
|
||||
}
|
||||
const captureView = ()=>{
|
||||
emit('captureView')
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
@@ -70,14 +75,22 @@ const {} = toRefs(data);
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div>
|
||||
<div class="captureView" @click="captureView">
|
||||
<div class="icon">
|
||||
<svgIcon name="captureView" size="12" />
|
||||
</div>
|
||||
{{ $t('threeModel.captureView') }}
|
||||
</div>
|
||||
<!-- <div class="download" @click="onDownload">{{ $t('threeModel.download') }}</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.modalDetail{
|
||||
width: 100%;
|
||||
width: 22rem;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .title{
|
||||
margin: 2.4rem 0;
|
||||
font-weight: 500;
|
||||
@@ -85,9 +98,9 @@ const {} = toRefs(data);
|
||||
line-height: 2.7rem;
|
||||
color: #000;
|
||||
}
|
||||
> .captureView ,
|
||||
> .download{
|
||||
margin-left: 4.2rem;
|
||||
margin-top: 24.8rem;
|
||||
line-height: 3rem;
|
||||
width: 20rem;
|
||||
border-radius: 1.5rem;
|
||||
@@ -99,6 +112,15 @@ const {} = toRefs(data);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
> .captureView{
|
||||
margin-top: 6.4rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: .7rem;
|
||||
}
|
||||
> .download{
|
||||
margin-top: 15.4rem;
|
||||
}
|
||||
> .detail{
|
||||
> .name{
|
||||
> .title{
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import threeGlb from '@/assets/images/three/sample.glb'
|
||||
import { uploadImage } from '@/api/upload'
|
||||
import { base64Tofile } from '@/components/Canvas/tools/tools'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import model from './model.vue'
|
||||
import detail from './detail.vue'
|
||||
@@ -11,13 +14,29 @@ const props = defineProps({
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const emit = defineEmits([
|
||||
'captureView'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
|
||||
const modelRef = ref(null)
|
||||
const captureView = async ()=>{
|
||||
let url = modelRef.value.captureView()
|
||||
|
||||
const file = base64Tofile(url, 'canvas.png')
|
||||
const formData = new FormData()
|
||||
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
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
// modelRef.value.open(threeGlb)
|
||||
if(props?.currentData?.glbPath)modelRef.value.open(props?.currentData?.glbPath)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
@@ -31,7 +50,7 @@ const {} = toRefs(data);
|
||||
<model ref="modelRef" />
|
||||
</div>
|
||||
<div class="detailBox">
|
||||
<detail ref="detailRef" :config="currentData" />
|
||||
<detail ref="detailRef" @captureView="captureView" :config="currentData" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -48,8 +67,10 @@ const {} = toRefs(data);
|
||||
width: 65.5rem;
|
||||
}
|
||||
> .detailBox{
|
||||
width: 22rem;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import gsap from 'gsap';
|
||||
import * as THREE from 'three';
|
||||
import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } from './threeTool'
|
||||
import { ThreeManager } from './threeTool copy'
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
@@ -12,154 +12,37 @@ import { initThree,clearModel,addModel,getModelInfo,calculateCameraPosition } fr
|
||||
//])
|
||||
|
||||
const threeDom = ref()//threeDom元素
|
||||
|
||||
let scene = shallowRef()//场景
|
||||
let group = shallowRef()//组
|
||||
let camera = shallowRef()//相机
|
||||
let renderer = shallowRef()//渲染器
|
||||
let pointLight = shallowRef();//光
|
||||
let ambient = shallowRef()//环境光
|
||||
let controls = shallowRef()//监听鼠标、键盘事件
|
||||
|
||||
const animationId = ref(null);
|
||||
//加载进度
|
||||
const load = ref({
|
||||
state:false,
|
||||
progress:0
|
||||
})
|
||||
// const textureLoader = ref(new THREE.TextureLoader())//材质
|
||||
const init = () => {
|
||||
//初始化threejs
|
||||
if (scene.value) return
|
||||
const initResult = initThree(threeDom.value)
|
||||
scene.value = initResult.scene
|
||||
group.value = initResult.group
|
||||
camera.value = initResult.camera
|
||||
renderer.value = initResult.renderer
|
||||
ambient.value = initResult.ambient
|
||||
controls.value = initResult.controls
|
||||
pointLight.value = initResult.pointLight
|
||||
let threeModel = null
|
||||
|
||||
threeDom.value.ondblclick = (event:any)=>{
|
||||
let intersects = openModel(event);
|
||||
if(!intersects || intersects.length<=0) return
|
||||
|
||||
const clickedObject = intersects[0].object;
|
||||
const modelInfo = getModelInfo(clickedObject);
|
||||
const { size } = modelInfo;
|
||||
const maxSize = Math.max(size.x, size.y, size.z);
|
||||
let distanceFactor = 1.2;
|
||||
let heightFactor = 0.3;
|
||||
let angle = 0;
|
||||
if (size.y > size.x * 2) {
|
||||
// 高瘦物体,拉远一点,稍微抬高视角
|
||||
distanceFactor = 1.5;
|
||||
heightFactor = 0.4;
|
||||
angle = Math.PI / 6; // 30度
|
||||
} else if (size.x > size.y * 2) {
|
||||
// 扁平物体,降低视角
|
||||
heightFactor = 0.2;
|
||||
angle = Math.PI / 8; // 22.5度
|
||||
}
|
||||
const { position: cameraPos, target } = calculateCameraPosition(
|
||||
modelInfo,
|
||||
camera.value,
|
||||
{
|
||||
distanceFactor,
|
||||
heightFactor,
|
||||
angle
|
||||
}
|
||||
);
|
||||
// 执行相机动画
|
||||
animateCamera(
|
||||
camera.value.position,
|
||||
cameraPos,
|
||||
controls.value.target,
|
||||
target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let openModel = (event:any)=>{
|
||||
let mouse = new THREE.Vector2();
|
||||
let raycaster = new THREE.Raycaster();
|
||||
mouse.x=(event.clientX/window.innerWidth)*2-1;
|
||||
mouse.y=-(event.clientY/window.innerHeight)*2+1;
|
||||
raycaster.setFromCamera(mouse, camera.value);
|
||||
let intersects = raycaster.intersectObjects(scene.value.children);
|
||||
return intersects
|
||||
}
|
||||
let isTweening = false;
|
||||
|
||||
function animateCamera(
|
||||
startCameraPos: THREE.Vector3,
|
||||
endCameraPos: THREE.Vector3,
|
||||
startTarget: THREE.Vector3,
|
||||
endTarget: THREE.Vector3
|
||||
) {
|
||||
if (isTweening) return;
|
||||
isTweening = true;
|
||||
|
||||
let options = {
|
||||
cx: startCameraPos.x,
|
||||
cy: startCameraPos.y,
|
||||
cz: startCameraPos.z,
|
||||
tx: startTarget.x,
|
||||
ty: startTarget.y,
|
||||
tz: startTarget.z
|
||||
};
|
||||
|
||||
gsap.to(options, {
|
||||
cx: endCameraPos.x,
|
||||
cy: endCameraPos.y,
|
||||
cz: endCameraPos.z,
|
||||
tx: endTarget.x,
|
||||
ty: endTarget.y,
|
||||
tz: endTarget.z,
|
||||
duration: 1,
|
||||
ease: 'power2.inOut', // 使用更自然的缓动
|
||||
onUpdate: () => {
|
||||
camera.value.position.set(options.cx, options.cy, options.cz);
|
||||
controls.value.target.set(options.tx, options.ty, options.tz);
|
||||
controls.value.update();
|
||||
},
|
||||
onComplete: () => {
|
||||
isTweening = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
const setModel = async (url:any)=>{
|
||||
clearModel(group,scene)
|
||||
await addModel(url,controls,camera,pointLight,group,load)
|
||||
// addMaterial()
|
||||
await threeModel.setModel(url,load)
|
||||
}
|
||||
const open = async (url)=>{
|
||||
const open = (url)=>{
|
||||
load.value.state = true
|
||||
await nextTick(()=>{
|
||||
init()
|
||||
nextTick(async ()=>{
|
||||
threeModel = new ThreeManager(threeDom.value)
|
||||
await threeModel.setHDRI()
|
||||
await setModel(url)
|
||||
load.value.state = false
|
||||
threeModel.operation()
|
||||
})
|
||||
controls.value.enableDamping = true;
|
||||
let animate = ()=>{
|
||||
animationId.value = requestAnimationFrame(animate);
|
||||
// renderer.value.render(scene.value, camera.value);
|
||||
// model.rotation.x += 0.01; //旋转物体
|
||||
var vector = camera.value.position.clone()
|
||||
controls.value.update();
|
||||
renderer.value.render(scene.value, camera.value);
|
||||
// point.position.set(vector.x,vector.y,vector.z);
|
||||
// group.rotation.y += 0.01;
|
||||
// composer.render();
|
||||
};
|
||||
animate();
|
||||
await setModel(url)
|
||||
load.value.state = false
|
||||
|
||||
}
|
||||
|
||||
const captureView = ()=>{
|
||||
return threeModel.exportAsImage()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({open})
|
||||
defineExpose({open,captureView})
|
||||
</script>
|
||||
<template>
|
||||
<div class="modelBox">
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,28 @@ 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 hdri from '@/assets/images/three/hdri.hdr'
|
||||
|
||||
export const initThree = (threeDom)=>{
|
||||
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, 10000);
|
||||
camera.position.set(0, 90, 6); //设置相机位置
|
||||
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;
|
||||
|
||||
/**
|
||||
* 创建渲染器对象
|
||||
*/
|
||||
const width = threeDom.offsetWidth; //窗口宽度
|
||||
|
||||
|
||||
const width = threeDom.offsetWidth; //窗口宽度
|
||||
const height = threeDom.offsetHeight; //窗口高度
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
@@ -34,9 +41,8 @@ export const initThree = (threeDom)=>{
|
||||
threeDom.appendChild(renderer.domElement);
|
||||
|
||||
// 设置渲染器大小
|
||||
//环境光
|
||||
const ambient = new THREE.AmbientLight(0xffffff,.8);
|
||||
scene.add(ambient);
|
||||
|
||||
|
||||
const controls = new OrbitControls(camera,renderer.domElement)//监听鼠标、键盘事件;
|
||||
// controls.minDistance = 500; // 设置相机与焦点的最小距离
|
||||
// controls.maxDistance = 4000; // 设置相机与焦点的最大距离
|
||||
@@ -47,6 +53,27 @@ export const initThree = (threeDom)=>{
|
||||
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
|
||||
|
||||
// 设置环境贴图(影响材质反射)
|
||||
scene.environment = hdrTexture
|
||||
// 可选:同时设置为背景
|
||||
scene.background = hdrTexture
|
||||
|
||||
console.log('HDR 环境贴图加载成功:', hdri)
|
||||
} catch (error) {
|
||||
console.error('HDR 加载失败:', error)
|
||||
// 降级方案:使用环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
|
||||
scene.add(ambientLight)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 光源设置
|
||||
*/
|
||||
@@ -57,9 +84,33 @@ export const initThree = (threeDom)=>{
|
||||
DirectionalLight 平行光,比如太阳光
|
||||
SpotLight 聚光源
|
||||
*/
|
||||
|
||||
|
||||
//设置环境光全亮
|
||||
const pointLight = new THREE.AmbientLight(0xffffff,.2);
|
||||
scene.add(pointLight);
|
||||
//环境光
|
||||
const pointLight = new THREE.AmbientLight(0xffffff,.8);
|
||||
// scene.add(pointLight);
|
||||
|
||||
function 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);
|
||||
// 存储 offset 向量副本,避免后续重复创建
|
||||
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//开启阴影
|
||||
@@ -80,7 +131,7 @@ export const initThree = (threeDom)=>{
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
// const texture = textureLoader.load('/3dModel/sketch-thick.jpg');
|
||||
scene.background = new THREE.Color("#fff");
|
||||
return {scene,group,camera,renderer,ambient,controls,pointLight}
|
||||
return {scene,group,camera,renderer,controls,pointLight,studioLights}
|
||||
}
|
||||
export const clearModel = (group,scene)=>{
|
||||
const oldGroup:any = group.value;
|
||||
@@ -229,4 +280,14 @@ export const addModel = async (
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
// 导出当前视图为图片
|
||||
export const exportAsImage = (renderer, camera, scene, filename = 'model.png')=>{
|
||||
// 渲染当前场景
|
||||
renderer.render(scene, camera)
|
||||
|
||||
// 获取 canvas 数据
|
||||
const canvas = renderer.domElement
|
||||
const dataURL = canvas.toDataURL('image/png')
|
||||
return dataURL
|
||||
}
|
||||
@@ -59,7 +59,7 @@
|
||||
<image-preview ref="imagePreviewRef" />
|
||||
<baseModal ref="threeModelRef">
|
||||
<template v-slot="{ currentData }">
|
||||
<threeModel :currentData="currentData" />
|
||||
<threeModel :currentData="currentData" @captureView="captureView" />
|
||||
</template>
|
||||
</baseModal>
|
||||
<Assistant />
|
||||
@@ -253,6 +253,27 @@
|
||||
const openThreeModelPreview = (currentData) => {
|
||||
threeModelRef.value.open(currentData)
|
||||
}
|
||||
const captureView = (captureData)=>{
|
||||
const timestamp = Date.now()
|
||||
nodeManager.createResultNode({
|
||||
data: {
|
||||
superiorID_: captureData.nodeId,
|
||||
data: {
|
||||
selectable: false,
|
||||
imageProcessTasks: [
|
||||
{
|
||||
id: timestamp + '',
|
||||
url: captureData.minioUrl,
|
||||
status: 'RETURNED',
|
||||
taskId: timestamp + ''
|
||||
}
|
||||
],
|
||||
selectTaskId: timestamp + ''
|
||||
}
|
||||
}
|
||||
})
|
||||
// threeModelRef.value.close()
|
||||
}
|
||||
provide('openImagePreview', openImagePreview)
|
||||
provide('openThreeModelPreview', openThreeModelPreview)
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ interface NodeData {
|
||||
data?: object// 节点数据
|
||||
tier?: string// 节点层级
|
||||
isHeader?: boolean// 是否显示头
|
||||
superiorID?: string// 上级节点ID
|
||||
superiorID?: string// 上级节点ID,有连接线
|
||||
superiorID_?: string// 上级节点ID,没有连接线
|
||||
superiorNodeType?: string// 上级节点类型
|
||||
disableDelete?: boolean// 是否禁用删除
|
||||
disableCopy?: boolean// 是否禁用复制
|
||||
@@ -45,23 +46,31 @@ export class NodeManager {
|
||||
/** 创建节点 */
|
||||
createNode(options: NodeOptions) {
|
||||
const superiorID = options?.data?.superiorID
|
||||
const superiorID_ = options?.data?.superiorID_// 上级节点ID,用于创建子节点时使用
|
||||
//获取上级节点所生成的最后一个node,设置位置为最后一个节点的xy 加上 节点间距
|
||||
const superiorGenerateNodes = this.stateManager.getSubordNodes(superiorID)
|
||||
const superiorGenerateNodes = superiorID?this.stateManager.getSubordNodes(superiorID):[]
|
||||
const currentNode = superiorGenerateNodes.find((node) => {
|
||||
return (node.data.createIndexPosition === options?.data?.createIndexPosition && options?.data?.createIndexPosition)
|
||||
})
|
||||
const endGenerateNode = superiorGenerateNodes.reduce((max, current) => {
|
||||
return current.data.createIndexPosition > max.data.createIndexPosition ? current : max
|
||||
}, superiorGenerateNodes[0])
|
||||
const snode = superiorID ? this.stateManager.flowManager.getNodeById(superiorID) : this.stateManager.flowManager.getLastNode();
|
||||
let snode = null as any
|
||||
if(superiorID){
|
||||
snode = this.stateManager.flowManager.getNodeById(superiorID)
|
||||
}else if(superiorID_){
|
||||
snode = this.stateManager.flowManager.getNodeById(superiorID_)
|
||||
}else{
|
||||
snode = this.stateManager.flowManager.getLastNode()
|
||||
}
|
||||
const id = options.id || createId()
|
||||
const positionX = options.positionX || 0
|
||||
const positionY = options.positionY || 0
|
||||
const position = options.position ||
|
||||
(
|
||||
currentNode ?
|
||||
currentNode ?//当前节点位置,覆盖操作
|
||||
currentNode.position :
|
||||
endGenerateNode ?
|
||||
endGenerateNode ?//最大子级位置
|
||||
{
|
||||
x: endGenerateNode.position.x + positionX,
|
||||
y: endGenerateNode.position.y + positionY + this.ranksep + 200
|
||||
|
||||
@@ -211,7 +211,7 @@ export class StateManager {
|
||||
}
|
||||
if(!deletePromise) return console.log('删除操作被取消')
|
||||
|
||||
if(node.data.data.imageProcessTasks.length > 1){
|
||||
if(node.data.data?.imageProcessTasks?.length > 1){
|
||||
node.data.data.imageProcessTasks = node.data.data.imageProcessTasks.filter((item) => item.taskId !== node.data.data.selectTaskId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { computed, ref, onBeforeUnmount, shallowRef } from 'vue'
|
||||
const props = defineProps({
|
||||
modalWidth: {
|
||||
type: String,
|
||||
@@ -29,7 +29,7 @@
|
||||
})
|
||||
const showDialog = ref(false)
|
||||
let currentData = ref(null)
|
||||
const open = (data: any,) => {
|
||||
const open = (data: any) => {
|
||||
currentData.value = data
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user