import { api } from '../../../scripts/api.js' import { app } from '../../../scripts/app.js' import { createRelightModal, modalStyles } from './config.js' import { SceneUtils } from './scene_utils.js' export class LightEditor { constructor() { const styleElement = document.createElement('style'); styleElement.textContent = modalStyles; document.head.appendChild(styleElement); this.modal = createRelightModal(); this.canvasContainer = this.modal.querySelector('.relight-canvas-container'); this.lightIndicator = this.modal.querySelector('.light-source-indicator'); this.isMovingLight = false; this.currentNode = null; this.isSceneSetup = false; this.lightX = 0; this.lightY = 0; this.lightZ = 1; this.zOffset = 0; this.lightSources = []; this.activeSourceIndex = -1; this.bindEvents(); } bindEvents() { this.onCanvasMouseDownHandler = this.onCanvasMouseDown.bind(this); this.onCanvasMouseMoveHandler = this.onCanvasMouseMove.bind(this); this.onCanvasMouseUpHandler = this.onCanvasMouseUp.bind(this); this.onSliderChangeHandler = this.onSliderChange.bind(this); this.onLightTypeChangeHandler = this.onLightTypeChange.bind(this); const cancelBtn = this.modal.querySelector('.relight-btn.cancel'); cancelBtn.addEventListener('click', () => this.cleanupAndClose(true)); const applyBtn = this.modal.querySelector('.relight-btn.apply'); applyBtn.addEventListener('click', () => this.applyChanges()); this.canvasContainer.addEventListener('mousedown', this.onCanvasMouseDownHandler); const sliders = this.modal.querySelectorAll('.relight-slider'); sliders.forEach(slider => { slider.addEventListener('input', this.onSliderChangeHandler); }); // 添加光源类型切换事件监听 const lightTypeSelect = this.modal.querySelector('#lightType'); if (lightTypeSelect) { lightTypeSelect.addEventListener('change', this.onLightTypeChangeHandler); } } onLightTypeChange(event) { const lightType = event.target.value; const spotlightControls = this.modal.querySelectorAll('.spotlight-controls'); const pointlightControls = this.modal.querySelectorAll('.pointlight-controls'); // 显示或隐藏聚光灯控制项 spotlightControls.forEach(control => { control.style.display = lightType === 'spot' ? 'block' : 'none'; }); // 显示或隐藏点光源控制项 pointlightControls.forEach(control => { control.style.display = lightType === 'point' ? 'block' : 'none'; }); // 如果有活动光源,转换其类型 if (this.activeSourceIndex !== -1) { const activeSource = this.lightSources[this.activeSourceIndex]; if (activeSource) { this.convertLightType(activeSource, lightType); } } } convertLightType(source, newType) { // 保存原始光源的属性 const position = source.position; const intensity = source.intensity; const color = source.light.color.getHex(); const visible = source.light.visible; // 从场景中移除原始光源 this.scene.remove(source.light); if (source.lightType === 'spot' && source.light.target) { this.scene.remove(source.light.target); } // 创建新的光源 let newLight; if (newType === 'spot') { const spotlightAngleSlider = this.modal.querySelector('#spotlightAngle'); const spotlightPenumbraSlider = this.modal.querySelector('#spotlightPenumbra'); const angle = spotlightAngleSlider ? parseFloat(spotlightAngleSlider.value) * Math.PI : Math.PI / 3; const penumbra = spotlightPenumbraSlider ? parseFloat(spotlightPenumbraSlider.value) : 0.2; newLight = new THREE.SpotLight(color, intensity, 10, angle, penumbra); // 设置目标点位置 let targetPosition; if (source.targetPosition) { targetPosition = source.targetPosition; } else { // 如果没有现成的目标点,默认设置在光源下方一些位置 targetPosition = { x: position.x, y: position.y - 1, z: 0 }; } newLight.target.position.set(targetPosition.x, targetPosition.y, targetPosition.z); source.targetPosition = targetPosition; this.scene.add(newLight.target); // 为光源添加聚光灯特有属性 source.spotParams = { angle: angle, penumbra: penumbra }; // 聚光灯指示器样式修改 if (source.indicator) { source.indicator.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; source.indicator.style.transform = 'translate(-50%, -20%)'; } // 创建连接线 if (!source.connectionLine) { source.connectionLine = document.createElement('div'); source.connectionLine.className = 'spotlight-connection-line'; this.canvasContainer.appendChild(source.connectionLine); } // 创建目标点指示器 if (!source.targetIndicator) { source.targetIndicator = this.createTargetIndicator( source.lightColor || '#ffffff' ); source.targetIndicator.style.display = 'none'; // 默认隐藏 this.canvasContainer.appendChild(source.targetIndicator); } // 更新连接线位置 this.updateSpotlightLine(source); } else { const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; newLight = new THREE.PointLight(color, intensity, radius, 2); // 点光源指示器样式恢复 if (source.indicator) { source.indicator.style.clipPath = ''; source.indicator.style.borderRadius = '50%'; source.indicator.style.transform = 'translate(-50%, -50%)'; } // 隐藏连接线 if (source.connectionLine) { source.connectionLine.style.display = 'none'; } // 隐藏目标点指示器 if (source.targetIndicator) { source.targetIndicator.style.display = 'none'; } // 移除聚光灯特有属性 if (source.spotParams) { delete source.spotParams; } // 为点光源添加半径参数 source.pointParams = { radius: radius }; } // 设置新光源的位置和可见性 newLight.position.set(position.x, position.y, position.z); newLight.visible = visible; // 更新光源对象 source.light = newLight; source.lightType = newType; // 将新光源添加到场景 this.scene.add(newLight); // 更新渲染 this.render(); } async cleanupAndClose(cancelled = false) { if (cancelled && this.currentNode) { try { await api.fetchApi("/lg_relight_ultra/cancel", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ node_id: this.currentNode.id }) }); console.log('[RelightNode] 已发送取消信号Cancellation signal sent'); } catch (error) { console.error('[RelightNode] 发送取消信号失败Failed to send cancel signal:', error); } } document.removeEventListener('mousemove', this.onCanvasMouseMoveHandler); document.removeEventListener('mouseup', this.onCanvasMouseUpHandler); this.lightSources.forEach(source => { if (source.indicator && source.indicator.parentNode) { source.indicator.remove(); } if (source.targetIndicator && source.targetIndicator.parentNode) { source.targetIndicator.remove(); } if (source.connectionLine && source.connectionLine.parentNode) { source.connectionLine.remove(); } }); if (this.scene) { this.lightSources.forEach(source => { this.scene.remove(source.light); if (source.lightType === 'spot' && source.light.target) { this.scene.remove(source.light.target); } }); this.lightSources = []; this.activeSourceIndex = -1; } this.isMovingLight = false; this.modal.close(); } applyChanges() { if (this.renderer && this.scene && this.camera && this.currentNode) { this.renderer.render(this.scene, this.camera); this.uploadCanvasResult(this.renderer.domElement, this.currentNode.id); this.saveLightConfiguration(this.currentNode.id); } this.cleanupAndClose(); } onCanvasMouseDown(event) { if (event.target !== this.canvasContainer && event.target !== this.displayRenderer?.domElement) return; const activeSource = this.activeSourceIndex !== -1 ? this.lightSources[this.activeSourceIndex] : null; // 鼠标右键,且当前有活动的聚光灯 if (event.button === 2 && activeSource && activeSource.lightType === 'spot') { event.preventDefault(); event.stopPropagation(); // 切换到目标点编辑模式 activeSource.editingTarget = true; // 创建或显示目标点指示器 if (!activeSource.targetIndicator) { activeSource.targetIndicator = this.createTargetIndicator(activeSource.lightColor || '#ffffff'); this.canvasContainer.appendChild(activeSource.targetIndicator); } else { activeSource.targetIndicator.style.display = 'block'; } // 更新连接线 if (!activeSource.connectionLine) { activeSource.connectionLine = document.createElement('div'); activeSource.connectionLine.className = 'spotlight-connection-line'; this.canvasContainer.appendChild(activeSource.connectionLine); } this.isMovingLight = true; document.addEventListener('mousemove', this.onCanvasMouseMoveHandler); document.addEventListener('mouseup', this.onCanvasMouseUpHandler); this.updateLightFromMouseEvent(event); return; } // 如果之前是在编辑目标点,现在切换回编辑光源位置 if (activeSource && activeSource.editingTarget) { activeSource.editingTarget = false; } this.isMovingLight = true; document.addEventListener('mousemove', this.onCanvasMouseMoveHandler); document.addEventListener('mouseup', this.onCanvasMouseUpHandler); this.updateLightFromMouseEvent(event); event.stopPropagation(); } onCanvasMouseMove(event) { if (!this.isMovingLight) return; this.updateLightFromMouseEvent(event); event.stopPropagation(); } onCanvasMouseUp(event) { const activeSource = this.activeSourceIndex !== -1 ? this.lightSources[this.activeSourceIndex] : null; // 如果处于目标点编辑模式,鼠标抬起后完成目标点的放置 if (activeSource && activeSource.editingTarget) { activeSource.editingTarget = false; // 隐藏目标点指示器,但保留连接线 if (activeSource.targetIndicator) { activeSource.targetIndicator.style.display = 'none'; } } this.isMovingLight = false; document.removeEventListener('mousemove', this.onCanvasMouseMoveHandler); document.removeEventListener('mouseup', this.onCanvasMouseUpHandler); event.stopPropagation(); } updateLightFromMouseEvent(event) { if (!this.displayRenderer || !this.displayRenderer.domElement || this.activeSourceIndex === -1) return; const activeSource = this.lightSources[this.activeSourceIndex]; if (!activeSource) return; const mouseX = event.clientX; const mouseY = event.clientY; const rect = this.displayRenderer.domElement.getBoundingClientRect(); const x = Math.max(0, Math.min(1, (mouseX - rect.left) / rect.width)); const y = Math.max(0, Math.min(1, (mouseY - rect.top) / rect.height)); this.updateLightIndicatorExact(mouseX, mouseY, activeSource.indicator); // 移除X轴计算中的负号 this.lightX = ((x * 2) - 1); this.lightY = ((1 - y) * 2) - 1; // 从深度图获取Z轴高度 const zValue = SceneUtils.getZValueFromDepthMap(this.depthMapTexture, x, y, this.zOffset); const xValueEl = this.modal.querySelector('.light-x-value'); const yValueEl = this.modal.querySelector('.light-y-value'); const zValueEl = this.modal.querySelector('.light-z-value'); xValueEl.textContent = this.lightX.toFixed(2); yValueEl.textContent = this.lightY.toFixed(2); zValueEl.textContent = zValue.toFixed(2); // 更新光源位置,包括从深度图获取的Z值 activeSource.position = { x: this.lightX, y: this.lightY, z: zValue }; activeSource.light.position.set(this.lightX, this.lightY, zValue); // 如果当前正在编辑目标点而不是光源位置 if (activeSource.editingTarget && activeSource.lightType === 'spot') { // 更新目标点位置 activeSource.targetPosition = { x: this.lightX, y: this.lightY, z: 0 }; activeSource.light.target.position.set(this.lightX, this.lightY, 0); // 更新目标点指示器的位置 if (activeSource.targetIndicator) { this.updateLightIndicatorExact(mouseX, mouseY, activeSource.targetIndicator); } // 更新连接线 this.updateSpotlightLine(activeSource); } else if (activeSource.lightType === 'spot') { // 如果没有设置过目标点,默认指向下方 if (!activeSource.targetPosition) { activeSource.targetPosition = { x: this.lightX, y: this.lightY - 1, z: 0 }; activeSource.light.target.position.set( activeSource.targetPosition.x, activeSource.targetPosition.y, activeSource.targetPosition.z ); } // 更新连接线 this.updateSpotlightLine(activeSource); } this.render(); } updateLightIndicatorExact(clientX, clientY, indicator) { if (!indicator) return; const rect = this.canvasContainer.getBoundingClientRect(); const offsetX = clientX - rect.left; const offsetY = clientY - rect.top; indicator.style.position = 'absolute'; indicator.style.left = `${offsetX}px`; indicator.style.top = `${offsetY}px`; indicator.style.display = 'block'; } onSliderChange(event) { const sliderId = event.target.id; const value = parseFloat(event.target.value); const activeSource = this.lightSources[this.activeSourceIndex]; if (!activeSource) return; switch (sliderId) { case 'zOffset': this.zOffset = value; // 如果有活动光源,需要更新它的z坐标(需要重新从深度图获取基础z值) if (activeSource) { // 从深度图获取当前位置的基础Z值 const rect = this.displayRenderer.domElement.getBoundingClientRect(); const indicator = activeSource.indicator; const indicatorRect = indicator.getBoundingClientRect(); const x = Math.max(0, Math.min(1, (indicatorRect.left + indicatorRect.width/2 - rect.left) / rect.width)); const y = Math.max(0, Math.min(1, (indicatorRect.top + indicatorRect.height/2 - rect.top) / rect.height)); // 重新计算Z值并更新光源位置 const zValue = SceneUtils.getZValueFromDepthMap(this.depthMapTexture, x, y, this.zOffset); activeSource.position.z = zValue; activeSource.light.position.set( activeSource.position.x, activeSource.position.y, zValue ); // 更新显示的Z值 const zValueEl = this.modal.querySelector('.light-z-value'); if (zValueEl) { zValueEl.textContent = zValue.toFixed(2); } } break; case 'lightIntensity': activeSource.light.intensity = value; activeSource.intensity = value; break; case 'ambientLight': if (this.ambientLight) { this.ambientLight.intensity = value; } break; case 'normalStrength': if (this.material) { // 将法线强度应用到材质 this.material.normalScale.set(value, value); // 如果有必要,可以存储该值以便保存配置 this.normalStrength = value; } break; case 'spotlightAngle': if (activeSource.lightType === 'spot') { activeSource.light.angle = value * Math.PI; activeSource.spotParams.angle = value * Math.PI; } break; case 'spotlightPenumbra': if (activeSource.lightType === 'spot') { activeSource.light.penumbra = value; activeSource.spotParams.penumbra = value; } break; case 'pointlightRadius': if (activeSource.lightType === 'point') { activeSource.light.distance = value; if (activeSource.pointParams) { activeSource.pointParams.radius = value; } else { activeSource.pointParams = { radius: value }; } } break; } this.render(); } async setupScene(texture, depthMap, normalMap, maskTexture = null) { try { // 存储深度图纹理引用 this.depthMapTexture = depthMap; if (!texture.image || !texture.image.complete) { console.warn('[RelightNode] 纹理图像未完全加载Texture image not fully loaded'); await new Promise(resolve => setTimeout(resolve, 100)); } const imageWidth = texture.image.width; const imageHeight = texture.image.height; const imageAspect = imageWidth / imageHeight; const containerRect = this.canvasContainer.getBoundingClientRect(); const containerWidth = containerRect.width; const containerHeight = containerRect.height; const containerAspect = containerWidth / containerHeight; let displayWidth, displayHeight; if (imageAspect > containerAspect) { displayWidth = containerWidth; displayHeight = containerWidth / imageAspect; } else { displayHeight = containerHeight; displayWidth = containerHeight * imageAspect; } const frustumHeight = 2; const frustumWidth = frustumHeight * imageAspect; if (!this.isSceneSetup) { console.log('[RelightNode] 首次创建场景Creating a scene for the first time'); this.scene = new THREE.Scene(); this.camera = new THREE.OrthographicCamera( frustumWidth / -2, frustumWidth / 2, frustumHeight / 2, frustumHeight / -2, 0.1, 1000 ); this.camera.position.z = 5; this.displayRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true, alpha: true }); this.ambientLight = new THREE.AmbientLight(0xffffff, 1.0); this.scene.add(this.ambientLight); this.isSceneSetup = true; } else { this.camera.left = frustumWidth / -2; this.camera.right = frustumWidth / 2; this.camera.top = frustumHeight / 2; this.camera.bottom = frustumHeight / -2; this.camera.updateProjectionMatrix(); } // 设置显示渲染器的尺寸和样式 this.displayRenderer.setSize(displayWidth, displayHeight); const displayCanvas = this.displayRenderer.domElement; displayCanvas.style.maxWidth = '100%'; displayCanvas.style.maxHeight = '100%'; displayCanvas.style.margin = 'auto'; displayCanvas.style.position = 'absolute'; displayCanvas.style.left = '50%'; displayCanvas.style.top = '50%'; displayCanvas.style.transform = 'translate(-50%, -50%)'; if (!this.canvasContainer.contains(displayCanvas)) { this.canvasContainer.appendChild(displayCanvas); } // 设置输出渲染器为原始图像尺寸 this.renderer.setSize(imageWidth, imageHeight); const geometry = new THREE.PlaneGeometry(2 * imageAspect, 2, 32, 32); // 获取法线强度值(如果已存在) const normalStrengthSlider = this.modal.querySelector('#normalStrength'); this.normalStrength = normalStrengthSlider ? parseFloat(normalStrengthSlider.value) : 0; let material; if (maskTexture) { material = SceneUtils.createMaskedMaterial(texture, depthMap, normalMap, maskTexture, this.normalStrength); } else { material = SceneUtils.createSimpleMaterial(texture, depthMap, normalMap, this.normalStrength); } if (this.mesh) { this.mesh.geometry.dispose(); this.mesh.geometry = geometry; this.mesh.material.dispose(); this.mesh.material = material; } else { this.mesh = new THREE.Mesh(geometry, material); this.scene.add(this.mesh); } this.material = material; this.renderer.setClearColor(0x000000, 0); const canvas = this.renderer.domElement; canvas.style.maxWidth = '100%'; canvas.style.maxHeight = '100%'; canvas.style.margin = 'auto'; canvas.style.position = 'absolute'; canvas.style.left = '50%'; canvas.style.top = '50%'; canvas.style.transform = 'translate(-50%, -50%)'; this.renderer.render(this.scene, this.camera); // 更新渲染方法 this.render(); console.log('[RelightNode] 场景设置完成'); return true; } catch (error) { console.error('[RelightNode] 场景设置错误:', error); return false; } } uploadCanvasResult(canvas, nodeId) { // 使用输出渲染器的画布而不是显示渲染器的画布 this.renderer.domElement.toBlob(async (blob) => { try { console.log('[RelightNode] 正在上传渲染结果...'); const formData = new FormData(); formData.append('node_id', nodeId); formData.append('result_image', blob, 'result.png'); const response = await api.fetchApi('/lg_relight/upload_result', { method: 'POST', body: formData }); const result = await response.json(); if (result.error) { console.error('[RelightNode] 上传失败:', result.error); } else { console.log('[RelightNode] 上传成功'); } } catch (error) { console.error('[RelightNode] 上传失败:', error); } }, 'image/png', 1.0); } createLightSource() { const lightIntensitySlider = this.modal.querySelector('#lightIntensity'); const lightTypeSelect = this.modal.querySelector('#lightType'); const lightType = lightTypeSelect ? lightTypeSelect.value : 'point'; let light; let indicator; const lightIntensity = parseFloat(lightIntensitySlider.value); // 根据选择的光源类型创建对应的光源 if (lightType === 'spot') { const spotlightAngleSlider = this.modal.querySelector('#spotlightAngle'); const spotlightPenumbraSlider = this.modal.querySelector('#spotlightPenumbra'); const angle = spotlightAngleSlider ? parseFloat(spotlightAngleSlider.value) * Math.PI : Math.PI / 3; const penumbra = spotlightPenumbraSlider ? parseFloat(spotlightPenumbraSlider.value) : 0.2; light = new THREE.SpotLight(0xffffff, lightIntensity, 10, angle, penumbra); light.target.position.set(0, -1, 0); // 默认向下照射 this.scene.add(light.target); indicator = this.createLightIndicator('#ffffff', 'spot'); } else { const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; light = new THREE.PointLight(0xffffff, lightIntensity, radius, 2); indicator = this.createLightIndicator('#ffffff', 'point'); } const lightSource = { id: Date.now(), name: `光源 ${this.lightSources.length + 1}`, light: light, position: { x: 0, y: 0, z: 1.0 }, // 默认值,会在鼠标点击时更新 intensity: lightIntensity, indicatorColor: '#ffffff', lightColor: '#ffffff', lightType: lightType, indicator: indicator, editingTarget: false // 是否正在编辑目标点 }; // 如果是聚光灯,添加额外的聚光灯参数 if (lightType === 'spot') { lightSource.spotParams = { angle: light.angle, penumbra: light.penumbra }; lightSource.targetPosition = { x: 0, y: -1, z: 0 }; // 创建连接线元素 lightSource.connectionLine = document.createElement('div'); lightSource.connectionLine.className = 'spotlight-connection-line'; this.canvasContainer.appendChild(lightSource.connectionLine); // 创建目标点指示器 lightSource.targetIndicator = this.createTargetIndicator('#ffffff'); lightSource.targetIndicator.style.display = 'none'; // 默认隐藏 this.canvasContainer.appendChild(lightSource.targetIndicator); } else if (lightType === 'point') { // 为点光源添加半径参数 const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; lightSource.pointParams = { radius: radius }; } // 直接设置光源位置 lightSource.light.position.set(0, 0, 1.0); this.scene.add(lightSource.light); this.lightSources.push(lightSource); this.setActiveLight(this.lightSources.length - 1); // 更新材质参数 - 固定为0 if (this.material) { this.material.shininess = 0; this.material.specular.setRGB(0, 0, 0); // 从滑条获取法线强度值 const normalStrengthSlider = this.modal.querySelector('#normalStrength'); const normalStrength = normalStrengthSlider ? parseFloat(normalStrengthSlider.value) : 0; this.material.normalScale.set(normalStrength, normalStrength); this.normalStrength = normalStrength; } // 确保环境光强度与UI滑块一致 if (this.ambientLight) { const ambientLightSlider = this.modal.querySelector('#ambientLight'); if (ambientLightSlider) { this.ambientLight.intensity = parseFloat(ambientLightSlider.value); } } return lightSource; } createLightIndicator(color, type = 'point') { const indicator = document.createElement('div'); indicator.className = 'light-source-indicator'; indicator.style.backgroundColor = color; indicator.style.boxShadow = `0 0 15px ${color}`; // 根据光源类型设置指示器样式 if (type === 'spot') { indicator.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; indicator.style.transform = 'translate(-50%, -20%)'; } // 添加选中状态的外圈 const selectionRing = document.createElement('div'); selectionRing.className = 'selection-ring'; selectionRing.style.display = 'none'; // 默认隐藏 selectionRing.style.borderColor = '#00ff00'; // 使用亮绿色 indicator.appendChild(selectionRing); this.canvasContainer.appendChild(indicator); return indicator; } updateLightSourcesList() { const listContainer = this.modal.querySelector('.light-sources-list'); if (!listContainer) return; listContainer.innerHTML = ''; this.lightSources.forEach((source, index) => { const item = document.createElement('div'); item.className = `light-source-item ${index === this.activeSourceIndex ? 'active' : ''}`; // 根据光源的可见状态选择眼睛图标 const visibilityIcon = source.light.visible ? '👁️' : '👁️🗨️'; // 添加光源类型图标 const typeIcon = source.lightType === 'spot' ? '🔦' : '💡'; item.innerHTML = `