├── web ├── i18n.js ├── i18n.json ├── scene_utils.js ├── main.js ├── config.js ├── relight_v3.js └── light_editor.js ├── README_CN.md ├── README.md ├── example_workflow ├── relight_ultra.json ├── LG_Relight.json └── relight_ultra+LBM.json └── __init__.py /web/i18n.js: -------------------------------------------------------------------------------- 1 | // web/i18n.js 2 | import i18nData from './i18n.json'; 3 | 4 | function getUserLang() { 5 | // Usa preferencia ComfyUI si existe, si no, toma el idioma del navegador 6 | return window.comfyUILang || navigator.language.slice(0, 2) || 'en'; 7 | } 8 | 9 | export function t(key) { 10 | const lang = getUserLang(); 11 | return (i18nData[lang] && i18nData[lang][key]) || i18nData['en'][key] || key; 12 | } 13 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Language / 语言 2 | [English](README.md) | **中文** 3 | 4 | --- 5 | 6 | ## 🙏 特别感谢 / Special Thanks 7 | 感谢 **[@vivi-gomez](https://github.com/vivi-gomez)** 对本项目的翻译工作! 8 | Thanks to **[@vivi-gomez](https://github.com/vivi-gomez)** for the translation work on this project! 9 | 10 | --- 11 | 12 | # Comfyui-LG_Relight 13 | 14 | Comfyui中3D实时打光的简单实现 15 | 开源节点哈,祝大家玩的开心! 16 | 17 | 最新代码请访问: 18 | https://github.com/LAOGOU-666/Comfyui-LG_Relight 19 | 20 | ## 新增ULTRA版本,支持添加多个光源并且独立调整,支持点光源,聚光灯模式选择,实现更全面的3D打光 21 | ![Image](https://github.com/user-attachments/assets/e63a7ea2-ea90-4888-af3d-39e2b4b45140) 22 | 23 | ![Image](https://github.com/user-attachments/assets/b0b44127-f755-4ee2-9351-a8bd34db2ed7) 24 | 25 | * 2025/5/14加更skip_dialog跳过弹窗功能,一次设置,批量复用,修复若干BUG以及UI 26 | ![Image](https://github.com/user-attachments/assets/257ecd3f-62b7-4407-883b-18dcbc62f47a) 27 | ## 更新模态弹窗,彻底解决弹窗问题 28 | - 弃用原来的PYQT和JS版本,目前版本支持所有系统,解决弹窗问题,操作方式和原来一致 29 | ![Image](https://github.com/user-attachments/assets/2b6a9577-6eae-43ff-9dc5-0c5b92d4f69b) 30 | ![Image](https://github.com/user-attachments/assets/fa97e56c-dff1-44e0-ada8-63c0b2ccb5dd) 31 | 32 | 33 | 单一的节点打光效果有限,配合ic_light效果更佳 34 | ![Image](https://github.com/user-attachments/assets/e9564b58-7a6b-4538-b89b-29de64dd270c) 35 | 36 | # 商务合作联系 37 | VX:wenrulaogou2033 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Language / 语言 2 | **English** | [中文](README_CN.md) 3 | 4 | --- 5 | 6 | ## 🙏 Special Thanks / 特别感谢 7 | Thanks to **[@vivi-gomez](https://github.com/vivi-gomez)** for the translation work on this project! 8 | 感谢 **[@vivi-gomez](https://github.com/vivi-gomez)** 对本项目的翻译工作! 9 | 10 | --- 11 | 12 | # Comfyui-LG_Relight-ENGLISH for testing (JUN 1 - 2025) 13 | This is a fork for testing the very good project of LAOGOU. 14 | It is a personal/manual translation to English, just for testing. 15 | 16 | For the latest code: 17 | https://github.com/LAOGOU-666/Comfyui-LG_Relight 18 | 19 | Simple implementation of 3D real-time lighting in Comfyui 20 | Open source node, I wish you all a happy time! 21 | 22 | ## Added ULTRA version, supports adding multiple light sources and adjusting them independently, supports point light sources, spotlight mode selection, and realizes more comprehensive 3D lighting 23 | ![Image](https://github.com/user-attachments/assets/e63a7ea2-ea90-4888-af3d-39e2b4b45140) 24 | 25 | ![Image](https://github.com/user-attachments/assets/b0b44127-f755-4ee2-9351-a8bd34db2ed7) 26 | 27 | * 2025/5/14 Added skip_dialog function, one-time setting, batch reuse, and fixed several BUGs and UI 28 | ![Image](https://github.com/user-attachments/assets/257ecd3f-62b7-4407-883b-18dcbc62f47a) 29 | ## Update modal pop-up window to completely solve the pop-up window problem 30 | - Abandon the original PYQT and JS versions. The current version supports all systems, solves the pop-up window problem, and the operation method is the same as the original 31 | ![Image](https://github.com/user-attachments/assets/2b6a9577-6eae-43ff-9dc5-0c5b92d4f69b) 32 | ![Image](https://github.com/user-attachments/assets/fa97e56c-dff1-44e0-ada8-63c0b2ccb5dd) 33 | 34 | The lighting effect of a single node is limited, and the effect is better with ic_light 35 | ![Image](https://github.com/user-attachments/assets/e9564b58-7a6b-4538-b89b-29de64dd270c) 36 | 37 | # Business cooperation contact 38 | VX:wenrulaogou2033 39 | -------------------------------------------------------------------------------- /web/i18n.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh": { 3 | "选择光源颜色": "选择光源颜色", 4 | "隐藏": "隐藏", 5 | "显示": "显示", 6 | "删除": "删除", 7 | "光源": "光源", 8 | 9 | "重置": "重置", 10 | "亮度": "亮度", 11 | "阴影范围": "阴影范围", 12 | "阴影强度": "阴影强度", 13 | "高光范围": "高光范围", 14 | "高光强度": "高光强度", 15 | "高光颜色": "高光颜色", 16 | "阴影颜色": "阴影颜色", 17 | "应用": "应用", 18 | "取消": "取消", 19 | "Z轴": "Z轴", 20 | "光照重建 - 3D打光": "光照重建 - 3D打光", 21 | "点击或拖动图像设置光源位置": "点击或拖动图像设置光源位置", 22 | "光照设置": "光照设置", 23 | "光照位置": "光照位置", 24 | "Z轴偏移": "Z轴偏移", 25 | "光照强度": "光照强度", 26 | "环境光强度": "环境光强度", 27 | "法线强度": "法线强度", 28 | "光源类型": "光源类型", 29 | "点光源": "点光源", 30 | "聚光灯": "聚光灯", 31 | "光源半径": "光源半径", 32 | "聚光灯角度": "聚光灯角度", 33 | "聚光灯衰减": "聚光灯衰减", 34 | "光源管理": "光源管理", 35 | "添加光源": "添加光源", 36 | "Three.js 加载成功": "Three.js 加载成功", 37 | "Three.js 加载失败:": "Three.js 加载失败:", 38 | "开始初始化扩展...": "开始初始化扩展...", 39 | "正在加载 Three.js...": "正在加载 Three.js...", 40 | "处理节点:": "处理节点:", 41 | "处理错误:": "处理错误:", 42 | "聚光灯模式: 左键拖动移动光源位置,右键点击设置照射目标方向": "聚光灯模式: 左键拖动移动光源位置,右键点击设置照射目标方向", 43 | "注册节点定义...": "注册节点定义...", 44 | "节点添加到画布, ID:": "节点添加到画布, ID:", 45 | "更新种子": "更新种子", 46 | "种子已更新为:": "种子已更新为:", 47 | "光照配置和UI已重置": "光照配置和UI已重置", 48 | "聚光灯目标未添加到场景中,请在创建聚光灯后调用 scene.add(spotlight.target)": "聚光灯目标未添加到场景中,请在创建聚光灯后调用 scene.add(spotlight.target)", 49 | "创建带遮罩的材质": "创建带遮罩的材质", 50 | "读取深度图像素失败": "读取深度图像素失败", 51 | "获取Z值时出错": "获取Z值时出错", 52 | "发送取消信号失败": "发送取消信号失败", 53 | "应用重光照失败": "应用重光照失败", 54 | "显示重光照窗口失败": "显示重光照窗口失败" 55 | }, 56 | "en": { 57 | "选择光源颜色": "Select light color", 58 | "隐藏": "Hide", 59 | "显示": "Show", 60 | "删除": "Delete", 61 | "光源": "Light Source", 62 | 63 | "重置": "Reset", 64 | "亮度": "Brightness", 65 | "阴影范围": "Shadow Range", 66 | "阴影强度": "Shadow Strength", 67 | "高光范围": "Highlight Range", 68 | "高光强度": "Highlight Strength", 69 | "高光颜色": "Highlight Color", 70 | "阴影颜色": "Shadow Color", 71 | "应用": "Apply", 72 | "取消": "Cancel", 73 | "Z轴": "Z Axis", 74 | "光照重建 - 3D打光": "Lighting Reconstruction - 3D Lighting", 75 | "点击或拖动图像设置光源位置": "Click or drag the image to set the light position", 76 | "光照设置": "Lighting Settings", 77 | "光照位置": "Light Position", 78 | "Z轴偏移": "Z Offset", 79 | "光照强度": "Light Intensity", 80 | "环境光强度": "Ambient Light Intensity", 81 | "法线强度": "Normal Strength", 82 | "光源类型": "Light Type", 83 | "点光源": "Point Light", 84 | "聚光灯": "Spotlight", 85 | "光源半径": "Light Radius", 86 | "聚光灯角度": "Spotlight Angle", 87 | "聚光灯衰减": "Spotlight Penumbra", 88 | "光源管理": "Light Source Management", 89 | "添加光源": "Add Light Source", 90 | "Three.js 加载成功": "Three.js loaded successfully", 91 | "Three.js 加载失败:": "Failed to load Three.js:", 92 | "开始初始化扩展...": "Initializing extension...", 93 | "正在加载 Three.js...": "Loading Three.js...", 94 | "处理节点:": "Processing node:", 95 | "处理错误:": "Processing error:", 96 | "聚光灯模式: 左键拖动移动光源位置,右键点击设置照射目标方向": "Spotlight mode: Drag with left mouse to move the light, right click to set the target direction", 97 | "注册节点定义...": "Registering node definition...", 98 | "节点添加到画布, ID:": "Node added to canvas, ID:", 99 | "更新种子": "Update Seed", 100 | "种子已更新为:": "Seed updated to:", 101 | "光照配置和UI已重置": "Lighting configuration and UI have been reset", 102 | "聚光灯目标未添加到场景中,请在创建聚光灯后调用 scene.add(spotlight.target)": "Spotlight target not added to scene, please call scene.add(spotlight.target) after creating the spotlight", 103 | "创建带遮罩的材质": "Create masked material", 104 | "读取深度图像素失败": "Failed to read depth map pixel", 105 | "获取Z值时出错": "Error getting Z value", 106 | "发送取消信号失败": "Failed to send cancel signal", 107 | "应用重光照失败": "Failed to apply relight", 108 | "显示重光照窗口失败": "Failed to display relight window" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /web/scene_utils.js: -------------------------------------------------------------------------------- 1 | export class SceneUtils { 2 | static base64ToTexture(base64String) { 3 | return new Promise((resolve) => { 4 | const texture = new THREE.Texture(); 5 | const img = new Image(); 6 | img.src = `data:image/png;base64,${base64String}`; 7 | img.onload = () => { 8 | texture.image = img; 9 | texture.needsUpdate = true; 10 | resolve(texture); 11 | }; 12 | }); 13 | } 14 | 15 | static createSimpleMaterial(baseTexture, depthMap, normalMap, normalScale = 0) { 16 | return new THREE.MeshPhongMaterial({ 17 | map: baseTexture, 18 | normalMap: normalMap, 19 | normalScale: new THREE.Vector2(normalScale, normalScale), 20 | displacementMap: depthMap, 21 | displacementScale: 0.3, 22 | shininess: 0, 23 | specular: new THREE.Color(0) 24 | }); 25 | } 26 | 27 | static adjustSpotlightDirection(spotlight, targetPoint) { 28 | // 确保目标点被设置并更新 29 | if (targetPoint) { 30 | spotlight.target.position.copy(targetPoint); 31 | } 32 | 33 | // 确保目标被添加到场景中才能正常工作 34 | if (!spotlight.target.parent) { 35 | console.log('[RelightNode] 聚光灯目标未添加到场景中,请在创建聚光灯后调用 scene.add(spotlight.target)'); 36 | } 37 | 38 | spotlight.target.updateMatrixWorld(); 39 | } 40 | 41 | static calculateSpotlightDirection(spotlightPos, targetPos) { 42 | // 计算从聚光灯位置到目标位置的方向向量 43 | const direction = new THREE.Vector3( 44 | targetPos.x - spotlightPos.x, 45 | targetPos.y - spotlightPos.y, 46 | targetPos.z - spotlightPos.z 47 | ); 48 | 49 | // 归一化方向向量 50 | direction.normalize(); 51 | 52 | return direction; 53 | } 54 | 55 | static visualizeSpotlight(scene, spotlight, color = 0xffffff, segments = 8) { 56 | // 移除之前的可视化对象 57 | if (spotlight.visualHelper) { 58 | scene.remove(spotlight.visualHelper); 59 | } 60 | 61 | // 创建聚光灯锥体可视化几何体 62 | const angle = spotlight.angle; 63 | const distance = spotlight.distance || 10; 64 | 65 | // 计算锥体顶端半径 66 | const radius = Math.tan(angle) * distance; 67 | 68 | // 创建锥体几何体 69 | const geometry = new THREE.ConeGeometry(radius, distance, segments, 1, true); 70 | geometry.rotateX(Math.PI); 71 | 72 | // 创建材质 73 | const material = new THREE.MeshBasicMaterial({ 74 | color: color, 75 | wireframe: true, 76 | transparent: true, 77 | opacity: 0.3 78 | }); 79 | 80 | // 创建网格 81 | const cone = new THREE.Mesh(geometry, material); 82 | 83 | // 设置位置为聚光灯位置 84 | cone.position.copy(spotlight.position); 85 | 86 | // 获取方向 87 | const target = new THREE.Vector3( 88 | spotlight.target.position.x, 89 | spotlight.target.position.y, 90 | spotlight.target.position.z 91 | ); 92 | 93 | // 计算聚光灯到目标的方向 94 | const direction = this.calculateSpotlightDirection( 95 | {x: spotlight.position.x, y: spotlight.position.y, z: spotlight.position.z}, 96 | {x: target.x, y: target.y, z: target.z} 97 | ); 98 | 99 | // 使锥体指向目标 100 | cone.lookAt(target); 101 | 102 | // 存储可视化对象 103 | spotlight.visualHelper = cone; 104 | 105 | // 添加到场景 106 | scene.add(cone); 107 | 108 | return cone; 109 | } 110 | 111 | static createMaskedMaterial(baseTexture, depthMap, normalMap, maskTexture, normalScale = 1.0) { 112 | console.log('[RelightNode] 创建带遮罩的材质'); 113 | const material = new THREE.MeshPhongMaterial({ 114 | map: baseTexture, 115 | normalMap: normalMap, 116 | normalScale: new THREE.Vector2(normalScale, normalScale), 117 | displacementMap: depthMap, 118 | displacementScale: 0.3, 119 | shininess: 30, 120 | specular: new THREE.Color(0x444444) 121 | }); 122 | material.onBeforeCompile = (shader) => { 123 | shader.uniforms.maskTexture = { value: maskTexture }; 124 | shader.fragmentShader = shader.fragmentShader.replace( 125 | 'uniform float opacity;', 126 | 'uniform float opacity;\nuniform sampler2D maskTexture;' 127 | ); 128 | shader.fragmentShader = shader.fragmentShader.replace( 129 | '#include ', 130 | ` 131 | #include 132 | float maskValue = texture2D(maskTexture, vUv).r; 133 | vec3 originalColor = diffuseColor.rgb; 134 | reflectedLight.directDiffuse *= maskValue; 135 | reflectedLight.directSpecular *= maskValue; 136 | reflectedLight.indirectDiffuse *= maskValue; 137 | reflectedLight.indirectSpecular *= maskValue; 138 | ` 139 | ); 140 | shader.fragmentShader = shader.fragmentShader.replace( 141 | 'gl_FragColor = vec4( outgoingLight, diffuseColor.a );', 142 | ` 143 | vec3 finalColor = mix(originalColor, outgoingLight, maskValue); 144 | gl_FragColor = vec4(finalColor, diffuseColor.a); 145 | ` 146 | ); 147 | }; 148 | return material; 149 | } 150 | 151 | static getZValueFromDepthMap(depthMapTexture, x, y, zOffset) { 152 | // 默认Z值,当无法从深度图获取时使用 153 | const defaultZ = 1.0; 154 | 155 | try { 156 | if (!depthMapTexture || !depthMapTexture.image) { 157 | return defaultZ + zOffset; 158 | } 159 | 160 | // 创建临时画布以便于读取深度图像素 161 | if (!this.depthMapCanvas) { 162 | this.depthMapCanvas = document.createElement('canvas'); 163 | this.depthMapContext = this.depthMapCanvas.getContext('2d'); 164 | } 165 | 166 | const img = depthMapTexture.image; 167 | this.depthMapCanvas.width = img.width; 168 | this.depthMapCanvas.height = img.height; 169 | this.depthMapContext.drawImage(img, 0, 0); 170 | 171 | // 计算图像上的坐标 172 | const pixelX = Math.floor(x * img.width); 173 | const pixelY = Math.floor(y * img.height); 174 | 175 | // 获取像素数据 176 | try { 177 | const pixelData = this.depthMapContext.getImageData(pixelX, pixelY, 1, 1).data; 178 | // 从灰度值计算深度(0-255转为0.1-2.0范围) 179 | // 通常深度图白色表示更近,黑色表示更远 180 | const depth = pixelData[0] / 255; // 使用红色通道作为深度值 181 | 182 | // 转换为z轴范围,并添加偏移量 183 | const zValue = 1 + depth * 1 + zOffset; 184 | return zValue; 185 | } catch (error) { 186 | console.error('[RelightNode] 读取深度图像素失败:', error); 187 | return defaultZ + zOffset; 188 | } 189 | } catch (error) { 190 | console.error('[RelightNode] 获取Z值时出错:', error); 191 | return defaultZ + zOffset; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /example_workflow/relight_ultra.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 156, 3 | "last_link_id": 533, 4 | "nodes": [ 5 | { 6 | "id": 13, 7 | "type": "ImpactMinMax", 8 | "pos": [ 9 | 1587.468017578125, 10 | 446.4409484863281 11 | ], 12 | "size": [ 13 | 210, 14 | 78 15 | ], 16 | "flags": {}, 17 | "order": 2, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "a", 22 | "type": "*", 23 | "link": 13 24 | }, 25 | { 26 | "name": "b", 27 | "type": "*", 28 | "link": 14 29 | } 30 | ], 31 | "outputs": [ 32 | { 33 | "name": "INT", 34 | "type": "INT", 35 | "links": [ 36 | 15, 37 | 52 38 | ], 39 | "slot_index": 0 40 | } 41 | ], 42 | "properties": { 43 | "cnr_id": "comfyui-impact-pack", 44 | "ver": "dc70f40effeb21681f30af65062bc1b2a40fdd82", 45 | "Node name for S&R": "ImpactMinMax" 46 | }, 47 | "widgets_values": [ 48 | false 49 | ] 50 | }, 51 | { 52 | "id": 10, 53 | "type": "LoadImage", 54 | "pos": [ 55 | 838.7528076171875, 56 | 298.15252685546875 57 | ], 58 | "size": [ 59 | 315, 60 | 314 61 | ], 62 | "flags": {}, 63 | "order": 0, 64 | "mode": 0, 65 | "inputs": [], 66 | "outputs": [ 67 | { 68 | "name": "IMAGE", 69 | "type": "IMAGE", 70 | "links": [ 71 | 40 72 | ], 73 | "slot_index": 0 74 | }, 75 | { 76 | "name": "MASK", 77 | "type": "MASK", 78 | "links": null 79 | } 80 | ], 81 | "properties": { 82 | "cnr_id": "comfy-core", 83 | "ver": "0.3.18", 84 | "Node name for S&R": "LoadImage" 85 | }, 86 | "widgets_values": [ 87 | "1.png", 88 | "image" 89 | ] 90 | }, 91 | { 92 | "id": 12, 93 | "type": "LayerUtility: ImageScaleByAspectRatio V2", 94 | "pos": [ 95 | 1201.483154296875, 96 | 297.75164794921875 97 | ], 98 | "size": [ 99 | 336, 100 | 330 101 | ], 102 | "flags": {}, 103 | "order": 1, 104 | "mode": 0, 105 | "inputs": [ 106 | { 107 | "name": "image", 108 | "type": "IMAGE", 109 | "shape": 7, 110 | "link": 40 111 | }, 112 | { 113 | "name": "mask", 114 | "type": "MASK", 115 | "shape": 7, 116 | "link": null 117 | } 118 | ], 119 | "outputs": [ 120 | { 121 | "name": "image", 122 | "type": "IMAGE", 123 | "links": [ 124 | 12, 125 | 53, 126 | 525 127 | ], 128 | "slot_index": 0 129 | }, 130 | { 131 | "name": "mask", 132 | "type": "MASK", 133 | "links": null 134 | }, 135 | { 136 | "name": "original_size", 137 | "type": "BOX", 138 | "links": null 139 | }, 140 | { 141 | "name": "width", 142 | "type": "INT", 143 | "links": [ 144 | 13 145 | ], 146 | "slot_index": 3 147 | }, 148 | { 149 | "name": "height", 150 | "type": "INT", 151 | "links": [ 152 | 14 153 | ], 154 | "slot_index": 4 155 | } 156 | ], 157 | "properties": { 158 | "cnr_id": "comfyui_layerstyle", 159 | "ver": "458456e464ffa53baea5ad5efe84f8345305f135", 160 | "Node name for S&R": "LayerUtility: ImageScaleByAspectRatio V2" 161 | }, 162 | "widgets_values": [ 163 | "original", 164 | 1, 165 | 1, 166 | "crop", 167 | "lanczos", 168 | "64", 169 | "longest", 170 | 2048, 171 | "#000000" 172 | ], 173 | "color": "rgba(38, 73, 116, 0.7)" 174 | }, 175 | { 176 | "id": 11, 177 | "type": "DepthAnythingPreprocessor", 178 | "pos": [ 179 | 1583.8896484375, 180 | 304.3936462402344 181 | ], 182 | "size": [ 183 | 210, 184 | 78 185 | ], 186 | "flags": {}, 187 | "order": 3, 188 | "mode": 0, 189 | "inputs": [ 190 | { 191 | "name": "image", 192 | "type": "IMAGE", 193 | "link": 12 194 | }, 195 | { 196 | "name": "resolution", 197 | "type": "INT", 198 | "shape": 7, 199 | "widget": { 200 | "name": "resolution" 201 | }, 202 | "link": 15 203 | } 204 | ], 205 | "outputs": [ 206 | { 207 | "name": "IMAGE", 208 | "type": "IMAGE", 209 | "links": [ 210 | 526 211 | ], 212 | "slot_index": 0 213 | } 214 | ], 215 | "properties": { 216 | "cnr_id": "comfyui_controlnet_aux", 217 | "ver": "5a049bde9cc117dafc327cded156459289097ea1", 218 | "Node name for S&R": "DepthAnythingPreprocessor" 219 | }, 220 | "widgets_values": [ 221 | "depth_anything_vitl14.pth", 222 | 512 223 | ] 224 | }, 225 | { 226 | "id": 33, 227 | "type": "Metric3D-NormalMapPreprocessor", 228 | "pos": [ 229 | 1589.9364013671875, 230 | 577.2870483398438 231 | ], 232 | "size": [ 233 | 210, 234 | 126 235 | ], 236 | "flags": {}, 237 | "order": 4, 238 | "mode": 0, 239 | "inputs": [ 240 | { 241 | "name": "image", 242 | "type": "IMAGE", 243 | "link": 53 244 | }, 245 | { 246 | "name": "resolution", 247 | "type": "INT", 248 | "shape": 7, 249 | "widget": { 250 | "name": "resolution" 251 | }, 252 | "link": 52 253 | } 254 | ], 255 | "outputs": [ 256 | { 257 | "name": "IMAGE", 258 | "type": "IMAGE", 259 | "links": [ 260 | 527 261 | ], 262 | "slot_index": 0 263 | } 264 | ], 265 | "properties": { 266 | "cnr_id": "comfyui_controlnet_aux", 267 | "ver": "5a049bde9cc117dafc327cded156459289097ea1", 268 | "Node name for S&R": "Metric3D-NormalMapPreprocessor" 269 | }, 270 | "widgets_values": [ 271 | "vit-small", 272 | 1000, 273 | 1000, 274 | 512 275 | ] 276 | }, 277 | { 278 | "id": 156, 279 | "type": "LG_Relight_Ultra", 280 | "pos": [ 281 | 1849.5479736328125, 282 | 299.65399169921875 283 | ], 284 | "size": [ 285 | 210, 286 | 166 287 | ], 288 | "flags": {}, 289 | "order": 5, 290 | "mode": 0, 291 | "inputs": [ 292 | { 293 | "name": "bg_img", 294 | "type": "IMAGE", 295 | "link": 525 296 | }, 297 | { 298 | "name": "bg_depth_map", 299 | "type": "IMAGE", 300 | "link": 526 301 | }, 302 | { 303 | "name": "bg_normal_map", 304 | "type": "IMAGE", 305 | "link": 527 306 | }, 307 | { 308 | "name": "mask", 309 | "type": "MASK", 310 | "shape": 7, 311 | "link": null 312 | } 313 | ], 314 | "outputs": [ 315 | { 316 | "name": "IMAGE", 317 | "type": "IMAGE", 318 | "links": [ 319 | 528 320 | ] 321 | } 322 | ], 323 | "properties": { 324 | "cnr_id": "Comfyui-LG_Relight", 325 | "ver": "e85bde22382125589feafae0f7ec7e07c3aef4c4", 326 | "Node name for S&R": "LG_Relight_Ultra" 327 | }, 328 | "widgets_values": [ 329 | 6620222998883865, 330 | "randomize", 331 | "" 332 | ] 333 | }, 334 | { 335 | "id": 143, 336 | "type": "PreviewImage", 337 | "pos": [ 338 | 2145.65185546875, 339 | 300.3277893066406 340 | ], 341 | "size": [ 342 | 210, 343 | 246 344 | ], 345 | "flags": {}, 346 | "order": 6, 347 | "mode": 0, 348 | "inputs": [ 349 | { 350 | "name": "images", 351 | "type": "IMAGE", 352 | "link": 528 353 | } 354 | ], 355 | "outputs": [], 356 | "properties": { 357 | "cnr_id": "comfy-core", 358 | "ver": "0.3.18", 359 | "Node name for S&R": "PreviewImage" 360 | }, 361 | "widgets_values": [] 362 | } 363 | ], 364 | "links": [ 365 | [ 366 | 12, 367 | 12, 368 | 0, 369 | 11, 370 | 0, 371 | "IMAGE" 372 | ], 373 | [ 374 | 13, 375 | 12, 376 | 3, 377 | 13, 378 | 0, 379 | "*" 380 | ], 381 | [ 382 | 14, 383 | 12, 384 | 4, 385 | 13, 386 | 1, 387 | "*" 388 | ], 389 | [ 390 | 15, 391 | 13, 392 | 0, 393 | 11, 394 | 1, 395 | "INT" 396 | ], 397 | [ 398 | 40, 399 | 10, 400 | 0, 401 | 12, 402 | 0, 403 | "IMAGE" 404 | ], 405 | [ 406 | 52, 407 | 13, 408 | 0, 409 | 33, 410 | 1, 411 | "INT" 412 | ], 413 | [ 414 | 53, 415 | 12, 416 | 0, 417 | 33, 418 | 0, 419 | "IMAGE" 420 | ], 421 | [ 422 | 525, 423 | 12, 424 | 0, 425 | 156, 426 | 0, 427 | "IMAGE" 428 | ], 429 | [ 430 | 526, 431 | 11, 432 | 0, 433 | 156, 434 | 1, 435 | "IMAGE" 436 | ], 437 | [ 438 | 527, 439 | 33, 440 | 0, 441 | 156, 442 | 2, 443 | "IMAGE" 444 | ], 445 | [ 446 | 528, 447 | 156, 448 | 0, 449 | 143, 450 | 0, 451 | "IMAGE" 452 | ] 453 | ], 454 | "groups": [], 455 | "config": {}, 456 | "extra": { 457 | "ds": { 458 | "scale": 0.7513148009015783, 459 | "offset": { 460 | "0": -637.1597900390625, 461 | "1": 149.90675354003906 462 | } 463 | } 464 | }, 465 | "version": 0.4 466 | } -------------------------------------------------------------------------------- /web/main.js: -------------------------------------------------------------------------------- 1 | import { api } from '../../../scripts/api.js' 2 | import { app } from '../../../scripts/app.js' 3 | import { relightConfig } from './config.js' 4 | import { LightEditor } from './light_editor.js' 5 | 6 | app.registerExtension({ 7 | name: "LG_Relight_Ultra", 8 | async setup() { 9 | console.log('[RelightNode] 开始初始化扩展 Start initializing the extension...'); 10 | if (!window.THREE) { 11 | console.log('[RelightNode] 正在加载 Loading Three.js...'); 12 | await new Promise((resolve, reject) => { 13 | const script = document.createElement('script'); 14 | script.src = relightConfig.libraryUrl; 15 | script.onload = () => { 16 | console.log('[RelightNode] Three.js 加载成功 Loading Successfully'); 17 | resolve(); 18 | }; 19 | script.onerror = (error) => { 20 | console.error('[RelightNode] Three.js 加载失败: Loading failed:', error); 21 | reject(error); 22 | }; 23 | document.head.appendChild(script); 24 | }); 25 | } 26 | const lightEditor = new LightEditor(); 27 | api.addEventListener("relight_image", async ({ detail }) => { 28 | try { 29 | const { node_id, skip_dialog } = detail; 30 | console.log('[RelightNode] 处理节点: Processing Node:', node_id); 31 | 32 | if (skip_dialog) { 33 | // 跳过弹窗,直接使用现有配置进行处理 34 | await lightEditor.processWithoutDialog(node_id, detail); 35 | } else { 36 | // 显示弹窗让用户编辑 37 | await lightEditor.show(node_id, detail); 38 | 39 | // 添加聚光灯操作提示 40 | const canvasContainer = document.querySelector('.relight-canvas-container'); 41 | if (canvasContainer) { 42 | const spotlightHint = document.createElement('div'); 43 | spotlightHint.className = 'spotlight-hint'; 44 | spotlightHint.textContent = '聚光灯模式: 左键拖动移动光源位置,右键点击设置照射目标方向, Spotlight mode: left-click and drag to move the light source, right-click to set the target direction'; 45 | canvasContainer.appendChild(spotlightHint); 46 | 47 | // 禁用右键菜单以便使用右键点击 48 | canvasContainer.addEventListener('contextmenu', (e) => { 49 | e.preventDefault(); 50 | return false; 51 | }); 52 | } 53 | } 54 | } catch (error) { 55 | console.error('[RelightNode] 处理错误: Handling Errors:', error); 56 | } 57 | }); 58 | }, 59 | async beforeRegisterNodeDef(nodeType, nodeData) { 60 | if (nodeType.comfyClass === "LG_Relight_Ultra") { 61 | console.log('[RelightNode] 注册节点定义 Registering Node Definitions...'); 62 | const originalOnAdded = nodeType.prototype.onAdded; 63 | const originalOnNodeCreated = nodeType.prototype.onNodeCreated; 64 | const originalOnRemoved = nodeType.prototype.onRemoved; 65 | const originalOnClearError = nodeType.prototype.onClearError; 66 | nodeType.prototype.onAdded = function() { 67 | console.log('[RelightNode] 节点添加到画布 Adding nodes to the canvas, ID:', this.id); 68 | if (originalOnAdded) { 69 | return originalOnAdded.apply(this, arguments); 70 | } 71 | }; 72 | nodeType.prototype.onNodeCreated = function() { 73 | if (originalOnNodeCreated) { 74 | originalOnNodeCreated.apply(this, arguments); 75 | } 76 | this.hasFixedSeed = false; 77 | const seedWidget = this.addWidget( 78 | "number", 79 | "seed", 80 | 0, 81 | (value) => { 82 | this.seed = value; 83 | }, 84 | { 85 | min: 0, 86 | max: Number.MAX_SAFE_INTEGER, 87 | step: 1, 88 | precision: 0 89 | } 90 | ); 91 | const seed_modeWidget = this.addWidget( 92 | "combo", 93 | "seed_mode", 94 | "randomize", 95 | () => {}, 96 | { 97 | values: ["fixed", "increment", "decrement", "randomize"], 98 | serialize: false 99 | } 100 | ); 101 | seed_modeWidget.beforeQueued = () => { 102 | const mode = seed_modeWidget.value; 103 | let newValue = seedWidget.value; 104 | if (mode === "randomize") { 105 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 106 | } else if (mode === "increment") { 107 | newValue += 1; 108 | } else if (mode === "decrement") { 109 | newValue -= 1; 110 | } else if (mode === "fixed") { 111 | if (!this.hasFixedSeed) { 112 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 113 | this.hasFixedSeed = true; 114 | } 115 | } 116 | seedWidget.value = newValue; 117 | this.seed = newValue; 118 | }; 119 | seed_modeWidget.callback = (value) => { 120 | if (value !== "fixed") { 121 | this.hasFixedSeed = false; 122 | } 123 | }; 124 | const updateButton = this.addWidget("button", "更新种子 Update torrent", null, () => { 125 | const mode = seed_modeWidget.value; 126 | let newValue = seedWidget.value; 127 | if (mode === "randomize") { 128 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 129 | } else if (mode === "increment") { 130 | newValue += 1; 131 | } else if (mode === "decrement") { 132 | newValue -= 1; 133 | } else if (mode === "fixed") { 134 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 135 | this.hasFixedSeed = true; 136 | } 137 | seedWidget.value = newValue; 138 | seedWidget.callback(newValue); 139 | console.log('[RelightNode] 种子已更新为: The torrent has been updated to:', newValue); 140 | }); 141 | 142 | // 默认光照设置 143 | this.defaultLightConfig = { 144 | lightType: 'point', // 'point' 或 'spot' 145 | lightZ: 2.0, 146 | lightIntensity: 1.0, 147 | ambientLight: 0.2, 148 | normalStrength: 1.0, 149 | specularStrength: 0.2, 150 | shininess: 0, 151 | spotlight: { 152 | angle: 0.5, 153 | penumbra: 0.2 154 | } 155 | }; 156 | 157 | nodeType.prototype.resetLightConfig = function() { 158 | if (this.lightConfig) { 159 | delete this.lightConfig; 160 | } 161 | const defaultValues = { 162 | 'lightZ': 2.0, 163 | 'lightIntensity': 1.0, 164 | 'ambientLight': 0.2, 165 | 'normalStrength': 1.0, 166 | 'specularStrength': 0.2, 167 | 'shininess': 0, 168 | 'spotlightAngle': 0.5, 169 | 'spotlightPenumbra': 0.2 170 | }; 171 | const modal = document.getElementById('relight-editor-modal'); 172 | if (modal) { 173 | Object.entries(defaultValues).forEach(([id, value]) => { 174 | const slider = modal.querySelector(`#${id}`); 175 | if (slider) { 176 | slider.value = value; 177 | const event = new Event('input', { bubbles: true }); 178 | slider.dispatchEvent(event); 179 | } 180 | }); 181 | 182 | // 重置光源类型选择器 Reset Light Type Selector 183 | const lightTypeSelect = modal.querySelector('#lightType'); 184 | if (lightTypeSelect) { 185 | lightTypeSelect.value = 'point'; 186 | const event = new Event('change', { bubbles: true }); 187 | lightTypeSelect.dispatchEvent(event); 188 | } 189 | 190 | // 重置光源列表 Reset Light List 191 | const lightSourcesList = modal.querySelector('.light-sources-list'); 192 | if (lightSourcesList) { 193 | lightSourcesList.innerHTML = ''; 194 | } 195 | const indicators = modal.querySelectorAll('.light-source-indicator, .spotlight-target-indicator, .spotlight-connection-line'); 196 | indicators.forEach(indicator => indicator.remove()); 197 | 198 | // 移除操作提示 Remove operation prompt 199 | const spotlightHint = modal.querySelector('.spotlight-hint'); 200 | if (spotlightHint) { 201 | spotlightHint.remove(); 202 | } 203 | } 204 | console.log('[RelightNode] 光照配置和UI已重置 Lighting configuration and UI have been reset'); 205 | }; 206 | 207 | nodeType.prototype.onRemoved = function() { 208 | this.resetLightConfig(); 209 | if (originalOnRemoved) { 210 | return originalOnRemoved.apply(this, arguments); 211 | } 212 | }; 213 | 214 | nodeType.prototype.onClearError = function() { 215 | this.resetLightConfig(); 216 | if (originalOnClearError) { 217 | return originalOnClearError.apply(this, arguments); 218 | } 219 | }; 220 | } 221 | } 222 | } 223 | }); 224 | -------------------------------------------------------------------------------- /web/config.js: -------------------------------------------------------------------------------- 1 | import { api } from '../../../scripts/api.js' 2 | import { app } from '../../../scripts/app.js' 3 | 4 | export const relightConfig = { 5 | nodeName: "LG_Relight_Ultra", 6 | libraryName: "ThreeJS", 7 | libraryUrl: "https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js", 8 | defaultSize: 512, 9 | routes: { 10 | uploadEndpoint: "/lg_relight/upload_result", 11 | dataEvent: "relight_image" 12 | } 13 | }; 14 | 15 | export function createRelightModal() { 16 | const modal = document.createElement("dialog"); 17 | modal.id = "relight-editor-modal"; 18 | modal.innerHTML = ` 19 |
20 |
21 |
光照重建 Lighting reconstruction - 3D打光
22 |
23 |
24 |
25 |
26 |
点击或拖动图像设置光源位置 Click or drag the image to set the light source position
27 |
28 |
29 |
30 |

光照设置Lighting settings

31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 |
51 |
52 | 53 | 57 |
58 |
59 | 60 | 61 |
62 | 66 | 70 |
71 |
72 |

Light source management

73 |
74 | 75 |
76 | 77 |
78 |
79 |
80 |
81 | 82 | 83 |
84 |
85 | `; 86 | document.body.appendChild(modal); 87 | return modal; 88 | } 89 | 90 | export const modalStyles = ` 91 | #relight-editor-modal * { 92 | user-select: none; 93 | -webkit-user-select: none; 94 | -moz-user-select: none; 95 | -ms-user-select: none; 96 | } 97 | #relight-editor-modal { 98 | border: none; 99 | border-radius: 8px; 100 | padding: 0; 101 | background: #2a2a2a; 102 | width: 90vw; 103 | height: 90vh; 104 | max-width: 90vw; 105 | max-height: 90vh; 106 | } 107 | .relight-modal-content { 108 | background: #1a1a1a; 109 | width: 100%; 110 | height: 100%; 111 | display: flex; 112 | flex-direction: column; 113 | } 114 | .relight-modal-header { 115 | padding: 10px 15px; 116 | border-bottom: 1px solid #333; 117 | display: flex; 118 | justify-content: space-between; 119 | align-items: center; 120 | background: #333; 121 | } 122 | .relight-modal-title { 123 | font-size: 18px; 124 | color: #fff; 125 | } 126 | .relight-modal-body { 127 | flex: 1; 128 | display: flex; 129 | overflow: hidden; 130 | height: calc(100% - 120px); 131 | } 132 | .relight-canvas-container { 133 | flex: 1; 134 | position: relative; 135 | overflow: hidden; 136 | background: #222; 137 | display: flex; 138 | justify-content: center; 139 | align-items: center; 140 | cursor: pointer; 141 | } 142 | .light-source-indicator { 143 | position: absolute; 144 | width: 24px; 145 | height: 24px; 146 | background: #ffffff; 147 | border-radius: 50%; 148 | transform: translate(-50%, -50%); 149 | pointer-events: none; 150 | box-shadow: 0 0 15px rgba(255, 255, 255, 0.7); 151 | z-index: 100; 152 | opacity: 1.0; 153 | transition: opacity 0.3s; 154 | } 155 | .spotlight-target-indicator { 156 | position: absolute; 157 | width: 12px; 158 | height: 12px; 159 | background: #ffffff; 160 | border: 2px solid rgba(0, 0, 0, 0.5); 161 | border-radius: 50%; 162 | transform: translate(-50%, -50%); 163 | pointer-events: none; 164 | z-index: 90; 165 | opacity: 0.9; 166 | } 167 | .spotlight-connection-line { 168 | position: absolute; 169 | height: 2px; 170 | background: rgba(255, 255, 255, 0.7); 171 | transform-origin: left center; 172 | pointer-events: none; 173 | z-index: 80; 174 | } 175 | .light-source-hint { 176 | position: absolute; 177 | top: 10px; 178 | left: 10px; 179 | color: rgba(255, 255, 255, 0.7); 180 | font-size: 12px; 181 | background: rgba(0, 0, 0, 0.5); 182 | padding: 5px 10px; 183 | border-radius: 3px; 184 | pointer-events: none; 185 | opacity: 0.7; 186 | transition: opacity 0.3s; 187 | } 188 | .spotlight-hint { 189 | position: absolute; 190 | bottom: 10px; 191 | left: 10px; 192 | color: rgba(255, 255, 255, 0.7); 193 | font-size: 12px; 194 | background: rgba(0, 0, 0, 0.5); 195 | padding: 5px 10px; 196 | border-radius: 3px; 197 | pointer-events: none; 198 | opacity: 0.7; 199 | z-index: 110; 200 | } 201 | .relight-canvas-container:hover .light-source-hint { 202 | opacity: 0.3; 203 | } 204 | .relight-controls { 205 | width: 300px; 206 | padding: 15px; 207 | background: #222; 208 | border-left: 1px solid #333; 209 | overflow-y: auto; 210 | height: 100%; 211 | } 212 | .relight-control-group { 213 | margin-bottom: 15px; 214 | } 215 | .relight-control-group h3 { 216 | font-size: 14px; 217 | color: #ccc; 218 | margin-bottom: 10px; 219 | border-bottom: 1px solid #333; 220 | padding-bottom: 5px; 221 | } 222 | .relight-control-item { 223 | margin-bottom: 10px; 224 | } 225 | .relight-control-item label { 226 | display: block; 227 | color: #aaa; 228 | margin-bottom: 5px; 229 | font-size: 12px; 230 | } 231 | .relight-slider { 232 | width: 100%; 233 | background: #333; 234 | height: 6px; 235 | -webkit-appearance: none; 236 | border-radius: 3px; 237 | } 238 | .relight-slider::-webkit-slider-thumb { 239 | -webkit-appearance: none; 240 | width: 16px; 241 | height: 16px; 242 | background: #0080ff; 243 | border-radius: 50%; 244 | cursor: pointer; 245 | } 246 | .relight-select { 247 | width: 100%; 248 | background: #333; 249 | color: #fff; 250 | border: none; 251 | padding: 6px 8px; 252 | border-radius: 4px; 253 | cursor: pointer; 254 | } 255 | .relight-select option { 256 | background: #2a2a2a; 257 | } 258 | .relight-buttons { 259 | padding: 15px; 260 | border-top: 1px solid #333; 261 | display: flex; 262 | justify-content: flex-end; 263 | } 264 | .relight-btn { 265 | background: #0080ff; 266 | color: white; 267 | border: none; 268 | padding: 8px 15px; 269 | border-radius: 4px; 270 | margin-left: 10px; 271 | cursor: pointer; 272 | font-size: 14px; 273 | } 274 | .relight-btn:hover { 275 | background: #0070e0; 276 | } 277 | .relight-btn.cancel { 278 | background: #444; 279 | } 280 | .relight-btn.cancel:hover { 281 | background: #555; 282 | } 283 | .light-intensity-indicator { 284 | width: 100%; 285 | height: 10px; 286 | background: linear-gradient(to right, #333, #fffa); 287 | border-radius: 5px; 288 | margin-top: 5px; 289 | } 290 | .light-value-display { 291 | display: flex; 292 | justify-content: space-between; 293 | color: #aaa; 294 | font-size: 12px; 295 | margin-top: 5px; 296 | } 297 | .light-sources-list { 298 | max-height: 200px; 299 | overflow-y: auto; 300 | margin-bottom: 10px; 301 | border: 1px solid #333; 302 | border-radius: 4px; 303 | background: #1a1a1a; 304 | } 305 | .light-source-item { 306 | background: #333; 307 | border-radius: 4px; 308 | padding: 8px; 309 | margin-bottom: 4px; 310 | position: relative; 311 | cursor: pointer; 312 | transition: background 0.2s; 313 | } 314 | .light-source-item:hover { 315 | background: #444; 316 | } 317 | .light-source-item.active { 318 | border: 1px solid #0080ff; 319 | background: #2a2a2a; 320 | } 321 | .light-source-header { 322 | display: flex; 323 | align-items: center; 324 | width: 100%; 325 | } 326 | .light-source-color { 327 | width: 16px; 328 | height: 16px; 329 | border-radius: 50%; 330 | margin-right: 5px; 331 | box-shadow: 0 0 3px rgba(0,0,0,0.3); 332 | flex-shrink: 0; 333 | } 334 | .light-source-name { 335 | color: #ddd; 336 | font-size: 12px; 337 | margin-right: 5px; 338 | flex-grow: 1; 339 | white-space: nowrap; 340 | overflow: hidden; 341 | text-overflow: ellipsis; 342 | } 343 | .light-source-controls { 344 | display: flex; 345 | align-items: center; 346 | gap: 5px; 347 | margin-left: auto; 348 | } 349 | .light-source-controls button { 350 | background: none; 351 | border: none; 352 | color: #aaa; 353 | cursor: pointer; 354 | padding: 2px 4px; 355 | font-size: 16px; 356 | transition: color 0.2s; 357 | } 358 | .light-source-controls button:hover { 359 | color: #fff; 360 | } 361 | .light-source-delete { 362 | color: #ff4444 !important; 363 | } 364 | .light-source-delete:hover { 365 | color: #ff6666 !important; 366 | } 367 | .light-color-picker { 368 | width: 20px; 369 | height: 20px; 370 | padding: 0; 371 | border: none; 372 | border-radius: 4px; 373 | cursor: pointer; 374 | background: none; 375 | } 376 | .light-color-picker::-webkit-color-swatch-wrapper { 377 | padding: 0; 378 | } 379 | .light-color-picker::-webkit-color-swatch { 380 | border: 1px solid #666; 381 | border-radius: 4px; 382 | } 383 | .selection-ring { 384 | position: absolute; 385 | top: -6px; 386 | left: -6px; 387 | right: -6px; 388 | bottom: -6px; 389 | border: 6px solid #00ff00; 390 | border-radius: 50%; 391 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); 392 | } 393 | .spotlight-controls { 394 | transition: display 0.3s; 395 | } 396 | #relight-editor-modal::backdrop { 397 | background: rgba(0, 0, 0, 0.5); 398 | } 399 | #relight-editor-modal { 400 | position: fixed; 401 | top: 50%; 402 | left: 50%; 403 | transform: translate(-50%, -50%); 404 | margin: 0; 405 | } 406 | `; 407 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import torch 2 | import numpy as np 3 | from PIL import Image 4 | import io 5 | import base64 6 | from server import PromptServer 7 | import threading 8 | from aiohttp import web 9 | import json 10 | import torch.nn.functional as F 11 | from threading import Event 12 | image_cache = {} 13 | event_dict = {} 14 | CATEGORY_TYPE = "🎈LAOGOU/Relight" 15 | class LG_Relight_Basic: 16 | 17 | @classmethod 18 | def INPUT_TYPES(cls): 19 | return { 20 | "required": { 21 | "image": ("IMAGE",), 22 | "normals": ("IMAGE",), 23 | "x": ("FLOAT", { 24 | "default": 0.5, 25 | "min": 0.0, 26 | "max": 1.0, 27 | "step": 0.001 28 | }), 29 | "y": ("FLOAT", { 30 | "default": 0.5, 31 | "min": 0.0, 32 | "max": 1.0, 33 | "step": 0.001 34 | }), 35 | "z": ("FLOAT", { 36 | "default": 1.0, 37 | "min": -1.0, 38 | "max": 1.0, 39 | "step": 0.001 40 | }), 41 | "brightness": ("FLOAT", { 42 | "default": 1.0, 43 | "min": 0.0, 44 | "max": 3.0, 45 | "step": 0.001 46 | }), 47 | "shadow_range": ("FLOAT", { 48 | "default": 1.0, 49 | "min": 0.0, 50 | "max": 2.0, 51 | "step": 0.001 52 | }), 53 | "shadow_strength": ("FLOAT", { 54 | "default": 1.0, 55 | "min": 0.0, 56 | "max": 2.0, 57 | "step": 0.001 58 | }), 59 | "highlight_range": ("FLOAT", { 60 | "default": 1.0, 61 | "min": 0.0, 62 | "max": 2.0, 63 | "step": 0.001 64 | }), 65 | "highlight_strength": ("FLOAT", { 66 | "default": 1.0, 67 | "min": 0.0, 68 | "max": 2.0, 69 | "step": 0.001 70 | }), 71 | "highlight_color": ("STRING", {"default": "#FFFFFF"}), 72 | "shadow_color": ("STRING", {"default": "#000000"}), 73 | } 74 | } 75 | 76 | RETURN_TYPES = ("IMAGE",) 77 | FUNCTION = "relight" 78 | CATEGORY = CATEGORY_TYPE 79 | 80 | def relight(self, image, normals, x, y, z, brightness, 81 | shadow_range, shadow_strength, highlight_range, highlight_strength, 82 | highlight_color, shadow_color): 83 | 84 | # 将十六进制颜色转换为RGB值 85 | def hex_to_rgb(hex_color): 86 | hex_color = hex_color.lstrip('#') 87 | return [int(hex_color[i:i+2], 16)/255.0 for i in (0, 2, 4)] 88 | 89 | highlight_color = hex_to_rgb(highlight_color) 90 | shadow_color = hex_to_rgb(shadow_color) 91 | 92 | norm = normals.detach().clone() * 2 - 1 93 | norm = F.interpolate(norm.movedim(-1,1), 94 | size=(image.shape[1], image.shape[2]), 95 | mode='bilinear').movedim(1,-1) 96 | 97 | # 将百分比坐标转换为光照方向 98 | # 确保光源位置与坐标一致 99 | light_x = -((x * 2) - 1) # 0->-1, 1->1 100 | # 注意这里y要反转,因为图像坐标系y轴向下,而光照坐标系y轴向上 101 | light_y = -((y * 2) - 1) # 0->1, 1->-1 102 | 103 | light = torch.tensor([light_x, light_y, z], device=image.device) 104 | light = F.normalize(light, dim=0) 105 | 106 | diffuse = norm[:,:,:,0] * light[0] + norm[:,:,:,1] * light[1] + norm[:,:,:,2] * light[2] 107 | diffuse = (diffuse + 1.0) * 0.5 108 | 109 | shadow_offset = shadow_strength - 1.0 110 | highlight_offset = highlight_strength - 1.0 111 | 112 | shadow_threshold = 1.0 - shadow_range 113 | highlight_threshold = 1.0 - highlight_range 114 | 115 | shadow_mask = torch.clamp((diffuse - shadow_threshold) / max(shadow_range, 1e-6), 0, 1) 116 | highlight_mask = torch.clamp((diffuse - highlight_threshold) / max(highlight_range, 1e-6), 0, 1) 117 | 118 | light_intensity = torch.ones_like(diffuse) 119 | 120 | if shadow_strength != 1.0: 121 | light_intensity = light_intensity * ( 122 | shadow_mask + 123 | (1.0 - shadow_mask) * (2.0 - shadow_strength) 124 | ) 125 | 126 | if highlight_strength != 1.0: 127 | light_intensity = light_intensity + highlight_mask * highlight_offset 128 | 129 | color_effect = torch.ones_like(image[:,:,:,:3]) 130 | if highlight_color != [1.0, 1.0, 1.0] or shadow_color != [0.0, 0.0, 0.0]: 131 | highlight_color = torch.tensor(highlight_color, device=image.device) 132 | shadow_color = torch.tensor(shadow_color, device=image.device) 133 | color_effect = ( 134 | shadow_mask.unsqueeze(-1) * highlight_color + 135 | (1.0 - shadow_mask).unsqueeze(-1) * shadow_color 136 | ) 137 | 138 | brightness_factor = brightness if brightness != 1.0 else 1.0 139 | 140 | relit = image.detach().clone() 141 | light_intensity = light_intensity.unsqueeze(-1).repeat(1,1,1,3) 142 | 143 | relit[:,:,:,:3] = torch.clip( 144 | relit[:,:,:,:3] * light_intensity * brightness_factor * color_effect, 145 | 0, 1 146 | ) 147 | 148 | return (relit,) 149 | 150 | 151 | 152 | class LG_Relight: 153 | @classmethod 154 | def INPUT_TYPES(cls): 155 | return { 156 | "required": { 157 | "image": ("IMAGE",), 158 | "normals": ("IMAGE",), 159 | }, 160 | "hidden": { 161 | "unique_id": "UNIQUE_ID", 162 | } 163 | } 164 | 165 | RETURN_TYPES = ("IMAGE",) 166 | FUNCTION = "relight" 167 | CATEGORY = CATEGORY_TYPE 168 | OUTPUT_NODE = True 169 | 170 | def encode_image_to_base64(self, image, is_mask=False): 171 | image = (image * 255).clip(0, 255).astype(np.uint8) 172 | 173 | if is_mask: 174 | if len(image.shape) == 3: 175 | image = image[0] 176 | image = np.stack([image] * 3, axis=-1) 177 | else: 178 | if len(image.shape) == 4: 179 | image = image[0] 180 | 181 | image = Image.fromarray(image) 182 | buffered = io.BytesIO() 183 | image.save(buffered, format="PNG") 184 | return base64.b64encode(buffered.getvalue()).decode() 185 | 186 | def relight(self, image, normals, unique_id): 187 | try: 188 | event = threading.Event() 189 | event_dict[unique_id] = event 190 | image_b64 = self.encode_image_to_base64(image.cpu().numpy()) 191 | normals_b64 = self.encode_image_to_base64(normals.cpu().numpy()) 192 | send_data = { 193 | "node_id": unique_id, 194 | "image": f"data:image/png;base64,{image_b64}", 195 | "normals": f"data:image/png;base64,{normals_b64}" 196 | } 197 | PromptServer.instance.send_sync("lg_relight_init", send_data) 198 | event.wait() 199 | del event_dict[unique_id] 200 | 201 | if unique_id in image_cache: 202 | img_data = base64.b64decode(image_cache[unique_id].split(",")[1]) 203 | img = Image.open(io.BytesIO(img_data)) 204 | img_np = np.array(img).astype(np.float32) / 255.0 205 | 206 | if len(img_np.shape) == 2: 207 | img_np = np.stack([img_np] * 3, axis=-1) 208 | elif len(img_np.shape) == 3 and img_np.shape[-1] == 4: 209 | img_np = img_np[..., :3] 210 | 211 | result = torch.from_numpy(img_np).unsqueeze(0) 212 | 213 | del image_cache[unique_id] 214 | return (result,) 215 | else: 216 | return (image,) 217 | 218 | except Exception as e: 219 | print(f"[ERROR] An error occurred during relight processing: {str(e)}") 220 | import traceback 221 | print(traceback.format_exc()) 222 | return (image,) 223 | finally: 224 | print(f"[DEBUG] 清理资源Cleaning up resources: node_id={unique_id}") 225 | if unique_id in event_dict: 226 | del event_dict[unique_id] 227 | if unique_id in image_cache: 228 | del image_cache[unique_id] 229 | 230 | @PromptServer.instance.routes.post("/lg_relight/update_image") 231 | async def update_image_v3(request): 232 | try: 233 | data = await request.json() 234 | node_id = data.get("node_id") 235 | image_data = data.get("image") 236 | 237 | if node_id and image_data: 238 | image_cache[node_id] = image_data 239 | if node_id in event_dict: 240 | event_dict[node_id].set() 241 | return web.Response(text=json.dumps({"status": "success"})) 242 | else: 243 | return web.Response(status=400, text=json.dumps({"error": "无效数据Invalid data"})) 244 | except Exception as e: 245 | return web.Response(status=500, text=json.dumps({"error": str(e)})) 246 | 247 | @PromptServer.instance.routes.post("/lg_relight/cancel") 248 | async def cancel_v3(request): 249 | try: 250 | data = await request.json() 251 | node_id = data.get("node_id") 252 | 253 | if node_id: 254 | if node_id in event_dict: 255 | event_dict[node_id].set() 256 | return web.Response(text=json.dumps({"status": "success"})) 257 | else: 258 | return web.Response(status=400, text=json.dumps({"error": "无效节点ID"})) 259 | except Exception as e: 260 | return web.Response(status=500, text=json.dumps({"error": str(e)})) 261 | 262 | 263 | lg_relight_dict = {} 264 | 265 | class LG_Relight_Ultra: 266 | _last_results = {} 267 | 268 | def __init__(self): 269 | self.node_id = None 270 | 271 | @classmethod 272 | def INPUT_TYPES(s): 273 | return { 274 | "required": { 275 | "bg_img": ("IMAGE",), 276 | "bg_depth_map": ("IMAGE",), 277 | "bg_normal_map": ("IMAGE",), 278 | "wait_timeout": ("INT", { 279 | "default": 120, 280 | "min": 5, 281 | "max": 300, 282 | "step": 1, 283 | "tooltip": "等待前端响应的最大时间(秒)\nMaximum time to wait for frontend response (seconds)" 284 | }), 285 | }, 286 | "optional": { 287 | "mask": ("MASK",), 288 | "skip_dialog": ("BOOLEAN", { 289 | "default": False, 290 | "tooltip": "开启后将不再显示光照编辑窗口,直接使用之前保存的光照设置(如果没有则使用默认设置)\nEnable to skip the lighting editor dialog and directly use previously saved lighting settings (or default settings if none exist)" 291 | }), 292 | }, 293 | "hidden": {"unique_id": "UNIQUE_ID"} 294 | } 295 | 296 | RETURN_TYPES = ("IMAGE",) 297 | FUNCTION = "relight_image" 298 | CATEGORY = CATEGORY_TYPE 299 | 300 | def relight_image(self, bg_img, bg_depth_map, bg_normal_map, wait_timeout, unique_id, mask=None, skip_dialog=False): 301 | try: 302 | self.node_id = str(unique_id) 303 | event = Event() 304 | lg_relight_dict[self.node_id] = event 305 | 306 | bg_pil = Image.fromarray((bg_img[0] * 255).byte().cpu().numpy()) 307 | depth_pil = Image.fromarray((bg_depth_map[0] * 255).byte().cpu().numpy()) 308 | normal_pil = Image.fromarray((bg_normal_map[0] * 255).byte().cpu().numpy()) 309 | 310 | bg_buffer = io.BytesIO() 311 | depth_buffer = io.BytesIO() 312 | normal_buffer = io.BytesIO() 313 | 314 | bg_pil.save(bg_buffer, format="PNG") 315 | depth_pil.save(depth_buffer, format="PNG") 316 | normal_pil.save(normal_buffer, format="PNG") 317 | 318 | data = { 319 | "node_id": self.node_id, 320 | "bg_image": base64.b64encode(bg_buffer.getvalue()).decode('utf-8'), 321 | "bg_depth_map": base64.b64encode(depth_buffer.getvalue()).decode('utf-8'), 322 | "bg_normal_map": base64.b64encode(normal_buffer.getvalue()).decode('utf-8'), 323 | "has_mask": mask is not None, 324 | "skip_dialog": skip_dialog 325 | } 326 | 327 | if mask is not None: 328 | try: 329 | mask_np = mask 330 | if isinstance(mask, torch.Tensor): 331 | mask_np = (mask * 255).byte().cpu().numpy() 332 | 333 | if len(mask_np.shape) == 3 and mask_np.shape[0] == 1: 334 | mask_np = mask_np[0] 335 | elif len(mask_np.shape) == 4 and mask_np.shape[0] == 1: 336 | mask_np = mask_np[0] 337 | 338 | if mask_np.dtype != np.uint8: 339 | mask_np = (mask_np * 255).astype(np.uint8) 340 | 341 | mask_pil = Image.fromarray(mask_np) 342 | mask_buffer = io.BytesIO() 343 | mask_pil.save(mask_buffer, format="PNG") 344 | data["mask"] = base64.b64encode(mask_buffer.getvalue()).decode('utf-8') 345 | 346 | except Exception: 347 | data["has_mask"] = False 348 | 349 | PromptServer.instance.send_sync("relight_image", data) 350 | 351 | wait_result = event.wait(timeout=wait_timeout) 352 | if not wait_result: 353 | return (bg_img,) 354 | 355 | if self.node_id in self._last_results: 356 | result_image = self._last_results[self.node_id] 357 | try: 358 | img = Image.open(io.BytesIO(result_image)) 359 | img_array = np.array(img) 360 | 361 | if len(img_array.shape) == 2: 362 | img_array = np.stack([img_array] * 3, axis=-1) 363 | elif img_array.shape[-1] == 4: 364 | img_array = img_array[..., :3] 365 | 366 | img_tensor = torch.from_numpy(img_array).float() / 255.0 367 | img_tensor = img_tensor.unsqueeze(0) 368 | return (img_tensor,) 369 | except Exception: 370 | return (bg_img,) 371 | 372 | return (bg_img,) 373 | 374 | finally: 375 | if self.node_id in lg_relight_dict: 376 | del lg_relight_dict[self.node_id] 377 | 378 | @PromptServer.instance.routes.post("/lg_relight/upload_result") 379 | async def upload_result(request): 380 | try: 381 | data = await request.post() 382 | node_id = str(data['node_id']) 383 | result_image = data['result_image'].file.read() 384 | 385 | LG_Relight_Ultra._last_results[node_id] = result_image 386 | 387 | if node_id in lg_relight_dict: 388 | lg_relight_dict[node_id].set() 389 | return web.json_response({"success": True}) 390 | else: 391 | return web.json_response({"error": "Node not found"}, status=404) 392 | 393 | except Exception as e: 394 | return web.json_response({"error": str(e)}, status=500) 395 | 396 | @PromptServer.instance.routes.post("/lg_relight_ultra/cancel") 397 | async def cancel_relight(request): 398 | try: 399 | data = await request.json() 400 | node_id = str(data.get("node_id")) 401 | 402 | if node_id in lg_relight_dict: 403 | lg_relight_dict[node_id].set() 404 | return web.json_response({"success": True}) 405 | else: 406 | return web.json_response({"success": True}) 407 | 408 | except Exception as e: 409 | return web.json_response({"success": False, "error": str(e)}) 410 | 411 | WEB_DIRECTORY = "web" 412 | 413 | NODE_CLASS_MAPPINGS = { 414 | "LG_Relight_Basic": LG_Relight_Basic, 415 | "LG_Relight": LG_Relight, 416 | "LG_Relight_Ultra": LG_Relight_Ultra 417 | } 418 | 419 | NODE_DISPLAY_NAME_MAPPINGS = { 420 | "LG_Relight_Basic": "🎈LG Relight Basic", 421 | "LG_Relight": "🎈LG Relight", 422 | "LG_Relight_Ultra": "🎈LG Relight Ultra" 423 | } 424 | -------------------------------------------------------------------------------- /example_workflow/LG_Relight.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 66, 3 | "last_link_id": 140, 4 | "nodes": [ 5 | { 6 | "id": 44, 7 | "type": "easy icLightApply", 8 | "pos": [ 9 | 3019.023681640625, 10 | 825.09130859375 11 | ], 12 | "size": [ 13 | 210, 14 | 146 15 | ], 16 | "flags": {}, 17 | "order": 13, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "model", 22 | "type": "MODEL", 23 | "link": 128 24 | }, 25 | { 26 | "name": "image", 27 | "type": "IMAGE", 28 | "link": 95 29 | }, 30 | { 31 | "name": "vae", 32 | "type": "VAE", 33 | "link": 102 34 | } 35 | ], 36 | "outputs": [ 37 | { 38 | "name": "model", 39 | "type": "MODEL", 40 | "links": [ 41 | 100 42 | ], 43 | "slot_index": 0 44 | }, 45 | { 46 | "name": "lighting_image", 47 | "type": "IMAGE", 48 | "links": null 49 | } 50 | ], 51 | "properties": { 52 | "Node name for S&R": "easy icLightApply" 53 | }, 54 | "widgets_values": [ 55 | "Foreground", 56 | "None", 57 | "Use Background Image", 58 | false 59 | ] 60 | }, 61 | { 62 | "id": 9, 63 | "type": "LoadImage", 64 | "pos": [ 65 | 2500, 66 | 0 67 | ], 68 | "size": [ 69 | 315, 70 | 314 71 | ], 72 | "flags": {}, 73 | "order": 0, 74 | "mode": 0, 75 | "inputs": [], 76 | "outputs": [ 77 | { 78 | "name": "IMAGE", 79 | "type": "IMAGE", 80 | "links": [ 81 | 88 82 | ], 83 | "slot_index": 0 84 | }, 85 | { 86 | "name": "MASK", 87 | "type": "MASK", 88 | "links": null 89 | } 90 | ], 91 | "properties": { 92 | "Node name for S&R": "LoadImage" 93 | }, 94 | "widgets_values": [ 95 | "328715300-12335218-186b-4c61-b43a-79aea9df8b21.png", 96 | "image" 97 | ] 98 | }, 99 | { 100 | "id": 60, 101 | "type": "PreviewImage", 102 | "pos": [ 103 | 2860, 104 | 0 105 | ], 106 | "size": [ 107 | 304.1535949707031, 108 | 316.8034973144531 109 | ], 110 | "flags": {}, 111 | "order": 11, 112 | "mode": 0, 113 | "inputs": [ 114 | { 115 | "name": "images", 116 | "type": "IMAGE", 117 | "link": 126 118 | } 119 | ], 120 | "outputs": [], 121 | "properties": { 122 | "Node name for S&R": "PreviewImage" 123 | }, 124 | "widgets_values": [] 125 | }, 126 | { 127 | "id": 10, 128 | "type": "PreviewImage", 129 | "pos": [ 130 | 3280, 131 | -10 132 | ], 133 | "size": [ 134 | 294.6190490722656, 135 | 319.8927001953125 136 | ], 137 | "flags": {}, 138 | "order": 12, 139 | "mode": 0, 140 | "inputs": [ 141 | { 142 | "name": "images", 143 | "type": "IMAGE", 144 | "link": 94 145 | } 146 | ], 147 | "outputs": [], 148 | "properties": { 149 | "Node name for S&R": "PreviewImage" 150 | }, 151 | "widgets_values": [] 152 | }, 153 | { 154 | "id": 43, 155 | "type": "LG_Relight", 156 | "pos": [ 157 | 3310, 158 | 370 159 | ], 160 | "size": [ 161 | 210, 162 | 102 163 | ], 164 | "flags": {}, 165 | "order": 10, 166 | "mode": 0, 167 | "inputs": [ 168 | { 169 | "name": "image", 170 | "type": "IMAGE", 171 | "link": 118 172 | }, 173 | { 174 | "name": "normals", 175 | "type": "IMAGE", 176 | "link": 93 177 | } 178 | ], 179 | "outputs": [ 180 | { 181 | "name": "IMAGE", 182 | "type": "IMAGE", 183 | "links": [ 184 | 94, 185 | 95, 186 | 113 187 | ], 188 | "slot_index": 0 189 | } 190 | ], 191 | "properties": { 192 | "Node name for S&R": "LG_Relight" 193 | }, 194 | "widgets_values": [ 195 | 8349887901936731, 196 | null 197 | ] 198 | }, 199 | { 200 | "id": 54, 201 | "type": "LayerMask: MaskPreview", 202 | "pos": [ 203 | 3720, 204 | -10 205 | ], 206 | "size": [ 207 | 277.20001220703125, 208 | 246 209 | ], 210 | "flags": {}, 211 | "order": 8, 212 | "mode": 0, 213 | "inputs": [ 214 | { 215 | "name": "mask", 216 | "type": "MASK", 217 | "link": 111 218 | } 219 | ], 220 | "outputs": [], 221 | "properties": { 222 | "Node name for S&R": "LayerMask: MaskPreview" 223 | }, 224 | "widgets_values": [], 225 | "color": "rgba(27, 80, 119, 0.7)" 226 | }, 227 | { 228 | "id": 48, 229 | "type": "CheckpointLoaderSimple", 230 | "pos": [ 231 | 2359.023681640625, 232 | 815.0912475585938 233 | ], 234 | "size": [ 235 | 315, 236 | 98 237 | ], 238 | "flags": {}, 239 | "order": 1, 240 | "mode": 0, 241 | "inputs": [], 242 | "outputs": [ 243 | { 244 | "name": "MODEL", 245 | "type": "MODEL", 246 | "links": [ 247 | 127 248 | ], 249 | "slot_index": 0 250 | }, 251 | { 252 | "name": "CLIP", 253 | "type": "CLIP", 254 | "links": [ 255 | 104, 256 | 105 257 | ], 258 | "slot_index": 1 259 | }, 260 | { 261 | "name": "VAE", 262 | "type": "VAE", 263 | "links": [ 264 | 102, 265 | 115, 266 | 116 267 | ], 268 | "slot_index": 2 269 | } 270 | ], 271 | "properties": { 272 | "Node name for S&R": "CheckpointLoaderSimple" 273 | }, 274 | "widgets_values": [ 275 | "STOIQONewreality_SD10.safetensors" 276 | ] 277 | }, 278 | { 279 | "id": 49, 280 | "type": "KSamplerAdvanced", 281 | "pos": [ 282 | 3269.3583984375, 283 | 832.4984130859375 284 | ], 285 | "size": [ 286 | 315, 287 | 546 288 | ], 289 | "flags": {}, 290 | "order": 15, 291 | "mode": 0, 292 | "inputs": [ 293 | { 294 | "name": "model", 295 | "type": "MODEL", 296 | "link": 100 297 | }, 298 | { 299 | "name": "positive", 300 | "type": "CONDITIONING", 301 | "link": 106 302 | }, 303 | { 304 | "name": "negative", 305 | "type": "CONDITIONING", 306 | "link": 107 307 | }, 308 | { 309 | "name": "latent_image", 310 | "type": "LATENT", 311 | "link": 114 312 | } 313 | ], 314 | "outputs": [ 315 | { 316 | "name": "LATENT", 317 | "type": "LATENT", 318 | "links": [ 319 | 101 320 | ], 321 | "slot_index": 0 322 | } 323 | ], 324 | "properties": { 325 | "Node name for S&R": "KSamplerAdvanced" 326 | }, 327 | "widgets_values": [ 328 | "enable", 329 | 884078753467353, 330 | "fixed", 331 | 8, 332 | 1.5, 333 | "lcm", 334 | "exponential", 335 | 4, 336 | 10000, 337 | "disable" 338 | ] 339 | }, 340 | { 341 | "id": 56, 342 | "type": "VAEEncode", 343 | "pos": [ 344 | 3029.023681640625, 345 | 1045.09130859375 346 | ], 347 | "size": [ 348 | 198.2010040283203, 349 | 46 350 | ], 351 | "flags": {}, 352 | "order": 14, 353 | "mode": 0, 354 | "inputs": [ 355 | { 356 | "name": "pixels", 357 | "type": "IMAGE", 358 | "link": 113 359 | }, 360 | { 361 | "name": "vae", 362 | "type": "VAE", 363 | "link": 115 364 | } 365 | ], 366 | "outputs": [ 367 | { 368 | "name": "LATENT", 369 | "type": "LATENT", 370 | "links": [ 371 | 114 372 | ], 373 | "slot_index": 0 374 | } 375 | ], 376 | "properties": { 377 | "Node name for S&R": "VAEEncode" 378 | }, 379 | "widgets_values": [] 380 | }, 381 | { 382 | "id": 51, 383 | "type": "CLIPTextEncode", 384 | "pos": [ 385 | 3019.023681640625, 386 | 1155.09130859375 387 | ], 388 | "size": [ 389 | 211.6783905029297, 390 | 100.8108139038086 391 | ], 392 | "flags": {}, 393 | "order": 4, 394 | "mode": 0, 395 | "inputs": [ 396 | { 397 | "name": "clip", 398 | "type": "CLIP", 399 | "link": 104 400 | } 401 | ], 402 | "outputs": [ 403 | { 404 | "name": "CONDITIONING", 405 | "type": "CONDITIONING", 406 | "links": [ 407 | 106 408 | ], 409 | "slot_index": 0 410 | } 411 | ], 412 | "properties": { 413 | "Node name for S&R": "CLIPTextEncode" 414 | }, 415 | "widgets_values": [ 416 | "bright light" 417 | ] 418 | }, 419 | { 420 | "id": 52, 421 | "type": "CLIPTextEncode", 422 | "pos": [ 423 | 3017.712646484375, 424 | 1301.9813232421875 425 | ], 426 | "size": [ 427 | 218.74574279785156, 428 | 84.7579345703125 429 | ], 430 | "flags": {}, 431 | "order": 5, 432 | "mode": 0, 433 | "inputs": [ 434 | { 435 | "name": "clip", 436 | "type": "CLIP", 437 | "link": 105 438 | } 439 | ], 440 | "outputs": [ 441 | { 442 | "name": "CONDITIONING", 443 | "type": "CONDITIONING", 444 | "links": [ 445 | 107 446 | ], 447 | "slot_index": 0 448 | } 449 | ], 450 | "properties": { 451 | "Node name for S&R": "CLIPTextEncode" 452 | }, 453 | "widgets_values": [ 454 | "" 455 | ] 456 | }, 457 | { 458 | "id": 47, 459 | "type": "VAEDecode", 460 | "pos": [ 461 | 3692.86962890625, 462 | 1072.046875 463 | ], 464 | "size": [ 465 | 140, 466 | 46 467 | ], 468 | "flags": {}, 469 | "order": 16, 470 | "mode": 0, 471 | "inputs": [ 472 | { 473 | "name": "samples", 474 | "type": "LATENT", 475 | "link": 101 476 | }, 477 | { 478 | "name": "vae", 479 | "type": "VAE", 480 | "link": 116 481 | } 482 | ], 483 | "outputs": [ 484 | { 485 | "name": "IMAGE", 486 | "type": "IMAGE", 487 | "links": [ 488 | 131 489 | ], 490 | "slot_index": 0 491 | } 492 | ], 493 | "properties": { 494 | "Node name for S&R": "VAEDecode" 495 | }, 496 | "widgets_values": [] 497 | }, 498 | { 499 | "id": 59, 500 | "type": "Image Comparer (rgthree)", 501 | "pos": [ 502 | 4006.73583984375, 503 | 834.6034545898438 504 | ], 505 | "size": [ 506 | 711.7552490234375, 507 | 721.58154296875 508 | ], 509 | "flags": {}, 510 | "order": 18, 511 | "mode": 0, 512 | "inputs": [ 513 | { 514 | "name": "image_a", 515 | "type": "IMAGE", 516 | "link": 134, 517 | "dir": 3 518 | }, 519 | { 520 | "name": "image_b", 521 | "type": "IMAGE", 522 | "link": 135, 523 | "dir": 3 524 | } 525 | ], 526 | "outputs": [], 527 | "properties": { 528 | "comparer_mode": "Slide" 529 | }, 530 | "widgets_values": [ 531 | [ 532 | { 533 | "name": "A", 534 | "selected": true, 535 | "url": "/api/view?filename=rgthree.compare._temp_obfhd_00001_.png&type=temp&subfolder=&rand=0.4328154917194871" 536 | }, 537 | { 538 | "name": "B", 539 | "selected": true, 540 | "url": "/api/view?filename=rgthree.compare._temp_obfhd_00002_.png&type=temp&subfolder=&rand=0.364801374584669" 541 | } 542 | ] 543 | ] 544 | }, 545 | { 546 | "id": 53, 547 | "type": "InspyrenetRembg", 548 | "pos": [ 549 | 3677.6162109375, 550 | 375.69830322265625 551 | ], 552 | "size": [ 553 | 315, 554 | 78 555 | ], 556 | "flags": {}, 557 | "order": 6, 558 | "mode": 0, 559 | "inputs": [ 560 | { 561 | "name": "image", 562 | "type": "IMAGE", 563 | "link": 109 564 | } 565 | ], 566 | "outputs": [ 567 | { 568 | "name": "IMAGE", 569 | "type": "IMAGE", 570 | "links": null, 571 | "slot_index": 0 572 | }, 573 | { 574 | "name": "MASK", 575 | "type": "MASK", 576 | "links": [ 577 | 111, 578 | 133 579 | ], 580 | "slot_index": 1 581 | } 582 | ], 583 | "properties": { 584 | "Node name for S&R": "InspyrenetRembg" 585 | }, 586 | "widgets_values": [ 587 | "default" 588 | ] 589 | }, 590 | { 591 | "id": 62, 592 | "type": "easy imageDetailTransfer", 593 | "pos": [ 594 | 3632.468505859375, 595 | 839.0242309570312 596 | ], 597 | "size": [ 598 | 315, 599 | 194 600 | ], 601 | "flags": {}, 602 | "order": 17, 603 | "mode": 0, 604 | "inputs": [ 605 | { 606 | "name": "target", 607 | "type": "IMAGE", 608 | "link": 131 609 | }, 610 | { 611 | "name": "source", 612 | "type": "IMAGE", 613 | "link": 132 614 | }, 615 | { 616 | "name": "mask", 617 | "type": "MASK", 618 | "link": 133, 619 | "shape": 7 620 | } 621 | ], 622 | "outputs": [ 623 | { 624 | "name": "image", 625 | "type": "IMAGE", 626 | "links": [ 627 | 134 628 | ], 629 | "slot_index": 0 630 | } 631 | ], 632 | "properties": { 633 | "Node name for S&R": "easy imageDetailTransfer" 634 | }, 635 | "widgets_values": [ 636 | "add", 637 | 1, 638 | 1, 639 | "Hide", 640 | "ComfyUI" 641 | ] 642 | }, 643 | { 644 | "id": 61, 645 | "type": "LoraLoaderModelOnly", 646 | "pos": [ 647 | 2738.29736328125, 648 | 827.00927734375 649 | ], 650 | "size": [ 651 | 210, 652 | 82 653 | ], 654 | "flags": {}, 655 | "order": 3, 656 | "mode": 4, 657 | "inputs": [ 658 | { 659 | "name": "model", 660 | "type": "MODEL", 661 | "link": 127 662 | } 663 | ], 664 | "outputs": [ 665 | { 666 | "name": "MODEL", 667 | "type": "MODEL", 668 | "links": [ 669 | 128 670 | ], 671 | "slot_index": 0 672 | } 673 | ], 674 | "properties": { 675 | "Node name for S&R": "LoraLoaderModelOnly" 676 | }, 677 | "widgets_values": [ 678 | "LCM_LoRA_Weights_SD15.safetensors", 679 | 0.4 680 | ] 681 | }, 682 | { 683 | "id": 14, 684 | "type": "DSINE-NormalMapPreprocessor", 685 | "pos": [ 686 | 2910, 687 | 380 688 | ], 689 | "size": [ 690 | 210, 691 | 102 692 | ], 693 | "flags": {}, 694 | "order": 9, 695 | "mode": 0, 696 | "inputs": [ 697 | { 698 | "name": "image", 699 | "type": "IMAGE", 700 | "link": 89 701 | }, 702 | { 703 | "name": "resolution", 704 | "type": "INT", 705 | "link": 138, 706 | "widget": { 707 | "name": "resolution" 708 | }, 709 | "shape": 7 710 | } 711 | ], 712 | "outputs": [ 713 | { 714 | "name": "IMAGE", 715 | "type": "IMAGE", 716 | "links": [ 717 | 93, 718 | 126 719 | ], 720 | "slot_index": 0 721 | } 722 | ], 723 | "properties": { 724 | "Node name for S&R": "DSINE-NormalMapPreprocessor" 725 | }, 726 | "widgets_values": [ 727 | 0.01, 728 | 1, 729 | 512 730 | ] 731 | }, 732 | { 733 | "id": 42, 734 | "type": "LayerUtility: ImageScaleByAspectRatio V2", 735 | "pos": [ 736 | 2480, 737 | 370 738 | ], 739 | "size": [ 740 | 336, 741 | 330 742 | ], 743 | "flags": {}, 744 | "order": 2, 745 | "mode": 0, 746 | "inputs": [ 747 | { 748 | "name": "image", 749 | "type": "IMAGE", 750 | "link": 88, 751 | "shape": 7 752 | }, 753 | { 754 | "name": "mask", 755 | "type": "MASK", 756 | "link": null, 757 | "shape": 7 758 | } 759 | ], 760 | "outputs": [ 761 | { 762 | "name": "image", 763 | "type": "IMAGE", 764 | "links": [ 765 | 89, 766 | 109, 767 | 118, 768 | 132, 769 | 135 770 | ], 771 | "slot_index": 0 772 | }, 773 | { 774 | "name": "mask", 775 | "type": "MASK", 776 | "links": null, 777 | "slot_index": 1 778 | }, 779 | { 780 | "name": "original_size", 781 | "type": "BOX", 782 | "links": null 783 | }, 784 | { 785 | "name": "width", 786 | "type": "INT", 787 | "links": [ 788 | 136 789 | ], 790 | "slot_index": 3 791 | }, 792 | { 793 | "name": "height", 794 | "type": "INT", 795 | "links": [ 796 | 137 797 | ], 798 | "slot_index": 4 799 | } 800 | ], 801 | "properties": { 802 | "Node name for S&R": "LayerUtility: ImageScaleByAspectRatio V2" 803 | }, 804 | "widgets_values": [ 805 | "original", 806 | 1, 807 | 1, 808 | "crop", 809 | "lanczos", 810 | "64", 811 | "longest", 812 | 1280, 813 | "#000000" 814 | ], 815 | "color": "rgba(38, 73, 116, 0.7)" 816 | }, 817 | { 818 | "id": 64, 819 | "type": "ImpactMinMax", 820 | "pos": [ 821 | 2910.306640625, 822 | 546.4757080078125 823 | ], 824 | "size": [ 825 | 210, 826 | 78 827 | ], 828 | "flags": {}, 829 | "order": 7, 830 | "mode": 0, 831 | "inputs": [ 832 | { 833 | "name": "a", 834 | "type": "*", 835 | "link": 136 836 | }, 837 | { 838 | "name": "b", 839 | "type": "*", 840 | "link": 137 841 | } 842 | ], 843 | "outputs": [ 844 | { 845 | "name": "INT", 846 | "type": "INT", 847 | "links": [ 848 | 138 849 | ], 850 | "slot_index": 0 851 | } 852 | ], 853 | "properties": { 854 | "Node name for S&R": "ImpactMinMax" 855 | }, 856 | "widgets_values": [ 857 | false 858 | ] 859 | } 860 | ], 861 | "links": [ 862 | [ 863 | 88, 864 | 9, 865 | 0, 866 | 42, 867 | 0, 868 | "IMAGE" 869 | ], 870 | [ 871 | 89, 872 | 42, 873 | 0, 874 | 14, 875 | 0, 876 | "IMAGE" 877 | ], 878 | [ 879 | 93, 880 | 14, 881 | 0, 882 | 43, 883 | 1, 884 | "IMAGE" 885 | ], 886 | [ 887 | 94, 888 | 43, 889 | 0, 890 | 10, 891 | 0, 892 | "IMAGE" 893 | ], 894 | [ 895 | 95, 896 | 43, 897 | 0, 898 | 44, 899 | 1, 900 | "IMAGE" 901 | ], 902 | [ 903 | 100, 904 | 44, 905 | 0, 906 | 49, 907 | 0, 908 | "MODEL" 909 | ], 910 | [ 911 | 101, 912 | 49, 913 | 0, 914 | 47, 915 | 0, 916 | "LATENT" 917 | ], 918 | [ 919 | 102, 920 | 48, 921 | 2, 922 | 44, 923 | 2, 924 | "VAE" 925 | ], 926 | [ 927 | 104, 928 | 48, 929 | 1, 930 | 51, 931 | 0, 932 | "CLIP" 933 | ], 934 | [ 935 | 105, 936 | 48, 937 | 1, 938 | 52, 939 | 0, 940 | "CLIP" 941 | ], 942 | [ 943 | 106, 944 | 51, 945 | 0, 946 | 49, 947 | 1, 948 | "CONDITIONING" 949 | ], 950 | [ 951 | 107, 952 | 52, 953 | 0, 954 | 49, 955 | 2, 956 | "CONDITIONING" 957 | ], 958 | [ 959 | 109, 960 | 42, 961 | 0, 962 | 53, 963 | 0, 964 | "IMAGE" 965 | ], 966 | [ 967 | 111, 968 | 53, 969 | 1, 970 | 54, 971 | 0, 972 | "MASK" 973 | ], 974 | [ 975 | 113, 976 | 43, 977 | 0, 978 | 56, 979 | 0, 980 | "IMAGE" 981 | ], 982 | [ 983 | 114, 984 | 56, 985 | 0, 986 | 49, 987 | 3, 988 | "LATENT" 989 | ], 990 | [ 991 | 115, 992 | 48, 993 | 2, 994 | 56, 995 | 1, 996 | "VAE" 997 | ], 998 | [ 999 | 116, 1000 | 48, 1001 | 2, 1002 | 47, 1003 | 1, 1004 | "VAE" 1005 | ], 1006 | [ 1007 | 118, 1008 | 42, 1009 | 0, 1010 | 43, 1011 | 0, 1012 | "IMAGE" 1013 | ], 1014 | [ 1015 | 126, 1016 | 14, 1017 | 0, 1018 | 60, 1019 | 0, 1020 | "IMAGE" 1021 | ], 1022 | [ 1023 | 127, 1024 | 48, 1025 | 0, 1026 | 61, 1027 | 0, 1028 | "MODEL" 1029 | ], 1030 | [ 1031 | 128, 1032 | 61, 1033 | 0, 1034 | 44, 1035 | 0, 1036 | "MODEL" 1037 | ], 1038 | [ 1039 | 131, 1040 | 47, 1041 | 0, 1042 | 62, 1043 | 0, 1044 | "IMAGE" 1045 | ], 1046 | [ 1047 | 132, 1048 | 42, 1049 | 0, 1050 | 62, 1051 | 1, 1052 | "IMAGE" 1053 | ], 1054 | [ 1055 | 133, 1056 | 53, 1057 | 1, 1058 | 62, 1059 | 2, 1060 | "MASK" 1061 | ], 1062 | [ 1063 | 134, 1064 | 62, 1065 | 0, 1066 | 59, 1067 | 0, 1068 | "IMAGE" 1069 | ], 1070 | [ 1071 | 135, 1072 | 42, 1073 | 0, 1074 | 59, 1075 | 1, 1076 | "IMAGE" 1077 | ], 1078 | [ 1079 | 136, 1080 | 42, 1081 | 3, 1082 | 64, 1083 | 0, 1084 | "*" 1085 | ], 1086 | [ 1087 | 137, 1088 | 42, 1089 | 4, 1090 | 64, 1091 | 1, 1092 | "*" 1093 | ], 1094 | [ 1095 | 138, 1096 | 64, 1097 | 0, 1098 | 14, 1099 | 1, 1100 | "INT" 1101 | ] 1102 | ], 1103 | "groups": [], 1104 | "config": {}, 1105 | "extra": { 1106 | "ds": { 1107 | "scale": 1.015255979947733, 1108 | "offset": [ 1109 | -2224.081617579055, 1110 | 122.19710799320313 1111 | ] 1112 | }, 1113 | "workspace_info": { 1114 | "id": "IWpC6ZCDJPX-dqU6dlHuP" 1115 | } 1116 | }, 1117 | "version": 0.4 1118 | } -------------------------------------------------------------------------------- /web/relight_v3.js: -------------------------------------------------------------------------------- 1 | import { app } from "../../scripts/app.js"; 2 | import { api } from "../../scripts/api.js"; 3 | function createRelightModal() { 4 | const modal = document.createElement("dialog"); 5 | modal.id = "lg-relight-v3-modal"; 6 | modal.innerHTML = ` 7 |
8 |
9 |

LG_Relight

10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 |
61 |
62 |
63 | 64 | 65 |
66 |
67 |
68 |
69 |
70 | `; 71 | document.body.appendChild(modal); 72 | return modal; 73 | } 74 | const style = document.createElement("style"); 75 | style.textContent = ` 76 | #lg-relight-v3-modal { 77 | border: none; 78 | border-radius: 8px; 79 | padding: 0; 80 | background: #2a2a2a; 81 | width: 90vw; 82 | height: 90vh; 83 | max-width: 90vw; 84 | max-height: 90vh; 85 | margin: 0; 86 | position: fixed; 87 | top: 50%; 88 | left: 50%; 89 | transform: translate(-50%, -50%); 90 | } 91 | dialog::backdrop { 92 | background-color: rgba(0, 0, 0, 0.5); 93 | } 94 | .relight-container { 95 | width: 100%; 96 | height: 100%; 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | .relight-header { 101 | display: flex; 102 | justify-content: space-between; 103 | align-items: center; 104 | padding: 8px 16px; 105 | background: #333; 106 | border-bottom: 1px solid #444; 107 | } 108 | .relight-header h3 { 109 | margin: 0; 110 | color: #fff; 111 | } 112 | .close-button { 113 | background: none; 114 | border: none; 115 | color: #fff; 116 | font-size: 24px; 117 | cursor: pointer; 118 | } 119 | .relight-content { 120 | padding: 0; 121 | display: flex; 122 | flex-direction: row; 123 | height: calc(100% - 44px); 124 | overflow: hidden; 125 | } 126 | .relight-preview { 127 | position: relative; 128 | overflow: hidden; 129 | background: #1a1a1a; 130 | display: flex; 131 | flex-direction: column; 132 | justify-content: center; 133 | align-items: center; 134 | flex: 1; 135 | height: 100%; 136 | cursor: crosshair; 137 | } 138 | #relight-canvas { 139 | max-width: 100%; 140 | max-height: 100%; 141 | object-fit: contain; 142 | } 143 | .relight-controls { 144 | display: flex; 145 | flex-direction: column; 146 | justify-content: space-between; 147 | width: 300px; 148 | padding: 20px; 149 | height: 100%; 150 | box-sizing: border-box; 151 | background: #2a2a2a; 152 | overflow-y: visible; 153 | } 154 | .slider-group { 155 | display: flex; 156 | flex-direction: column; 157 | gap: 16px; 158 | } 159 | .control-row { 160 | display: flex; 161 | align-items: center; 162 | gap: 8px; 163 | } 164 | .control-row label { 165 | min-width: 100px; 166 | white-space: nowrap; 167 | } 168 | .control-row input[type="range"] { 169 | width: 120px; 170 | } 171 | .reset-btn, .reset-color-btn { 172 | padding: 2px 8px; 173 | background: #555; 174 | border: none; 175 | border-radius: 4px; 176 | color: white; 177 | cursor: pointer; 178 | white-space: nowrap; 179 | } 180 | .bottom-controls { 181 | margin-top: 30px; 182 | } 183 | .color-controls { 184 | display: flex; 185 | flex-direction: column; 186 | gap: 15px; 187 | margin-bottom: 20px; 188 | } 189 | .color-row { 190 | display: flex; 191 | align-items: center; 192 | gap: 8px; 193 | } 194 | .color-row label { 195 | min-width: 80px; 196 | } 197 | .action-buttons { 198 | display: flex; 199 | gap: 10px; 200 | justify-content: center; 201 | } 202 | .action-buttons button { 203 | padding: 8px 16px; 204 | border: none; 205 | border-radius: 4px; 206 | cursor: pointer; 207 | min-width: 80px; 208 | } 209 | #apply-relight { 210 | background: #2a8af6; 211 | color: white; 212 | } 213 | #cancel-relight { 214 | background: #666; 215 | color: white; 216 | } 217 | `; 218 | document.head.appendChild(style); 219 | class RelightProcessor { 220 | constructor() { 221 | this.modal = createRelightModal(); 222 | this.canvas = this.modal.querySelector("#relight-canvas"); 223 | this.ctx = this.canvas.getContext("2d"); 224 | this.originalImage = null; 225 | this.normalsImage = null; 226 | this.processedImage = null; 227 | this.values = { 228 | x: 0.0, 229 | y: 0.0, 230 | z: 1.0, 231 | brightness: 1.0, 232 | shadowRange: 1.0, 233 | shadowStrength: 1.0, 234 | highlightRange: 1.0, 235 | highlightStrength: 1.0, 236 | highlightColor: [1.0, 1.0, 1.0], 237 | shadowColor: [0.0, 0.0, 0.0] 238 | }; 239 | this.isDragging = false; 240 | this.setupEventListeners(); 241 | } 242 | setupEventListeners() { 243 | this.modal.querySelector(".close-button").addEventListener("click", () => { 244 | this.cleanupAndClose(true); 245 | }); 246 | this.modal.querySelector("#cancel-relight").addEventListener("click", () => { 247 | this.cleanupAndClose(true); 248 | }); 249 | this.modal.querySelector("#apply-relight").addEventListener("click", () => { 250 | this.applyRelight(); 251 | }); 252 | this.modal.addEventListener("keydown", (e) => { 253 | if (e.key === "Escape") { 254 | this.cleanupAndClose(true); 255 | } 256 | }); 257 | const previewArea = this.modal.querySelector(".relight-preview"); 258 | previewArea.addEventListener("mousedown", (e) => { 259 | this.isDragging = true; 260 | this.updateLightDirection(e); 261 | }); 262 | previewArea.addEventListener("mousemove", (e) => { 263 | if (this.isDragging) { 264 | this.updateLightDirection(e); 265 | } 266 | }); 267 | previewArea.addEventListener("mouseup", () => { 268 | this.isDragging = false; 269 | }); 270 | previewArea.addEventListener("mouseleave", () => { 271 | this.isDragging = false; 272 | }); 273 | const sliders = { 274 | "z": this.modal.querySelector("#z-slider"), 275 | "brightness": this.modal.querySelector("#brightness-slider"), 276 | "shadow-range": this.modal.querySelector("#shadow-range-slider"), 277 | "shadow-strength": this.modal.querySelector("#shadow-strength-slider"), 278 | "highlight-range": this.modal.querySelector("#highlight-range-slider"), 279 | "highlight-strength": this.modal.querySelector("#highlight-strength-slider") 280 | }; 281 | for (const [key, slider] of Object.entries(sliders)) { 282 | slider.addEventListener("input", () => { 283 | this.updateValues(); 284 | this.updateUI(); 285 | this.processAndUpdatePreview(); 286 | }); 287 | } 288 | this.modal.querySelector("#highlight-color").addEventListener("input", () => { 289 | this.updateValues(); 290 | this.processAndUpdatePreview(); 291 | }); 292 | this.modal.querySelector("#shadow-color").addEventListener("input", () => { 293 | this.updateValues(); 294 | this.processAndUpdatePreview(); 295 | }); 296 | this.modal.querySelectorAll(".reset-btn").forEach(btn => { 297 | btn.addEventListener("click", () => { 298 | const sliderName = btn.dataset.slider; 299 | const slider = sliders[sliderName]; 300 | if (sliderName === "z") { 301 | slider.value = 1000; 302 | } else { 303 | slider.value = 100; 304 | } 305 | this.updateValues(); 306 | this.updateUI(); 307 | this.processAndUpdatePreview(); 308 | }); 309 | }); 310 | this.modal.querySelector("[data-color='highlight']").addEventListener("click", () => { 311 | this.modal.querySelector("#highlight-color").value = "#FFFFFF"; 312 | this.updateValues(); 313 | this.processAndUpdatePreview(); 314 | }); 315 | this.modal.querySelector("[data-color='shadow']").addEventListener("click", () => { 316 | this.modal.querySelector("#shadow-color").value = "#000000"; 317 | this.updateValues(); 318 | this.processAndUpdatePreview(); 319 | }); 320 | } 321 | updateLightDirection(e) { 322 | const rect = this.canvas.getBoundingClientRect(); 323 | const centerX = rect.width / 2; 324 | const centerY = rect.height / 2; 325 | const x = (e.clientX - rect.left - centerX) / centerX; 326 | const y = -(e.clientY - rect.top - centerY) / centerY; 327 | this.values.x = Math.max(-1, Math.min(1, x)); 328 | this.values.y = Math.max(-1, Math.min(1, y)); 329 | this.updateUI(); 330 | this.processAndUpdatePreview(); 331 | } 332 | updateValues() { 333 | const getSliderValue = (id, scale = 1000) => { 334 | return parseFloat(this.modal.querySelector(id).value) / scale; 335 | }; 336 | this.values.z = getSliderValue("#z-slider"); 337 | this.values.brightness = getSliderValue("#brightness-slider", 100); 338 | this.values.shadowRange = 2.0 - getSliderValue("#shadow-range-slider", 100); 339 | this.values.shadowStrength = getSliderValue("#shadow-strength-slider", 100); 340 | this.values.highlightRange = getSliderValue("#highlight-range-slider", 100); 341 | this.values.highlightStrength = getSliderValue("#highlight-strength-slider", 100); 342 | const hexToRgb = (hex) => { 343 | const r = parseInt(hex.substring(1, 3), 16) / 255; 344 | const g = parseInt(hex.substring(3, 5), 16) / 255; 345 | const b = parseInt(hex.substring(5, 7), 16) / 255; 346 | return [r, g, b]; 347 | }; 348 | this.values.highlightColor = hexToRgb(this.modal.querySelector("#highlight-color").value); 349 | this.values.shadowColor = hexToRgb(this.modal.querySelector("#shadow-color").value); 350 | } 351 | updateUI() { 352 | this.modal.querySelector("#z-value").textContent = this.values.z.toFixed(3); 353 | this.modal.querySelector("#brightness-value").textContent = this.values.brightness.toFixed(3); 354 | this.modal.querySelector("#shadow-range-value").textContent = (2.0 - this.values.shadowRange).toFixed(3); 355 | this.modal.querySelector("#shadow-strength-value").textContent = this.values.shadowStrength.toFixed(3); 356 | this.modal.querySelector("#highlight-range-value").textContent = this.values.highlightRange.toFixed(3); 357 | this.modal.querySelector("#highlight-strength-value").textContent = this.values.highlightStrength.toFixed(3); 358 | } 359 | async cleanupAndClose(cancelled = false) { 360 | if (cancelled && this.currentNodeId) { 361 | try { 362 | await api.fetchApi("/lg_relight/cancel", { 363 | method: "POST", 364 | headers: { 365 | "Content-Type": "application/json", 366 | }, 367 | body: JSON.stringify({ 368 | node_id: this.currentNodeId 369 | }) 370 | }); 371 | } catch (error) { 372 | console.error("发送取消信号失败:", error); 373 | } 374 | } 375 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 376 | this.originalImage = null; 377 | this.normalsImage = null; 378 | this.processedImage = null; 379 | this.modal.close(); 380 | } 381 | loadImages(imageData, normalsData) { 382 | return new Promise((resolve, reject) => { 383 | const imgOriginal = new Image(); 384 | const imgNormals = new Image(); 385 | let loadedCount = 0; 386 | const checkAllLoaded = () => { 387 | loadedCount++; 388 | if (loadedCount === 2) { 389 | resolve({ original: imgOriginal, normals: imgNormals }); 390 | } 391 | }; 392 | imgOriginal.onload = checkAllLoaded; 393 | imgNormals.onload = checkAllLoaded; 394 | imgOriginal.onerror = () => reject(new Error("Failed to load original image")); 395 | imgNormals.onerror = () => reject(new Error("Failed to load normal map")); 396 | imgOriginal.src = imageData; 397 | imgNormals.src = normalsData; 398 | }); 399 | } 400 | processAndUpdatePreview() { 401 | if (!this.originalImage || !this.normalsImage) return; 402 | const offscreenCanvas = document.createElement("canvas"); 403 | offscreenCanvas.width = this.originalImage.width; 404 | offscreenCanvas.height = this.originalImage.height; 405 | const offCtx = offscreenCanvas.getContext("2d"); 406 | offCtx.drawImage(this.originalImage, 0, 0); 407 | const imageData = offCtx.getImageData(0, 0, offscreenCanvas.width, offscreenCanvas.height); 408 | const normalsCanvas = document.createElement("canvas"); 409 | normalsCanvas.width = this.normalsImage.width; 410 | normalsCanvas.height = this.normalsImage.height; 411 | const normalsCtx = normalsCanvas.getContext("2d"); 412 | normalsCtx.drawImage(this.normalsImage, 0, 0); 413 | const normalsData = normalsCtx.getImageData(0, 0, normalsCanvas.width, normalsCanvas.height); 414 | this.processImage(imageData, normalsData); 415 | offCtx.putImageData(imageData, 0, 0); 416 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 417 | this.canvas.width = offscreenCanvas.width; 418 | this.canvas.height = offscreenCanvas.height; 419 | this.ctx.drawImage(offscreenCanvas, 0, 0); 420 | this.processedImage = offscreenCanvas; 421 | } 422 | processImage(imageData, normalsData) { 423 | const { x, y, z, brightness, shadowRange, shadowStrength, highlightRange, highlightStrength, highlightColor, shadowColor } = this.values; 424 | const magnitude = Math.sqrt(x*x + y*y + z*z); 425 | const lightX = magnitude > 0 ? x / magnitude : 0; 426 | const lightY = magnitude > 0 ? y / magnitude : 0; 427 | const lightZ = magnitude > 0 ? z / magnitude : 0; 428 | const imgData = imageData.data; 429 | const norData = normalsData.data; 430 | const width = imageData.width; 431 | const height = imageData.height; 432 | const shadowThreshold = 1.0 - shadowRange; 433 | const highlightThreshold = 1.0 - highlightRange; 434 | for (let y = 0; y < height; y++) { 435 | for (let x = 0; x < width; x++) { 436 | const i = (y * width + x) * 4; 437 | let nx = norData[i] / 127.5 - 1.0; 438 | let ny = norData[i + 1] / 127.5 - 1.0; 439 | let nz = norData[i + 2] / 127.5 - 1.0; 440 | let diffuse = nx * lightX + ny * lightY + nz * lightZ; 441 | diffuse = (diffuse + 1.0) * 0.5; 442 | let shadowMask = Math.min(Math.max((diffuse - shadowThreshold) / Math.max(shadowRange, 0.000001), 0), 1); 443 | let highlightMask = Math.min(Math.max((diffuse - highlightThreshold) / Math.max(highlightRange, 0.000001), 0), 1); 444 | let lightIntensity = 1.0; 445 | if (shadowStrength !== 1.0) { 446 | lightIntensity = lightIntensity * ( 447 | shadowMask + 448 | (1.0 - shadowMask) * (2.0 - shadowStrength) 449 | ); 450 | } 451 | if (highlightStrength !== 1.0) { 452 | const highlightOffset = highlightStrength - 1.0; 453 | lightIntensity = lightIntensity + highlightMask * highlightOffset; 454 | } 455 | let finalR = imgData[i]; 456 | let finalG = imgData[i + 1]; 457 | let finalB = imgData[i + 2]; 458 | if (highlightColor[0] !== 1.0 || highlightColor[1] !== 1.0 || highlightColor[2] !== 1.0 || 459 | shadowColor[0] !== 0.0 || shadowColor[1] !== 0.0 || shadowColor[2] !== 0.0) { 460 | const colorR = shadowMask * highlightColor[0] + (1.0 - shadowMask) * shadowColor[0]; 461 | const colorG = shadowMask * highlightColor[1] + (1.0 - shadowMask) * shadowColor[1]; 462 | const colorB = shadowMask * highlightColor[2] + (1.0 - shadowMask) * shadowColor[2]; 463 | finalR *= lightIntensity * brightness * colorR; 464 | finalG *= lightIntensity * brightness * colorG; 465 | finalB *= lightIntensity * brightness * colorB; 466 | } else { 467 | finalR *= lightIntensity * brightness; 468 | finalG *= lightIntensity * brightness; 469 | finalB *= lightIntensity * brightness; 470 | } 471 | imgData[i] = Math.min(Math.max(finalR, 0), 255); 472 | imgData[i + 1] = Math.min(Math.max(finalG, 0), 255); 473 | imgData[i + 2] = Math.min(Math.max(finalB, 0), 255); 474 | } 475 | } 476 | } 477 | async applyRelight() { 478 | if (!this.processedImage) return; 479 | try { 480 | const dataURL = this.processedImage.toDataURL('image/png'); 481 | await api.fetchApi("/lg_relight/update_image", { 482 | method: "POST", 483 | headers: { 484 | "Content-Type": "application/json", 485 | }, 486 | body: JSON.stringify({ 487 | node_id: this.currentNodeId, 488 | image: dataURL 489 | }) 490 | }); 491 | this.cleanupAndClose(); 492 | } catch (error) { 493 | console.error("应用重光照失败:", error); 494 | this.cleanupAndClose(); 495 | } 496 | } 497 | async show(nodeId, imageData, normalsData) { 498 | this.currentNodeId = nodeId; 499 | try { 500 | const images = await this.loadImages(imageData, normalsData); 501 | this.originalImage = images.original; 502 | this.normalsImage = images.normals; 503 | this.canvas.width = this.originalImage.width; 504 | this.canvas.height = this.originalImage.height; 505 | this.updateValues(); 506 | this.updateUI(); 507 | this.processAndUpdatePreview(); 508 | this.modal.showModal(); 509 | } catch (error) { 510 | console.error("显示重光照窗口失败:", error); 511 | this.cleanupAndClose(true); 512 | } 513 | } 514 | } 515 | app.registerExtension({ 516 | name: "Comfy.LGRelightV3", 517 | async setup() { 518 | const relightProcessor = new RelightProcessor(); 519 | api.addEventListener("lg_relight_init", ({ detail }) => { 520 | const { node_id, image, normals } = detail; 521 | relightProcessor.show(node_id, image, normals); 522 | }); 523 | }, 524 | async beforeRegisterNodeDef(nodeType, nodeData) { 525 | if (nodeData.name === "LG_Relight") { 526 | const onNodeCreated = nodeType.prototype.onNodeCreated; 527 | nodeType.prototype.onNodeCreated = function() { 528 | if (onNodeCreated) { 529 | onNodeCreated.apply(this, arguments); 530 | } 531 | const seedWidget = this.addWidget( 532 | "number", 533 | "seed", 534 | 0, 535 | (value) => { 536 | this.seed = value; 537 | }, 538 | { 539 | min: 0, 540 | max: Number.MAX_SAFE_INTEGER, 541 | step: 1, 542 | precision: 0 543 | } 544 | ); 545 | const seed_modeWidget = this.addWidget( 546 | "combo", 547 | "seed_mode", 548 | "randomize", 549 | () => {}, 550 | { 551 | values: ["fixed", "increment", "decrement", "randomize"], 552 | serialize: false 553 | } 554 | ); 555 | seed_modeWidget.beforeQueued = () => { 556 | const mode = seed_modeWidget.value; 557 | let newValue = seedWidget.value; 558 | if (mode === "randomize") { 559 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 560 | } else if (mode === "increment") { 561 | newValue += 1; 562 | } else if (mode === "decrement") { 563 | newValue -= 1; 564 | } else if (mode === "fixed") { 565 | if (!this.hasFixedSeed) { 566 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 567 | this.hasFixedSeed = true; 568 | } 569 | } 570 | seedWidget.value = newValue; 571 | this.seed = newValue; 572 | }; 573 | seed_modeWidget.callback = (value) => { 574 | if (value !== "fixed") { 575 | this.hasFixedSeed = false; 576 | } 577 | }; 578 | const updateButton = this.addWidget("button", "Update torrent", null, () => { 579 | const mode = seed_modeWidget.value; 580 | let newValue = seedWidget.value; 581 | if (mode === "randomize") { 582 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 583 | } else if (mode === "increment") { 584 | newValue += 1; 585 | } else if (mode === "decrement") { 586 | newValue -= 1; 587 | } else if (mode === "fixed") { 588 | newValue = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 589 | this.hasFixedSeed = true; 590 | } 591 | seedWidget.value = newValue; 592 | seedWidget.callback(newValue); 593 | if (window.rgthree && window.rgthree.queueOutputNodes) { 594 | window.rgthree.queueOutputNodes([this.id]); 595 | } 596 | }); 597 | }; 598 | } 599 | } 600 | }); 601 | -------------------------------------------------------------------------------- /example_workflow/relight_ultra+LBM.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 168, 3 | "last_link_id": 438, 4 | "nodes": [ 5 | { 6 | "id": 145, 7 | "type": "DepthAnything_V2", 8 | "pos": [ 9 | 1860.0753173828125, 10 | -86.67231750488281 11 | ], 12 | "size": [ 13 | 214.20001220703125, 14 | 46 15 | ], 16 | "flags": {}, 17 | "order": 9, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "da_model", 22 | "type": "DAMODEL", 23 | "link": 395 24 | }, 25 | { 26 | "name": "images", 27 | "type": "IMAGE", 28 | "link": 396 29 | } 30 | ], 31 | "outputs": [ 32 | { 33 | "name": "image", 34 | "type": "IMAGE", 35 | "links": [ 36 | 397 37 | ], 38 | "slot_index": 0 39 | } 40 | ], 41 | "properties": { 42 | "cnr_id": "comfyui-depthanythingv2", 43 | "ver": "9d7cb8c1e53b01744a75b599d3e91c93464a2d33", 44 | "Node name for S&R": "DepthAnything_V2" 45 | }, 46 | "widgets_values": [] 47 | }, 48 | { 49 | "id": 151, 50 | "type": "Metric3D-NormalMapPreprocessor", 51 | "pos": [ 52 | 1862.0408935546875, 53 | 47.004119873046875 54 | ], 55 | "size": [ 56 | 210, 57 | 130 58 | ], 59 | "flags": {}, 60 | "order": 13, 61 | "mode": 0, 62 | "inputs": [ 63 | { 64 | "name": "image", 65 | "type": "IMAGE", 66 | "link": 406 67 | }, 68 | { 69 | "name": "resolution", 70 | "type": "INT", 71 | "shape": 7, 72 | "widget": { 73 | "name": "resolution" 74 | }, 75 | "link": 407 76 | } 77 | ], 78 | "outputs": [ 79 | { 80 | "name": "IMAGE", 81 | "type": "IMAGE", 82 | "links": [ 83 | 408 84 | ], 85 | "slot_index": 0 86 | } 87 | ], 88 | "properties": { 89 | "cnr_id": "comfyui_controlnet_aux", 90 | "ver": "5a049bde9cc117dafc327cded156459289097ea1", 91 | "Node name for S&R": "Metric3D-NormalMapPreprocessor" 92 | }, 93 | "widgets_values": [ 94 | "vit-small", 95 | 1000, 96 | 1000, 97 | 512 98 | ] 99 | }, 100 | { 101 | "id": 140, 102 | "type": "LoadImage", 103 | "pos": [ 104 | 1009.3165283203125, 105 | -111.35148620605469 106 | ], 107 | "size": [ 108 | 315, 109 | 314 110 | ], 111 | "flags": {}, 112 | "order": 0, 113 | "mode": 0, 114 | "inputs": [], 115 | "outputs": [ 116 | { 117 | "name": "IMAGE", 118 | "type": "IMAGE", 119 | "links": [ 120 | 380 121 | ], 122 | "slot_index": 0 123 | }, 124 | { 125 | "name": "MASK", 126 | "type": "MASK", 127 | "links": [], 128 | "slot_index": 1 129 | } 130 | ], 131 | "properties": { 132 | "cnr_id": "comfy-core", 133 | "ver": "0.3.18", 134 | "Node name for S&R": "LoadImage" 135 | }, 136 | "widgets_values": [ 137 | "1.png", 138 | "image" 139 | ] 140 | }, 141 | { 142 | "id": 108, 143 | "type": "LG_Relight_Ultra", 144 | "pos": [ 145 | 2165.669189453125, 146 | -110.64283752441406 147 | ], 148 | "size": [ 149 | 210, 150 | 214 151 | ], 152 | "flags": {}, 153 | "order": 15, 154 | "mode": 0, 155 | "inputs": [ 156 | { 157 | "name": "bg_img", 158 | "type": "IMAGE", 159 | "link": 296 160 | }, 161 | { 162 | "name": "bg_depth_map", 163 | "type": "IMAGE", 164 | "link": 397 165 | }, 166 | { 167 | "name": "bg_normal_map", 168 | "type": "IMAGE", 169 | "link": 408 170 | }, 171 | { 172 | "name": "mask", 173 | "type": "MASK", 174 | "shape": 7, 175 | "link": null 176 | } 177 | ], 178 | "outputs": [ 179 | { 180 | "name": "IMAGE", 181 | "type": "IMAGE", 182 | "links": [ 183 | 370 184 | ], 185 | "slot_index": 0 186 | } 187 | ], 188 | "properties": { 189 | "cnr_id": "Comfyui-LG_Relight", 190 | "ver": "e85bde22382125589feafae0f7ec7e07c3aef4c4", 191 | "Node name for S&R": "LG_Relight_Ultra" 192 | }, 193 | "widgets_values": [ 194 | 60, 195 | false, 196 | 5036762820890403, 197 | "fixed", 198 | "" 199 | ] 200 | }, 201 | { 202 | "id": 159, 203 | "type": "FastCanvasTool", 204 | "pos": [ 205 | 1534.7457275390625, 206 | 1548.7452392578125 207 | ], 208 | "size": [ 209 | 140, 210 | 66 211 | ], 212 | "flags": {}, 213 | "order": 11, 214 | "mode": 0, 215 | "inputs": [ 216 | { 217 | "name": "bg_img", 218 | "type": "IMAGE", 219 | "link": 426 220 | }, 221 | { 222 | "name": "img_1", 223 | "type": "IMAGE", 224 | "link": 423 225 | }, 226 | { 227 | "name": "img_2", 228 | "type": "IMAGE", 229 | "link": null 230 | } 231 | ], 232 | "outputs": [ 233 | { 234 | "name": "fc_data", 235 | "type": "FC_DATA", 236 | "links": [ 237 | 415 238 | ] 239 | } 240 | ], 241 | "properties": { 242 | "cnr_id": "comfyui_lg_tools", 243 | "ver": "c35534a8e9f36be37c71c9301158cf2d1cf1faa3", 244 | "Node name for S&R": "FastCanvasTool" 245 | }, 246 | "widgets_values": [] 247 | }, 248 | { 249 | "id": 156, 250 | "type": "LoadImage", 251 | "pos": [ 252 | 1006.932861328125, 253 | 1616.0467529296875 254 | ], 255 | "size": [ 256 | 315, 257 | 314 258 | ], 259 | "flags": {}, 260 | "order": 1, 261 | "mode": 0, 262 | "inputs": [], 263 | "outputs": [ 264 | { 265 | "name": "IMAGE", 266 | "type": "IMAGE", 267 | "links": [ 268 | 421 269 | ], 270 | "slot_index": 0 271 | }, 272 | { 273 | "name": "MASK", 274 | "type": "MASK", 275 | "links": [], 276 | "slot_index": 1 277 | } 278 | ], 279 | "properties": { 280 | "cnr_id": "comfy-core", 281 | "ver": "0.3.18", 282 | "Node name for S&R": "LoadImage" 283 | }, 284 | "widgets_values": [ 285 | "1344x768_hair.png", 286 | "image" 287 | ] 288 | }, 289 | { 290 | "id": 162, 291 | "type": "InspyrenetRembgLoader", 292 | "pos": [ 293 | 1451.8292236328125, 294 | 1846.8974609375 295 | ], 296 | "size": [ 297 | 210, 298 | 82 299 | ], 300 | "flags": {}, 301 | "order": 2, 302 | "mode": 0, 303 | "inputs": [], 304 | "outputs": [ 305 | { 306 | "name": "INSPYRENET_MODEL", 307 | "type": "INSPYRENET_MODEL", 308 | "links": [ 309 | 420 310 | ] 311 | } 312 | ], 313 | "properties": { 314 | "cnr_id": "comfyui_lg_tools", 315 | "ver": "c35534a8e9f36be37c71c9301158cf2d1cf1faa3", 316 | "Node name for S&R": "InspyrenetRembgLoader" 317 | }, 318 | "widgets_values": [ 319 | "base", 320 | "default" 321 | ] 322 | }, 323 | { 324 | "id": 161, 325 | "type": "InspyrenetRembgProcess", 326 | "pos": [ 327 | 1453.150146484375, 328 | 1685.0533447265625 329 | ], 330 | "size": [ 331 | 210, 332 | 102 333 | ], 334 | "flags": {}, 335 | "order": 7, 336 | "mode": 0, 337 | "inputs": [ 338 | { 339 | "name": "model", 340 | "type": "INSPYRENET_MODEL", 341 | "link": 420 342 | }, 343 | { 344 | "name": "image", 345 | "type": "IMAGE", 346 | "link": 421 347 | } 348 | ], 349 | "outputs": [ 350 | { 351 | "name": "IMAGE", 352 | "type": "IMAGE", 353 | "links": [ 354 | 423 355 | ], 356 | "slot_index": 0 357 | }, 358 | { 359 | "name": "MASK", 360 | "type": "MASK", 361 | "links": null 362 | } 363 | ], 364 | "properties": { 365 | "cnr_id": "comfyui_lg_tools", 366 | "ver": "c35534a8e9f36be37c71c9301158cf2d1cf1faa3", 367 | "Node name for S&R": "InspyrenetRembgProcess" 368 | }, 369 | "widgets_values": [ 370 | 0.5, 371 | "" 372 | ] 373 | }, 374 | { 375 | "id": 12, 376 | "type": "LayerUtility: ImageScaleByAspectRatio V2", 377 | "pos": [ 378 | 1390.3602294921875, 379 | -112.21415710449219 380 | ], 381 | "size": [ 382 | 336, 383 | 330 384 | ], 385 | "flags": {}, 386 | "order": 6, 387 | "mode": 0, 388 | "inputs": [ 389 | { 390 | "name": "image", 391 | "type": "IMAGE", 392 | "shape": 7, 393 | "link": 380 394 | }, 395 | { 396 | "name": "mask", 397 | "type": "MASK", 398 | "shape": 7, 399 | "link": null 400 | } 401 | ], 402 | "outputs": [ 403 | { 404 | "name": "image", 405 | "type": "IMAGE", 406 | "links": [ 407 | 296, 408 | 396, 409 | 406 410 | ], 411 | "slot_index": 0 412 | }, 413 | { 414 | "name": "mask", 415 | "type": "MASK", 416 | "links": [], 417 | "slot_index": 1 418 | }, 419 | { 420 | "name": "original_size", 421 | "type": "BOX", 422 | "links": null 423 | }, 424 | { 425 | "name": "width", 426 | "type": "INT", 427 | "links": [ 428 | 399 429 | ], 430 | "slot_index": 3 431 | }, 432 | { 433 | "name": "height", 434 | "type": "INT", 435 | "links": [ 436 | 400 437 | ], 438 | "slot_index": 4 439 | } 440 | ], 441 | "properties": { 442 | "cnr_id": "comfyui_layerstyle", 443 | "ver": "458456e464ffa53baea5ad5efe84f8345305f135", 444 | "Node name for S&R": "LayerUtility: ImageScaleByAspectRatio V2" 445 | }, 446 | "widgets_values": [ 447 | "original", 448 | 1, 449 | 1, 450 | "crop", 451 | "lanczos", 452 | "64", 453 | "longest", 454 | 1024, 455 | "#000000" 456 | ], 457 | "color": "rgba(38, 73, 116, 0.7)" 458 | }, 459 | { 460 | "id": 157, 461 | "type": "LoadImage", 462 | "pos": [ 463 | 1008.0328369140625, 464 | 1144.90673828125 465 | ], 466 | "size": [ 467 | 315, 468 | 314 469 | ], 470 | "flags": {}, 471 | "order": 3, 472 | "mode": 0, 473 | "inputs": [], 474 | "outputs": [ 475 | { 476 | "name": "IMAGE", 477 | "type": "IMAGE", 478 | "links": [ 479 | 425 480 | ], 481 | "slot_index": 0 482 | }, 483 | { 484 | "name": "MASK", 485 | "type": "MASK", 486 | "links": null 487 | } 488 | ], 489 | "properties": { 490 | "cnr_id": "comfy-core", 491 | "ver": "0.3.18", 492 | "Node name for S&R": "LoadImage" 493 | }, 494 | "widgets_values": [ 495 | "2d587568389fb7c41ef219ac5c8e548f.jpg", 496 | "image" 497 | ] 498 | }, 499 | { 500 | "id": 158, 501 | "type": "FastCanvas", 502 | "pos": [ 503 | 1969.6422119140625, 504 | 1181.0726318359375 505 | ], 506 | "size": [ 507 | 440, 508 | 340 509 | ], 510 | "flags": {}, 511 | "order": 14, 512 | "mode": 0, 513 | "inputs": [ 514 | { 515 | "name": "fc_data", 516 | "type": "FC_DATA", 517 | "shape": 7, 518 | "link": 415 519 | } 520 | ], 521 | "outputs": [ 522 | { 523 | "name": "image", 524 | "type": "IMAGE", 525 | "links": [ 526 | 424 527 | ], 528 | "slot_index": 0 529 | }, 530 | { 531 | "name": "mask", 532 | "type": "MASK", 533 | "links": null 534 | }, 535 | { 536 | "name": "transform_data", 537 | "type": "TRANSFORM_DATA", 538 | "links": null 539 | } 540 | ], 541 | "properties": { 542 | "cnr_id": "comfyui_lg_tools", 543 | "ver": "c35534a8e9f36be37c71c9301158cf2d1cf1faa3", 544 | "Node name for S&R": "FastCanvas" 545 | }, 546 | "widgets_values": [ 547 | "initial_seed", 548 | "" 549 | ], 550 | "canvasData": { 551 | "originalSize": { 552 | "width": 420, 553 | "height": 200 554 | }, 555 | "maxDisplaySize": 768, 556 | "objects": [], 557 | "background": { 558 | "type": "rect", 559 | "fill": "#000000", 560 | "width": 420, 561 | "height": 200, 562 | "image": null 563 | } 564 | } 565 | }, 566 | { 567 | "id": 149, 568 | "type": "ImpactMinMax", 569 | "pos": [ 570 | 1856.8765869140625, 571 | 232.8126220703125 572 | ], 573 | "size": [ 574 | 210, 575 | 78 576 | ], 577 | "flags": {}, 578 | "order": 10, 579 | "mode": 0, 580 | "inputs": [ 581 | { 582 | "name": "a", 583 | "type": "*", 584 | "link": 399 585 | }, 586 | { 587 | "name": "b", 588 | "type": "*", 589 | "link": 400 590 | } 591 | ], 592 | "outputs": [ 593 | { 594 | "name": "INT", 595 | "type": "INT", 596 | "links": [ 597 | 407 598 | ], 599 | "slot_index": 0 600 | } 601 | ], 602 | "properties": { 603 | "cnr_id": "comfyui-impact-pack", 604 | "ver": "dc70f40effeb21681f30af65062bc1b2a40fdd82", 605 | "Node name for S&R": "ImpactMinMax" 606 | }, 607 | "widgets_values": [ 608 | false 609 | ] 610 | }, 611 | { 612 | "id": 165, 613 | "type": "ImpactMinMax", 614 | "pos": [ 615 | 2949.779052734375, 616 | 1500.405517578125 617 | ], 618 | "size": [ 619 | 210, 620 | 78 621 | ], 622 | "flags": {}, 623 | "order": 12, 624 | "mode": 0, 625 | "inputs": [ 626 | { 627 | "name": "a", 628 | "type": "*", 629 | "link": 430 630 | }, 631 | { 632 | "name": "b", 633 | "type": "*", 634 | "link": 431 635 | } 636 | ], 637 | "outputs": [ 638 | { 639 | "name": "INT", 640 | "type": "INT", 641 | "links": [ 642 | 432 643 | ], 644 | "slot_index": 0 645 | } 646 | ], 647 | "properties": { 648 | "cnr_id": "comfyui-impact-pack", 649 | "ver": "dc70f40effeb21681f30af65062bc1b2a40fdd82", 650 | "Node name for S&R": "ImpactMinMax" 651 | }, 652 | "widgets_values": [ 653 | false 654 | ] 655 | }, 656 | { 657 | "id": 166, 658 | "type": "Metric3D-NormalMapPreprocessor", 659 | "pos": [ 660 | 2955.91064453125, 661 | 1313.6285400390625 662 | ], 663 | "size": [ 664 | 210, 665 | 130 666 | ], 667 | "flags": {}, 668 | "order": 19, 669 | "mode": 0, 670 | "inputs": [ 671 | { 672 | "name": "image", 673 | "type": "IMAGE", 674 | "link": 433 675 | }, 676 | { 677 | "name": "resolution", 678 | "type": "INT", 679 | "shape": 7, 680 | "widget": { 681 | "name": "resolution" 682 | }, 683 | "link": 432 684 | } 685 | ], 686 | "outputs": [ 687 | { 688 | "name": "IMAGE", 689 | "type": "IMAGE", 690 | "links": [ 691 | 436 692 | ], 693 | "slot_index": 0 694 | } 695 | ], 696 | "properties": { 697 | "cnr_id": "comfyui_controlnet_aux", 698 | "ver": "5a049bde9cc117dafc327cded156459289097ea1", 699 | "Node name for S&R": "Metric3D-NormalMapPreprocessor" 700 | }, 701 | "widgets_values": [ 702 | "vit-small", 703 | 1000, 704 | 1000, 705 | 512 706 | ] 707 | }, 708 | { 709 | "id": 155, 710 | "type": "DepthAnything_V2", 711 | "pos": [ 712 | 2952.10595703125, 713 | 1171.686767578125 714 | ], 715 | "size": [ 716 | 214.20001220703125, 717 | 46 718 | ], 719 | "flags": {}, 720 | "order": 18, 721 | "mode": 0, 722 | "inputs": [ 723 | { 724 | "name": "da_model", 725 | "type": "DAMODEL", 726 | "link": 413 727 | }, 728 | { 729 | "name": "images", 730 | "type": "IMAGE", 731 | "link": 414 732 | } 733 | ], 734 | "outputs": [ 735 | { 736 | "name": "image", 737 | "type": "IMAGE", 738 | "links": [ 739 | 434 740 | ], 741 | "slot_index": 0 742 | } 743 | ], 744 | "properties": { 745 | "cnr_id": "comfyui-depthanythingv2", 746 | "ver": "9d7cb8c1e53b01744a75b599d3e91c93464a2d33", 747 | "Node name for S&R": "DepthAnything_V2" 748 | }, 749 | "widgets_values": [] 750 | }, 751 | { 752 | "id": 153, 753 | "type": "LBMSampler", 754 | "pos": [ 755 | 2556.023193359375, 756 | 1323.0052490234375 757 | ], 758 | "size": [ 759 | 315, 760 | 78 761 | ], 762 | "flags": {}, 763 | "order": 16, 764 | "mode": 0, 765 | "inputs": [ 766 | { 767 | "name": "model", 768 | "type": "LBM_MODEL", 769 | "link": 410 770 | }, 771 | { 772 | "name": "image", 773 | "type": "IMAGE", 774 | "link": 424 775 | } 776 | ], 777 | "outputs": [ 778 | { 779 | "name": "image", 780 | "type": "IMAGE", 781 | "links": [ 782 | 414, 783 | 433, 784 | 437 785 | ], 786 | "slot_index": 0 787 | } 788 | ], 789 | "properties": { 790 | "aux_id": "kijai/ComfyUI-LBMWrapper", 791 | "ver": "6bda0e7c6910033f2efc124131422fae568965db", 792 | "Node name for S&R": "LBMSampler" 793 | }, 794 | "widgets_values": [ 795 | 20 796 | ] 797 | }, 798 | { 799 | "id": 152, 800 | "type": "LoadLBMModel", 801 | "pos": [ 802 | 2562.306884765625, 803 | 1132.63037109375 804 | ], 805 | "size": [ 806 | 315, 807 | 106 808 | ], 809 | "flags": {}, 810 | "order": 4, 811 | "mode": 0, 812 | "inputs": [], 813 | "outputs": [ 814 | { 815 | "name": "model", 816 | "type": "LBM_MODEL", 817 | "links": [ 818 | 410 819 | ], 820 | "slot_index": 0 821 | } 822 | ], 823 | "properties": { 824 | "aux_id": "kijai/ComfyUI-LBMWrapper", 825 | "ver": "6bda0e7c6910033f2efc124131422fae568965db", 826 | "Node name for S&R": "LoadLBMModel" 827 | }, 828 | "widgets_values": [ 829 | "model.safetensors", 830 | "fp8_e4m3fn_fast", 831 | "main_device" 832 | ] 833 | }, 834 | { 835 | "id": 168, 836 | "type": "LG_Relight_Ultra", 837 | "pos": [ 838 | 3298.69189453125, 839 | 1160.7210693359375 840 | ], 841 | "size": [ 842 | 210, 843 | 214 844 | ], 845 | "flags": {}, 846 | "order": 20, 847 | "mode": 0, 848 | "inputs": [ 849 | { 850 | "name": "bg_img", 851 | "type": "IMAGE", 852 | "link": 437 853 | }, 854 | { 855 | "name": "bg_depth_map", 856 | "type": "IMAGE", 857 | "link": 434 858 | }, 859 | { 860 | "name": "bg_normal_map", 861 | "type": "IMAGE", 862 | "link": 436 863 | }, 864 | { 865 | "name": "mask", 866 | "type": "MASK", 867 | "shape": 7, 868 | "link": null 869 | } 870 | ], 871 | "outputs": [ 872 | { 873 | "name": "IMAGE", 874 | "type": "IMAGE", 875 | "links": [ 876 | 438 877 | ], 878 | "slot_index": 0 879 | } 880 | ], 881 | "properties": { 882 | "cnr_id": "Comfyui-LG_Relight", 883 | "ver": "e85bde22382125589feafae0f7ec7e07c3aef4c4", 884 | "Node name for S&R": "LG_Relight_Ultra" 885 | }, 886 | "widgets_values": [ 887 | 60, 888 | false, 889 | 6476798499977981, 890 | "fixed", 891 | "" 892 | ] 893 | }, 894 | { 895 | "id": 154, 896 | "type": "PreviewImage", 897 | "pos": [ 898 | 3721.659912109375, 899 | 1160.1920166015625 900 | ], 901 | "size": [ 902 | 706.6226196289062, 903 | 634.8649291992188 904 | ], 905 | "flags": {}, 906 | "order": 21, 907 | "mode": 0, 908 | "inputs": [ 909 | { 910 | "name": "images", 911 | "type": "IMAGE", 912 | "link": 438 913 | } 914 | ], 915 | "outputs": [], 916 | "properties": { 917 | "cnr_id": "comfy-core", 918 | "ver": "0.3.18", 919 | "Node name for S&R": "PreviewImage" 920 | }, 921 | "widgets_values": [] 922 | }, 923 | { 924 | "id": 164, 925 | "type": "LayerUtility: ImageScaleByAspectRatio V2", 926 | "pos": [ 927 | 1369.355224609375, 928 | 1127.646728515625 929 | ], 930 | "size": [ 931 | 336, 932 | 330 933 | ], 934 | "flags": {}, 935 | "order": 8, 936 | "mode": 0, 937 | "inputs": [ 938 | { 939 | "name": "image", 940 | "type": "IMAGE", 941 | "shape": 7, 942 | "link": 425 943 | }, 944 | { 945 | "name": "mask", 946 | "type": "MASK", 947 | "shape": 7, 948 | "link": null 949 | } 950 | ], 951 | "outputs": [ 952 | { 953 | "name": "image", 954 | "type": "IMAGE", 955 | "links": [ 956 | 426 957 | ], 958 | "slot_index": 0 959 | }, 960 | { 961 | "name": "mask", 962 | "type": "MASK", 963 | "links": [], 964 | "slot_index": 1 965 | }, 966 | { 967 | "name": "original_size", 968 | "type": "BOX", 969 | "links": null 970 | }, 971 | { 972 | "name": "width", 973 | "type": "INT", 974 | "links": [ 975 | 430 976 | ], 977 | "slot_index": 3 978 | }, 979 | { 980 | "name": "height", 981 | "type": "INT", 982 | "links": [ 983 | 431 984 | ], 985 | "slot_index": 4 986 | } 987 | ], 988 | "properties": { 989 | "cnr_id": "comfyui_layerstyle", 990 | "ver": "458456e464ffa53baea5ad5efe84f8345305f135", 991 | "Node name for S&R": "LayerUtility: ImageScaleByAspectRatio V2" 992 | }, 993 | "widgets_values": [ 994 | "original", 995 | 1, 996 | 1, 997 | "crop", 998 | "lanczos", 999 | "64", 1000 | "longest", 1001 | 1024, 1002 | "#000000" 1003 | ], 1004 | "color": "rgba(38, 73, 116, 0.7)" 1005 | }, 1006 | { 1007 | "id": 146, 1008 | "type": "DownloadAndLoadDepthAnythingV2Model", 1009 | "pos": [ 1010 | 638.7694091796875, 1011 | 1250.1092529296875 1012 | ], 1013 | "size": [ 1014 | 294, 1015 | 58 1016 | ], 1017 | "flags": {}, 1018 | "order": 5, 1019 | "mode": 0, 1020 | "inputs": [], 1021 | "outputs": [ 1022 | { 1023 | "name": "da_v2_model", 1024 | "type": "DAMODEL", 1025 | "links": [ 1026 | 395, 1027 | 413 1028 | ], 1029 | "slot_index": 0 1030 | } 1031 | ], 1032 | "properties": { 1033 | "cnr_id": "comfyui-depthanythingv2", 1034 | "ver": "9d7cb8c1e53b01744a75b599d3e91c93464a2d33", 1035 | "Node name for S&R": "DownloadAndLoadDepthAnythingV2Model" 1036 | }, 1037 | "widgets_values": [ 1038 | "depth_anything_v2_vitl_fp16.safetensors" 1039 | ] 1040 | }, 1041 | { 1042 | "id": 134, 1043 | "type": "PreviewImage", 1044 | "pos": [ 1045 | 2426.786376953125, 1046 | -111.42247009277344 1047 | ], 1048 | "size": [ 1049 | 1060.74755859375, 1050 | 1125.7974853515625 1051 | ], 1052 | "flags": {}, 1053 | "order": 17, 1054 | "mode": 0, 1055 | "inputs": [ 1056 | { 1057 | "name": "images", 1058 | "type": "IMAGE", 1059 | "link": 370 1060 | } 1061 | ], 1062 | "outputs": [], 1063 | "properties": { 1064 | "cnr_id": "comfy-core", 1065 | "ver": "0.3.18", 1066 | "Node name for S&R": "PreviewImage" 1067 | }, 1068 | "widgets_values": [] 1069 | } 1070 | ], 1071 | "links": [ 1072 | [ 1073 | 296, 1074 | 12, 1075 | 0, 1076 | 108, 1077 | 0, 1078 | "IMAGE" 1079 | ], 1080 | [ 1081 | 370, 1082 | 108, 1083 | 0, 1084 | 134, 1085 | 0, 1086 | "IMAGE" 1087 | ], 1088 | [ 1089 | 380, 1090 | 140, 1091 | 0, 1092 | 12, 1093 | 0, 1094 | "IMAGE" 1095 | ], 1096 | [ 1097 | 395, 1098 | 146, 1099 | 0, 1100 | 145, 1101 | 0, 1102 | "DAMODEL" 1103 | ], 1104 | [ 1105 | 396, 1106 | 12, 1107 | 0, 1108 | 145, 1109 | 1, 1110 | "IMAGE" 1111 | ], 1112 | [ 1113 | 397, 1114 | 145, 1115 | 0, 1116 | 108, 1117 | 1, 1118 | "IMAGE" 1119 | ], 1120 | [ 1121 | 399, 1122 | 12, 1123 | 3, 1124 | 149, 1125 | 0, 1126 | "*" 1127 | ], 1128 | [ 1129 | 400, 1130 | 12, 1131 | 4, 1132 | 149, 1133 | 1, 1134 | "*" 1135 | ], 1136 | [ 1137 | 406, 1138 | 12, 1139 | 0, 1140 | 151, 1141 | 0, 1142 | "IMAGE" 1143 | ], 1144 | [ 1145 | 407, 1146 | 149, 1147 | 0, 1148 | 151, 1149 | 1, 1150 | "INT" 1151 | ], 1152 | [ 1153 | 408, 1154 | 151, 1155 | 0, 1156 | 108, 1157 | 2, 1158 | "IMAGE" 1159 | ], 1160 | [ 1161 | 410, 1162 | 152, 1163 | 0, 1164 | 153, 1165 | 0, 1166 | "LBM_MODEL" 1167 | ], 1168 | [ 1169 | 413, 1170 | 146, 1171 | 0, 1172 | 155, 1173 | 0, 1174 | "DAMODEL" 1175 | ], 1176 | [ 1177 | 414, 1178 | 153, 1179 | 0, 1180 | 155, 1181 | 1, 1182 | "IMAGE" 1183 | ], 1184 | [ 1185 | 415, 1186 | 159, 1187 | 0, 1188 | 158, 1189 | 0, 1190 | "FC_DATA" 1191 | ], 1192 | [ 1193 | 420, 1194 | 162, 1195 | 0, 1196 | 161, 1197 | 0, 1198 | "INSPYRENET_MODEL" 1199 | ], 1200 | [ 1201 | 421, 1202 | 156, 1203 | 0, 1204 | 161, 1205 | 1, 1206 | "IMAGE" 1207 | ], 1208 | [ 1209 | 423, 1210 | 161, 1211 | 0, 1212 | 159, 1213 | 1, 1214 | "IMAGE" 1215 | ], 1216 | [ 1217 | 424, 1218 | 158, 1219 | 0, 1220 | 153, 1221 | 1, 1222 | "IMAGE" 1223 | ], 1224 | [ 1225 | 425, 1226 | 157, 1227 | 0, 1228 | 164, 1229 | 0, 1230 | "IMAGE" 1231 | ], 1232 | [ 1233 | 426, 1234 | 164, 1235 | 0, 1236 | 159, 1237 | 0, 1238 | "IMAGE" 1239 | ], 1240 | [ 1241 | 430, 1242 | 164, 1243 | 3, 1244 | 165, 1245 | 0, 1246 | "*" 1247 | ], 1248 | [ 1249 | 431, 1250 | 164, 1251 | 4, 1252 | 165, 1253 | 1, 1254 | "*" 1255 | ], 1256 | [ 1257 | 432, 1258 | 165, 1259 | 0, 1260 | 166, 1261 | 1, 1262 | "INT" 1263 | ], 1264 | [ 1265 | 433, 1266 | 153, 1267 | 0, 1268 | 166, 1269 | 0, 1270 | "IMAGE" 1271 | ], 1272 | [ 1273 | 434, 1274 | 155, 1275 | 0, 1276 | 168, 1277 | 1, 1278 | "IMAGE" 1279 | ], 1280 | [ 1281 | 436, 1282 | 166, 1283 | 0, 1284 | 168, 1285 | 2, 1286 | "IMAGE" 1287 | ], 1288 | [ 1289 | 437, 1290 | 153, 1291 | 0, 1292 | 168, 1293 | 0, 1294 | "IMAGE" 1295 | ], 1296 | [ 1297 | 438, 1298 | 168, 1299 | 0, 1300 | 154, 1301 | 0, 1302 | "IMAGE" 1303 | ] 1304 | ], 1305 | "groups": [ 1306 | { 1307 | "id": 1, 1308 | "title": "LBM+RELGIHT_ULTRA", 1309 | "bounding": [ 1310 | 996.932861328125, 1311 | 1054.0462646484375, 1312 | 3441.34130859375, 1313 | 886 1314 | ], 1315 | "color": "#3f789e", 1316 | "font_size": 24, 1317 | "flags": {} 1318 | }, 1319 | { 1320 | "id": 2, 1321 | "title": "RELIGHT_ULTRA", 1322 | "bounding": [ 1323 | 999.3165893554688, 1324 | -185.814208984375, 1325 | 2498.217529296875, 1326 | 1210.1890869140625 1327 | ], 1328 | "color": "#3f789e", 1329 | "font_size": 24, 1330 | "flags": {} 1331 | } 1332 | ], 1333 | "config": {}, 1334 | "extra": { 1335 | "ds": { 1336 | "scale": 0.42409761837248555, 1337 | "offset": { 1338 | "0": 54.231536865234375, 1339 | "1": 307.7550964355469 1340 | } 1341 | } 1342 | }, 1343 | "version": 0.4 1344 | } -------------------------------------------------------------------------------- /web/light_editor.js: -------------------------------------------------------------------------------- 1 | import { api } from '../../../scripts/api.js' 2 | import { app } from '../../../scripts/app.js' 3 | import { createRelightModal, modalStyles } from './config.js' 4 | import { SceneUtils } from './scene_utils.js' 5 | 6 | export class LightEditor { 7 | constructor() { 8 | const styleElement = document.createElement('style'); 9 | styleElement.textContent = modalStyles; 10 | document.head.appendChild(styleElement); 11 | this.modal = createRelightModal(); 12 | this.canvasContainer = this.modal.querySelector('.relight-canvas-container'); 13 | this.lightIndicator = this.modal.querySelector('.light-source-indicator'); 14 | this.isMovingLight = false; 15 | this.currentNode = null; 16 | this.isSceneSetup = false; 17 | this.lightX = 0; 18 | this.lightY = 0; 19 | this.lightZ = 1; 20 | this.zOffset = 0; 21 | this.lightSources = []; 22 | this.activeSourceIndex = -1; 23 | this.bindEvents(); 24 | } 25 | 26 | bindEvents() { 27 | this.onCanvasMouseDownHandler = this.onCanvasMouseDown.bind(this); 28 | this.onCanvasMouseMoveHandler = this.onCanvasMouseMove.bind(this); 29 | this.onCanvasMouseUpHandler = this.onCanvasMouseUp.bind(this); 30 | this.onSliderChangeHandler = this.onSliderChange.bind(this); 31 | this.onLightTypeChangeHandler = this.onLightTypeChange.bind(this); 32 | const cancelBtn = this.modal.querySelector('.relight-btn.cancel'); 33 | cancelBtn.addEventListener('click', () => this.cleanupAndClose(true)); 34 | const applyBtn = this.modal.querySelector('.relight-btn.apply'); 35 | applyBtn.addEventListener('click', () => this.applyChanges()); 36 | this.canvasContainer.addEventListener('mousedown', this.onCanvasMouseDownHandler); 37 | const sliders = this.modal.querySelectorAll('.relight-slider'); 38 | sliders.forEach(slider => { 39 | slider.addEventListener('input', this.onSliderChangeHandler); 40 | }); 41 | 42 | // 添加光源类型切换事件监听 43 | const lightTypeSelect = this.modal.querySelector('#lightType'); 44 | if (lightTypeSelect) { 45 | lightTypeSelect.addEventListener('change', this.onLightTypeChangeHandler); 46 | } 47 | } 48 | 49 | onLightTypeChange(event) { 50 | const lightType = event.target.value; 51 | const spotlightControls = this.modal.querySelectorAll('.spotlight-controls'); 52 | const pointlightControls = this.modal.querySelectorAll('.pointlight-controls'); 53 | 54 | // 显示或隐藏聚光灯控制项 55 | spotlightControls.forEach(control => { 56 | control.style.display = lightType === 'spot' ? 'block' : 'none'; 57 | }); 58 | 59 | // 显示或隐藏点光源控制项 60 | pointlightControls.forEach(control => { 61 | control.style.display = lightType === 'point' ? 'block' : 'none'; 62 | }); 63 | 64 | // 如果有活动光源,转换其类型 65 | if (this.activeSourceIndex !== -1) { 66 | const activeSource = this.lightSources[this.activeSourceIndex]; 67 | if (activeSource) { 68 | this.convertLightType(activeSource, lightType); 69 | } 70 | } 71 | } 72 | 73 | convertLightType(source, newType) { 74 | // 保存原始光源的属性 75 | const position = source.position; 76 | const intensity = source.intensity; 77 | const color = source.light.color.getHex(); 78 | const visible = source.light.visible; 79 | 80 | // 从场景中移除原始光源 81 | this.scene.remove(source.light); 82 | if (source.lightType === 'spot' && source.light.target) { 83 | this.scene.remove(source.light.target); 84 | } 85 | 86 | // 创建新的光源 87 | let newLight; 88 | if (newType === 'spot') { 89 | const spotlightAngleSlider = this.modal.querySelector('#spotlightAngle'); 90 | const spotlightPenumbraSlider = this.modal.querySelector('#spotlightPenumbra'); 91 | const angle = spotlightAngleSlider ? parseFloat(spotlightAngleSlider.value) * Math.PI : Math.PI / 3; 92 | const penumbra = spotlightPenumbraSlider ? parseFloat(spotlightPenumbraSlider.value) : 0.2; 93 | 94 | newLight = new THREE.SpotLight(color, intensity, 10, angle, penumbra); 95 | 96 | // 设置目标点位置 97 | let targetPosition; 98 | if (source.targetPosition) { 99 | targetPosition = source.targetPosition; 100 | } else { 101 | // 如果没有现成的目标点,默认设置在光源下方一些位置 102 | targetPosition = { 103 | x: position.x, 104 | y: position.y - 1, 105 | z: 0 106 | }; 107 | } 108 | newLight.target.position.set(targetPosition.x, targetPosition.y, targetPosition.z); 109 | source.targetPosition = targetPosition; 110 | 111 | this.scene.add(newLight.target); 112 | 113 | // 为光源添加聚光灯特有属性 114 | source.spotParams = { 115 | angle: angle, 116 | penumbra: penumbra 117 | }; 118 | 119 | // 聚光灯指示器样式修改 120 | if (source.indicator) { 121 | source.indicator.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; 122 | source.indicator.style.transform = 'translate(-50%, -20%)'; 123 | } 124 | 125 | // 创建连接线 126 | if (!source.connectionLine) { 127 | source.connectionLine = document.createElement('div'); 128 | source.connectionLine.className = 'spotlight-connection-line'; 129 | this.canvasContainer.appendChild(source.connectionLine); 130 | } 131 | 132 | // 创建目标点指示器 133 | if (!source.targetIndicator) { 134 | source.targetIndicator = this.createTargetIndicator( 135 | source.lightColor || '#ffffff' 136 | ); 137 | source.targetIndicator.style.display = 'none'; // 默认隐藏 138 | this.canvasContainer.appendChild(source.targetIndicator); 139 | } 140 | 141 | // 更新连接线位置 142 | this.updateSpotlightLine(source); 143 | } else { 144 | const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); 145 | const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; 146 | 147 | newLight = new THREE.PointLight(color, intensity, radius, 2); 148 | 149 | // 点光源指示器样式恢复 150 | if (source.indicator) { 151 | source.indicator.style.clipPath = ''; 152 | source.indicator.style.borderRadius = '50%'; 153 | source.indicator.style.transform = 'translate(-50%, -50%)'; 154 | } 155 | 156 | // 隐藏连接线 157 | if (source.connectionLine) { 158 | source.connectionLine.style.display = 'none'; 159 | } 160 | 161 | // 隐藏目标点指示器 162 | if (source.targetIndicator) { 163 | source.targetIndicator.style.display = 'none'; 164 | } 165 | 166 | // 移除聚光灯特有属性 167 | if (source.spotParams) { 168 | delete source.spotParams; 169 | } 170 | 171 | // 为点光源添加半径参数 172 | source.pointParams = { 173 | radius: radius 174 | }; 175 | } 176 | 177 | // 设置新光源的位置和可见性 178 | newLight.position.set(position.x, position.y, position.z); 179 | newLight.visible = visible; 180 | 181 | // 更新光源对象 182 | source.light = newLight; 183 | source.lightType = newType; 184 | 185 | // 将新光源添加到场景 186 | this.scene.add(newLight); 187 | 188 | // 更新渲染 189 | this.render(); 190 | } 191 | 192 | async cleanupAndClose(cancelled = false) { 193 | if (cancelled && this.currentNode) { 194 | try { 195 | await api.fetchApi("/lg_relight_ultra/cancel", { 196 | method: "POST", 197 | headers: { 198 | "Content-Type": "application/json", 199 | }, 200 | body: JSON.stringify({ 201 | node_id: this.currentNode.id 202 | }) 203 | }); 204 | console.log('[RelightNode] 已发送取消信号Cancellation signal sent'); 205 | } catch (error) { 206 | console.error('[RelightNode] 发送取消信号失败Failed to send cancel signal:', error); 207 | } 208 | } 209 | document.removeEventListener('mousemove', this.onCanvasMouseMoveHandler); 210 | document.removeEventListener('mouseup', this.onCanvasMouseUpHandler); 211 | this.lightSources.forEach(source => { 212 | if (source.indicator && source.indicator.parentNode) { 213 | source.indicator.remove(); 214 | } 215 | if (source.targetIndicator && source.targetIndicator.parentNode) { 216 | source.targetIndicator.remove(); 217 | } 218 | if (source.connectionLine && source.connectionLine.parentNode) { 219 | source.connectionLine.remove(); 220 | } 221 | }); 222 | if (this.scene) { 223 | this.lightSources.forEach(source => { 224 | this.scene.remove(source.light); 225 | if (source.lightType === 'spot' && source.light.target) { 226 | this.scene.remove(source.light.target); 227 | } 228 | }); 229 | this.lightSources = []; 230 | this.activeSourceIndex = -1; 231 | } 232 | this.isMovingLight = false; 233 | this.modal.close(); 234 | } 235 | 236 | applyChanges() { 237 | if (this.renderer && this.scene && this.camera && this.currentNode) { 238 | this.renderer.render(this.scene, this.camera); 239 | this.uploadCanvasResult(this.renderer.domElement, this.currentNode.id); 240 | this.saveLightConfiguration(this.currentNode.id); 241 | } 242 | this.cleanupAndClose(); 243 | } 244 | 245 | onCanvasMouseDown(event) { 246 | if (event.target !== this.canvasContainer && 247 | event.target !== this.displayRenderer?.domElement) return; 248 | 249 | const activeSource = this.activeSourceIndex !== -1 ? this.lightSources[this.activeSourceIndex] : null; 250 | 251 | // 鼠标右键,且当前有活动的聚光灯 252 | if (event.button === 2 && activeSource && activeSource.lightType === 'spot') { 253 | event.preventDefault(); 254 | event.stopPropagation(); 255 | 256 | // 切换到目标点编辑模式 257 | activeSource.editingTarget = true; 258 | 259 | // 创建或显示目标点指示器 260 | if (!activeSource.targetIndicator) { 261 | activeSource.targetIndicator = this.createTargetIndicator(activeSource.lightColor || '#ffffff'); 262 | this.canvasContainer.appendChild(activeSource.targetIndicator); 263 | } else { 264 | activeSource.targetIndicator.style.display = 'block'; 265 | } 266 | 267 | // 更新连接线 268 | if (!activeSource.connectionLine) { 269 | activeSource.connectionLine = document.createElement('div'); 270 | activeSource.connectionLine.className = 'spotlight-connection-line'; 271 | this.canvasContainer.appendChild(activeSource.connectionLine); 272 | } 273 | 274 | this.isMovingLight = true; 275 | document.addEventListener('mousemove', this.onCanvasMouseMoveHandler); 276 | document.addEventListener('mouseup', this.onCanvasMouseUpHandler); 277 | this.updateLightFromMouseEvent(event); 278 | 279 | return; 280 | } 281 | 282 | // 如果之前是在编辑目标点,现在切换回编辑光源位置 283 | if (activeSource && activeSource.editingTarget) { 284 | activeSource.editingTarget = false; 285 | } 286 | 287 | this.isMovingLight = true; 288 | document.addEventListener('mousemove', this.onCanvasMouseMoveHandler); 289 | document.addEventListener('mouseup', this.onCanvasMouseUpHandler); 290 | this.updateLightFromMouseEvent(event); 291 | event.stopPropagation(); 292 | } 293 | 294 | onCanvasMouseMove(event) { 295 | if (!this.isMovingLight) return; 296 | this.updateLightFromMouseEvent(event); 297 | event.stopPropagation(); 298 | } 299 | 300 | onCanvasMouseUp(event) { 301 | const activeSource = this.activeSourceIndex !== -1 ? this.lightSources[this.activeSourceIndex] : null; 302 | 303 | // 如果处于目标点编辑模式,鼠标抬起后完成目标点的放置 304 | if (activeSource && activeSource.editingTarget) { 305 | activeSource.editingTarget = false; 306 | 307 | // 隐藏目标点指示器,但保留连接线 308 | if (activeSource.targetIndicator) { 309 | activeSource.targetIndicator.style.display = 'none'; 310 | } 311 | } 312 | 313 | this.isMovingLight = false; 314 | document.removeEventListener('mousemove', this.onCanvasMouseMoveHandler); 315 | document.removeEventListener('mouseup', this.onCanvasMouseUpHandler); 316 | event.stopPropagation(); 317 | } 318 | 319 | updateLightFromMouseEvent(event) { 320 | if (!this.displayRenderer || !this.displayRenderer.domElement || this.activeSourceIndex === -1) return; 321 | const activeSource = this.lightSources[this.activeSourceIndex]; 322 | if (!activeSource) return; 323 | 324 | const mouseX = event.clientX; 325 | const mouseY = event.clientY; 326 | const rect = this.displayRenderer.domElement.getBoundingClientRect(); 327 | const x = Math.max(0, Math.min(1, (mouseX - rect.left) / rect.width)); 328 | const y = Math.max(0, Math.min(1, (mouseY - rect.top) / rect.height)); 329 | 330 | this.updateLightIndicatorExact(mouseX, mouseY, activeSource.indicator); 331 | 332 | // 移除X轴计算中的负号 333 | this.lightX = ((x * 2) - 1); 334 | this.lightY = ((1 - y) * 2) - 1; 335 | 336 | // 从深度图获取Z轴高度 337 | const zValue = SceneUtils.getZValueFromDepthMap(this.depthMapTexture, x, y, this.zOffset); 338 | 339 | const xValueEl = this.modal.querySelector('.light-x-value'); 340 | const yValueEl = this.modal.querySelector('.light-y-value'); 341 | const zValueEl = this.modal.querySelector('.light-z-value'); 342 | xValueEl.textContent = this.lightX.toFixed(2); 343 | yValueEl.textContent = this.lightY.toFixed(2); 344 | zValueEl.textContent = zValue.toFixed(2); 345 | 346 | // 更新光源位置,包括从深度图获取的Z值 347 | activeSource.position = { x: this.lightX, y: this.lightY, z: zValue }; 348 | activeSource.light.position.set(this.lightX, this.lightY, zValue); 349 | 350 | // 如果当前正在编辑目标点而不是光源位置 351 | if (activeSource.editingTarget && activeSource.lightType === 'spot') { 352 | // 更新目标点位置 353 | activeSource.targetPosition = { x: this.lightX, y: this.lightY, z: 0 }; 354 | activeSource.light.target.position.set(this.lightX, this.lightY, 0); 355 | 356 | // 更新目标点指示器的位置 357 | if (activeSource.targetIndicator) { 358 | this.updateLightIndicatorExact(mouseX, mouseY, activeSource.targetIndicator); 359 | } 360 | 361 | // 更新连接线 362 | this.updateSpotlightLine(activeSource); 363 | } else if (activeSource.lightType === 'spot') { 364 | // 如果没有设置过目标点,默认指向下方 365 | if (!activeSource.targetPosition) { 366 | activeSource.targetPosition = { 367 | x: this.lightX, 368 | y: this.lightY - 1, 369 | z: 0 370 | }; 371 | activeSource.light.target.position.set( 372 | activeSource.targetPosition.x, 373 | activeSource.targetPosition.y, 374 | activeSource.targetPosition.z 375 | ); 376 | } 377 | 378 | // 更新连接线 379 | this.updateSpotlightLine(activeSource); 380 | } 381 | 382 | this.render(); 383 | } 384 | 385 | updateLightIndicatorExact(clientX, clientY, indicator) { 386 | if (!indicator) return; 387 | const rect = this.canvasContainer.getBoundingClientRect(); 388 | const offsetX = clientX - rect.left; 389 | const offsetY = clientY - rect.top; 390 | indicator.style.position = 'absolute'; 391 | indicator.style.left = `${offsetX}px`; 392 | indicator.style.top = `${offsetY}px`; 393 | indicator.style.display = 'block'; 394 | } 395 | 396 | onSliderChange(event) { 397 | const sliderId = event.target.id; 398 | const value = parseFloat(event.target.value); 399 | const activeSource = this.lightSources[this.activeSourceIndex]; 400 | if (!activeSource) return; 401 | switch (sliderId) { 402 | case 'zOffset': 403 | this.zOffset = value; 404 | // 如果有活动光源,需要更新它的z坐标(需要重新从深度图获取基础z值) 405 | if (activeSource) { 406 | // 从深度图获取当前位置的基础Z值 407 | const rect = this.displayRenderer.domElement.getBoundingClientRect(); 408 | const indicator = activeSource.indicator; 409 | const indicatorRect = indicator.getBoundingClientRect(); 410 | const x = Math.max(0, Math.min(1, (indicatorRect.left + indicatorRect.width/2 - rect.left) / rect.width)); 411 | const y = Math.max(0, Math.min(1, (indicatorRect.top + indicatorRect.height/2 - rect.top) / rect.height)); 412 | 413 | // 重新计算Z值并更新光源位置 414 | const zValue = SceneUtils.getZValueFromDepthMap(this.depthMapTexture, x, y, this.zOffset); 415 | activeSource.position.z = zValue; 416 | activeSource.light.position.set( 417 | activeSource.position.x, 418 | activeSource.position.y, 419 | zValue 420 | ); 421 | 422 | // 更新显示的Z值 423 | const zValueEl = this.modal.querySelector('.light-z-value'); 424 | if (zValueEl) { 425 | zValueEl.textContent = zValue.toFixed(2); 426 | } 427 | } 428 | break; 429 | case 'lightIntensity': 430 | activeSource.light.intensity = value; 431 | activeSource.intensity = value; 432 | break; 433 | case 'ambientLight': 434 | if (this.ambientLight) { 435 | this.ambientLight.intensity = value; 436 | } 437 | break; 438 | case 'normalStrength': 439 | if (this.material) { 440 | // 将法线强度应用到材质 441 | this.material.normalScale.set(value, value); 442 | // 如果有必要,可以存储该值以便保存配置 443 | this.normalStrength = value; 444 | } 445 | break; 446 | case 'spotlightAngle': 447 | if (activeSource.lightType === 'spot') { 448 | activeSource.light.angle = value * Math.PI; 449 | activeSource.spotParams.angle = value * Math.PI; 450 | } 451 | break; 452 | case 'spotlightPenumbra': 453 | if (activeSource.lightType === 'spot') { 454 | activeSource.light.penumbra = value; 455 | activeSource.spotParams.penumbra = value; 456 | } 457 | break; 458 | case 'pointlightRadius': 459 | if (activeSource.lightType === 'point') { 460 | activeSource.light.distance = value; 461 | if (activeSource.pointParams) { 462 | activeSource.pointParams.radius = value; 463 | } else { 464 | activeSource.pointParams = { radius: value }; 465 | } 466 | } 467 | break; 468 | } 469 | this.render(); 470 | } 471 | 472 | async setupScene(texture, depthMap, normalMap, maskTexture = null) { 473 | try { 474 | // 存储深度图纹理引用 475 | this.depthMapTexture = depthMap; 476 | 477 | if (!texture.image || !texture.image.complete) { 478 | console.warn('[RelightNode] 纹理图像未完全加载Texture image not fully loaded'); 479 | await new Promise(resolve => setTimeout(resolve, 100)); 480 | } 481 | const imageWidth = texture.image.width; 482 | const imageHeight = texture.image.height; 483 | const imageAspect = imageWidth / imageHeight; 484 | 485 | const containerRect = this.canvasContainer.getBoundingClientRect(); 486 | const containerWidth = containerRect.width; 487 | const containerHeight = containerRect.height; 488 | const containerAspect = containerWidth / containerHeight; 489 | 490 | let displayWidth, displayHeight; 491 | if (imageAspect > containerAspect) { 492 | displayWidth = containerWidth; 493 | displayHeight = containerWidth / imageAspect; 494 | } else { 495 | displayHeight = containerHeight; 496 | displayWidth = containerHeight * imageAspect; 497 | } 498 | 499 | const frustumHeight = 2; 500 | const frustumWidth = frustumHeight * imageAspect; 501 | 502 | if (!this.isSceneSetup) { 503 | console.log('[RelightNode] 首次创建场景Creating a scene for the first time'); 504 | this.scene = new THREE.Scene(); 505 | this.camera = new THREE.OrthographicCamera( 506 | frustumWidth / -2, 507 | frustumWidth / 2, 508 | frustumHeight / 2, 509 | frustumHeight / -2, 510 | 0.1, 511 | 1000 512 | ); 513 | this.camera.position.z = 5; 514 | 515 | this.displayRenderer = new THREE.WebGLRenderer({ 516 | antialias: true, 517 | alpha: true 518 | }); 519 | 520 | this.renderer = new THREE.WebGLRenderer({ 521 | antialias: true, 522 | preserveDrawingBuffer: true, 523 | alpha: true 524 | }); 525 | 526 | this.ambientLight = new THREE.AmbientLight(0xffffff, 1.0); 527 | this.scene.add(this.ambientLight); 528 | this.isSceneSetup = true; 529 | } else { 530 | this.camera.left = frustumWidth / -2; 531 | this.camera.right = frustumWidth / 2; 532 | this.camera.top = frustumHeight / 2; 533 | this.camera.bottom = frustumHeight / -2; 534 | this.camera.updateProjectionMatrix(); 535 | } 536 | 537 | // 设置显示渲染器的尺寸和样式 538 | this.displayRenderer.setSize(displayWidth, displayHeight); 539 | const displayCanvas = this.displayRenderer.domElement; 540 | displayCanvas.style.maxWidth = '100%'; 541 | displayCanvas.style.maxHeight = '100%'; 542 | displayCanvas.style.margin = 'auto'; 543 | displayCanvas.style.position = 'absolute'; 544 | displayCanvas.style.left = '50%'; 545 | displayCanvas.style.top = '50%'; 546 | displayCanvas.style.transform = 'translate(-50%, -50%)'; 547 | 548 | if (!this.canvasContainer.contains(displayCanvas)) { 549 | this.canvasContainer.appendChild(displayCanvas); 550 | } 551 | 552 | // 设置输出渲染器为原始图像尺寸 553 | this.renderer.setSize(imageWidth, imageHeight); 554 | 555 | const geometry = new THREE.PlaneGeometry(2 * imageAspect, 2, 32, 32); 556 | 557 | // 获取法线强度值(如果已存在) 558 | const normalStrengthSlider = this.modal.querySelector('#normalStrength'); 559 | this.normalStrength = normalStrengthSlider ? parseFloat(normalStrengthSlider.value) : 0; 560 | 561 | let material; 562 | if (maskTexture) { 563 | material = SceneUtils.createMaskedMaterial(texture, depthMap, normalMap, maskTexture, this.normalStrength); 564 | } else { 565 | material = SceneUtils.createSimpleMaterial(texture, depthMap, normalMap, this.normalStrength); 566 | } 567 | if (this.mesh) { 568 | this.mesh.geometry.dispose(); 569 | this.mesh.geometry = geometry; 570 | this.mesh.material.dispose(); 571 | this.mesh.material = material; 572 | } else { 573 | this.mesh = new THREE.Mesh(geometry, material); 574 | this.scene.add(this.mesh); 575 | } 576 | this.material = material; 577 | this.renderer.setClearColor(0x000000, 0); 578 | const canvas = this.renderer.domElement; 579 | canvas.style.maxWidth = '100%'; 580 | canvas.style.maxHeight = '100%'; 581 | canvas.style.margin = 'auto'; 582 | canvas.style.position = 'absolute'; 583 | canvas.style.left = '50%'; 584 | canvas.style.top = '50%'; 585 | canvas.style.transform = 'translate(-50%, -50%)'; 586 | this.renderer.render(this.scene, this.camera); 587 | 588 | // 更新渲染方法 589 | this.render(); 590 | 591 | console.log('[RelightNode] 场景设置完成'); 592 | return true; 593 | } catch (error) { 594 | console.error('[RelightNode] 场景设置错误:', error); 595 | return false; 596 | } 597 | } 598 | 599 | uploadCanvasResult(canvas, nodeId) { 600 | // 使用输出渲染器的画布而不是显示渲染器的画布 601 | this.renderer.domElement.toBlob(async (blob) => { 602 | try { 603 | console.log('[RelightNode] 正在上传渲染结果...'); 604 | const formData = new FormData(); 605 | formData.append('node_id', nodeId); 606 | formData.append('result_image', blob, 'result.png'); 607 | const response = await api.fetchApi('/lg_relight/upload_result', { 608 | method: 'POST', 609 | body: formData 610 | }); 611 | const result = await response.json(); 612 | if (result.error) { 613 | console.error('[RelightNode] 上传失败:', result.error); 614 | } else { 615 | console.log('[RelightNode] 上传成功'); 616 | } 617 | } catch (error) { 618 | console.error('[RelightNode] 上传失败:', error); 619 | } 620 | }, 'image/png', 1.0); 621 | } 622 | 623 | createLightSource() { 624 | const lightIntensitySlider = this.modal.querySelector('#lightIntensity'); 625 | const lightTypeSelect = this.modal.querySelector('#lightType'); 626 | const lightType = lightTypeSelect ? lightTypeSelect.value : 'point'; 627 | 628 | let light; 629 | let indicator; 630 | const lightIntensity = parseFloat(lightIntensitySlider.value); 631 | 632 | // 根据选择的光源类型创建对应的光源 633 | if (lightType === 'spot') { 634 | const spotlightAngleSlider = this.modal.querySelector('#spotlightAngle'); 635 | const spotlightPenumbraSlider = this.modal.querySelector('#spotlightPenumbra'); 636 | const angle = spotlightAngleSlider ? parseFloat(spotlightAngleSlider.value) * Math.PI : Math.PI / 3; 637 | const penumbra = spotlightPenumbraSlider ? parseFloat(spotlightPenumbraSlider.value) : 0.2; 638 | 639 | light = new THREE.SpotLight(0xffffff, lightIntensity, 10, angle, penumbra); 640 | light.target.position.set(0, -1, 0); // 默认向下照射 641 | this.scene.add(light.target); 642 | 643 | indicator = this.createLightIndicator('#ffffff', 'spot'); 644 | } else { 645 | const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); 646 | const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; 647 | 648 | light = new THREE.PointLight(0xffffff, lightIntensity, radius, 2); 649 | indicator = this.createLightIndicator('#ffffff', 'point'); 650 | } 651 | 652 | const lightSource = { 653 | id: Date.now(), 654 | name: `光源 ${this.lightSources.length + 1}`, 655 | light: light, 656 | position: { x: 0, y: 0, z: 1.0 }, // 默认值,会在鼠标点击时更新 657 | intensity: lightIntensity, 658 | indicatorColor: '#ffffff', 659 | lightColor: '#ffffff', 660 | lightType: lightType, 661 | indicator: indicator, 662 | editingTarget: false // 是否正在编辑目标点 663 | }; 664 | 665 | // 如果是聚光灯,添加额外的聚光灯参数 666 | if (lightType === 'spot') { 667 | lightSource.spotParams = { 668 | angle: light.angle, 669 | penumbra: light.penumbra 670 | }; 671 | lightSource.targetPosition = { x: 0, y: -1, z: 0 }; 672 | 673 | // 创建连接线元素 674 | lightSource.connectionLine = document.createElement('div'); 675 | lightSource.connectionLine.className = 'spotlight-connection-line'; 676 | this.canvasContainer.appendChild(lightSource.connectionLine); 677 | 678 | // 创建目标点指示器 679 | lightSource.targetIndicator = this.createTargetIndicator('#ffffff'); 680 | lightSource.targetIndicator.style.display = 'none'; // 默认隐藏 681 | this.canvasContainer.appendChild(lightSource.targetIndicator); 682 | } else if (lightType === 'point') { 683 | // 为点光源添加半径参数 684 | const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); 685 | const radius = pointlightRadiusSlider ? parseFloat(pointlightRadiusSlider.value) : 10; 686 | lightSource.pointParams = { 687 | radius: radius 688 | }; 689 | } 690 | 691 | // 直接设置光源位置 692 | lightSource.light.position.set(0, 0, 1.0); 693 | this.scene.add(lightSource.light); 694 | this.lightSources.push(lightSource); 695 | this.setActiveLight(this.lightSources.length - 1); 696 | 697 | // 更新材质参数 - 固定为0 698 | if (this.material) { 699 | this.material.shininess = 0; 700 | this.material.specular.setRGB(0, 0, 0); 701 | 702 | // 从滑条获取法线强度值 703 | const normalStrengthSlider = this.modal.querySelector('#normalStrength'); 704 | const normalStrength = normalStrengthSlider ? parseFloat(normalStrengthSlider.value) : 0; 705 | this.material.normalScale.set(normalStrength, normalStrength); 706 | this.normalStrength = normalStrength; 707 | } 708 | 709 | // 确保环境光强度与UI滑块一致 710 | if (this.ambientLight) { 711 | const ambientLightSlider = this.modal.querySelector('#ambientLight'); 712 | if (ambientLightSlider) { 713 | this.ambientLight.intensity = parseFloat(ambientLightSlider.value); 714 | } 715 | } 716 | 717 | return lightSource; 718 | } 719 | 720 | createLightIndicator(color, type = 'point') { 721 | const indicator = document.createElement('div'); 722 | indicator.className = 'light-source-indicator'; 723 | indicator.style.backgroundColor = color; 724 | indicator.style.boxShadow = `0 0 15px ${color}`; 725 | 726 | // 根据光源类型设置指示器样式 727 | if (type === 'spot') { 728 | indicator.style.clipPath = 'polygon(50% 0%, 0% 100%, 100% 100%)'; 729 | indicator.style.transform = 'translate(-50%, -20%)'; 730 | } 731 | 732 | // 添加选中状态的外圈 733 | const selectionRing = document.createElement('div'); 734 | selectionRing.className = 'selection-ring'; 735 | selectionRing.style.display = 'none'; // 默认隐藏 736 | selectionRing.style.borderColor = '#00ff00'; // 使用亮绿色 737 | indicator.appendChild(selectionRing); 738 | 739 | this.canvasContainer.appendChild(indicator); 740 | return indicator; 741 | } 742 | 743 | updateLightSourcesList() { 744 | const listContainer = this.modal.querySelector('.light-sources-list'); 745 | if (!listContainer) return; 746 | listContainer.innerHTML = ''; 747 | this.lightSources.forEach((source, index) => { 748 | const item = document.createElement('div'); 749 | item.className = `light-source-item ${index === this.activeSourceIndex ? 'active' : ''}`; 750 | 751 | // 根据光源的可见状态选择眼睛图标 752 | const visibilityIcon = source.light.visible ? '👁️' : '👁️‍🗨️'; 753 | 754 | // 添加光源类型图标 755 | const typeIcon = source.lightType === 'spot' ? '🔦' : '💡'; 756 | 757 | item.innerHTML = ` 758 |
759 |
760 | ${typeIcon} ${source.name} 761 |
762 | 763 | 764 | 765 |
766 |
767 | `; 768 | // 添加数据属性以识别索引 769 | item.dataset.lightIndex = index; 770 | item.addEventListener('click', (e) => { 771 | // 添加点击时的日志输出 772 | console.log(`[RelightNode] 点击了光源项 Click on the light source ${index}, 当前活动光源 Currently active light source: ${this.activeSourceIndex}`); 773 | this.setActiveLight(index); 774 | }); 775 | const colorPicker = item.querySelector('.light-color-picker'); 776 | colorPicker.addEventListener('input', (e) => { 777 | e.stopPropagation(); 778 | this.updateLightColor(index, e.target.value); 779 | }); 780 | const deleteBtn = item.querySelector('.light-source-delete'); 781 | if (deleteBtn) { 782 | deleteBtn.addEventListener('click', (e) => { 783 | e.stopPropagation(); 784 | this.deleteLight(index); 785 | }); 786 | } 787 | const visibilityBtn = item.querySelector('.light-source-visibility'); 788 | if (visibilityBtn) { 789 | visibilityBtn.addEventListener('click', (e) => { 790 | e.stopPropagation(); 791 | this.toggleLightVisibility(index); 792 | // 更新按钮图标 793 | visibilityBtn.textContent = source.light.visible ? '👁️' : '👁️‍🗨️'; 794 | visibilityBtn.title = source.light.visible ? '隐藏Hide' : '显示Show'; 795 | }); 796 | } 797 | listContainer.appendChild(item); 798 | }); 799 | } 800 | 801 | setActiveLight(index) { 802 | console.log(`[RelightNode] 设置活动光源Set active light source: ${index}, 当前光源数量Current number of light sources: ${this.lightSources.length}`); 803 | 804 | // 先清除所有光源的选中状态 805 | this.lightSources.forEach(source => { 806 | if (source.indicator) { 807 | const ring = source.indicator.querySelector('.selection-ring'); 808 | if (ring) { 809 | ring.style.display = 'none'; 810 | } 811 | } 812 | }); 813 | 814 | // 设置新的选中光源 815 | this.activeSourceIndex = index; 816 | const source = this.lightSources[index]; 817 | if (source && source.indicator) { 818 | const ring = source.indicator.querySelector('.selection-ring'); 819 | if (ring) { 820 | console.log(`[RelightNode] 显示光源Display Light Source ${index} 的选择环Selection ring`); 821 | ring.style.display = 'block'; 822 | } else { 823 | console.warn(`[RelightNode] 光源 ${index} 没有选择环元素No ring element selected`); 824 | } 825 | } else { 826 | console.warn(`[RelightNode] 光源light source ${index} 或其指示器不存在or its indicator does not exist`); 827 | } 828 | 829 | // 更新光源列表UI中的活动项 830 | const listItems = this.modal.querySelectorAll('.light-source-item'); 831 | listItems.forEach(item => { 832 | const itemIndex = parseInt(item.dataset.lightIndex); 833 | if (itemIndex === index) { 834 | item.classList.add('active'); 835 | } else { 836 | item.classList.remove('active'); 837 | } 838 | }); 839 | 840 | // 更新控制面板的值 841 | if (source) { 842 | const lightIntensitySlider = this.modal.querySelector('#lightIntensity'); 843 | if (lightIntensitySlider) { 844 | lightIntensitySlider.value = source.light.intensity; 845 | } 846 | const xValueEl = this.modal.querySelector('.light-x-value'); 847 | const yValueEl = this.modal.querySelector('.light-y-value'); 848 | const zValueEl = this.modal.querySelector('.light-z-value'); 849 | if (xValueEl && yValueEl && zValueEl) { 850 | xValueEl.textContent = source.position.x.toFixed(2); 851 | yValueEl.textContent = source.position.y.toFixed(2); 852 | zValueEl.textContent = source.position.z.toFixed(2); 853 | } 854 | 855 | // 更新光源类型选择器 856 | const lightTypeSelect = this.modal.querySelector('#lightType'); 857 | if (lightTypeSelect) { 858 | lightTypeSelect.value = source.lightType || 'point'; 859 | 860 | // 显示或隐藏聚光灯控制项 861 | const spotlightControls = this.modal.querySelectorAll('.spotlight-controls'); 862 | spotlightControls.forEach(control => { 863 | control.style.display = source.lightType === 'spot' ? 'block' : 'none'; 864 | }); 865 | 866 | // 显示或隐藏点光源控制项 867 | const pointlightControls = this.modal.querySelectorAll('.pointlight-controls'); 868 | pointlightControls.forEach(control => { 869 | control.style.display = source.lightType === 'point' ? 'block' : 'none'; 870 | }); 871 | 872 | // 更新聚光灯参数 873 | if (source.lightType === 'spot' && source.spotParams) { 874 | const spotlightAngleSlider = this.modal.querySelector('#spotlightAngle'); 875 | const spotlightPenumbraSlider = this.modal.querySelector('#spotlightPenumbra'); 876 | 877 | if (spotlightAngleSlider) { 878 | spotlightAngleSlider.value = source.spotParams.angle / Math.PI; 879 | } 880 | 881 | if (spotlightPenumbraSlider) { 882 | spotlightPenumbraSlider.value = source.spotParams.penumbra; 883 | } 884 | } 885 | 886 | // 更新点光源参数 887 | if (source.lightType === 'point' && source.pointParams) { 888 | const pointlightRadiusSlider = this.modal.querySelector('#pointlightRadius'); 889 | 890 | if (pointlightRadiusSlider) { 891 | pointlightRadiusSlider.value = source.pointParams.radius; 892 | } 893 | } 894 | } 895 | } 896 | } 897 | 898 | deleteLight(index) { 899 | const source = this.lightSources[index]; 900 | if (source) { 901 | this.scene.remove(source.light); 902 | if (source.lightType === 'spot' && source.light.target) { 903 | this.scene.remove(source.light.target); 904 | } 905 | if (source.indicator && source.indicator.parentNode) { 906 | source.indicator.remove(); 907 | } 908 | if (source.connectionLine && source.connectionLine.parentNode) { 909 | source.connectionLine.remove(); 910 | } 911 | if (source.targetIndicator && source.targetIndicator.parentNode) { 912 | source.targetIndicator.remove(); 913 | } 914 | this.lightSources.splice(index, 1); 915 | if (this.activeSourceIndex === index) { 916 | this.activeSourceIndex = Math.max(-1, this.lightSources.length - 1); 917 | } else if (this.activeSourceIndex > index) { 918 | this.activeSourceIndex--; 919 | } 920 | if (this.lightSources.length === 0) { 921 | this.activeSourceIndex = -1; 922 | if (this.ambientLight) { 923 | this.ambientLight.intensity = 1.0; 924 | } 925 | } 926 | this.updateLightSourcesList(); 927 | this.render(); 928 | } 929 | } 930 | 931 | toggleLightVisibility(index) { 932 | const source = this.lightSources[index]; 933 | if (source) { 934 | source.light.visible = !source.light.visible; 935 | source.indicator.style.opacity = source.light.visible ? '0.7' : '0.2'; 936 | this.render(); 937 | } 938 | } 939 | 940 | updateLightIntensity(index, value) { 941 | const source = this.lightSources[index]; 942 | if (source) { 943 | source.intensity = value; 944 | source.light.intensity = value; 945 | this.render(); 946 | } 947 | } 948 | 949 | render() { 950 | if (this.displayRenderer && this.renderer && this.scene && this.camera) { 951 | // 更新显示用的画布 952 | this.displayRenderer.render(this.scene, this.camera); 953 | // 同时更新用于输出的画布 954 | this.renderer.render(this.scene, this.camera); 955 | } 956 | } 957 | 958 | async show(nodeId, detail) { 959 | try { 960 | this.currentNode = app.graph.getNodeById(nodeId); 961 | if (!this.currentNode) { 962 | console.error('[RelightNode] 找不到节点Node not found:', nodeId); 963 | return; 964 | } 965 | console.log('[RelightNode] 开始处理图像Start processing the image...'); 966 | const { bg_image, bg_depth_map, bg_normal_map, has_mask, mask } = detail; 967 | this.hasMask = has_mask; 968 | this.modal.showModal(); 969 | 970 | // 清理之前可能存在的光源 971 | this.lightSources.forEach(source => { 972 | if (this.scene) { 973 | this.scene.remove(source.light); 974 | if (source.lightType === 'spot' && source.light.target) { 975 | this.scene.remove(source.light.target); 976 | } 977 | } 978 | if (source.indicator && source.indicator.parentNode) { 979 | source.indicator.remove(); 980 | } 981 | if (source.targetIndicator && source.targetIndicator.parentNode) { 982 | source.targetIndicator.remove(); 983 | } 984 | if (source.connectionLine && source.connectionLine.parentNode) { 985 | source.connectionLine.remove(); 986 | } 987 | }); 988 | this.lightSources = []; 989 | this.activeSourceIndex = -1; 990 | 991 | const texturePromises = [ 992 | SceneUtils.base64ToTexture(bg_image), 993 | SceneUtils.base64ToTexture(bg_depth_map), 994 | SceneUtils.base64ToTexture(bg_normal_map) 995 | ]; 996 | if (has_mask && mask) { 997 | texturePromises.push(SceneUtils.base64ToTexture(mask)); 998 | console.log('[RelightNode] 检测到遮罩数据,将加载遮罩纹理Mask data is detected and the mask texture will be loaded'); 999 | } 1000 | const loadedTextures = await Promise.all(texturePromises); 1001 | const texture = loadedTextures[0]; 1002 | const depthMap = loadedTextures[1]; 1003 | const normalMap = loadedTextures[2]; 1004 | const maskTexture = has_mask ? loadedTextures[3] : null; 1005 | console.log('[RelightNode] 纹理加载完成,设置场景Texture loading complete, set up the scene...'); 1006 | await this.setupScene(texture, depthMap, normalMap, maskTexture); 1007 | 1008 | // 移除画布上的所有指示器元素 1009 | const existingIndicators = this.canvasContainer.querySelectorAll('.light-source-indicator, .spotlight-target-indicator, .spotlight-connection-line'); 1010 | existingIndicators.forEach(indicator => indicator.remove()); 1011 | 1012 | const configRestored = await this.restoreLightConfiguration(nodeId); 1013 | if (!configRestored) { 1014 | console.log('[RelightNode] 没有找到已保存的配置,使用空白配置No saved configuration found, using a blank configuration'); 1015 | // 没有恢复到配置,保持空白状态 1016 | } 1017 | 1018 | if (this.renderer && this.scene && this.camera) { 1019 | this.renderer.render(this.scene, this.camera); 1020 | } 1021 | 1022 | // 重新绑定添加光源按钮事件 1023 | const addLightBtn = this.modal.querySelector('.relight-btn.add-light'); 1024 | if (addLightBtn) { 1025 | // 移除已有的事件监听器,避免重复添加 1026 | const newAddLightBtn = addLightBtn.cloneNode(true); 1027 | addLightBtn.parentNode.replaceChild(newAddLightBtn, addLightBtn); 1028 | 1029 | // 添加新的事件监听器 1030 | newAddLightBtn.addEventListener('click', () => { 1031 | console.log('[RelightNode] 添加新光源Adding a New Light'); 1032 | if (this.scene) { 1033 | const newSource = this.createLightSource(); 1034 | console.log('[RelightNode] 新光源已创建New light source created,ID:', newSource.id); 1035 | this.updateLightSourcesList(); 1036 | } else { 1037 | console.error('[RelightNode] 场景未初始化,无法添加光源The scene is not initialized, so light sources cannot be added.'); 1038 | } 1039 | }); 1040 | } 1041 | 1042 | // 初始化光源列表 1043 | this.updateLightSourcesList(); 1044 | 1045 | console.log('[RelightNode] 编辑器显示成功,当前光源数量The editor displays success, the current number of light sources:', this.lightSources.length); 1046 | } catch (error) { 1047 | console.error('[RelightNode] 处理图像时出错Error processing image:', error); 1048 | } 1049 | } 1050 | 1051 | updateLightColor(index, color) { 1052 | const source = this.lightSources[index]; 1053 | if (source) { 1054 | // 更新光源颜色 1055 | source.lightColor = color; 1056 | source.indicatorColor = color; // 同时更新指示器颜色 1057 | const colorObj = new THREE.Color(color); 1058 | source.light.color = colorObj; 1059 | 1060 | // 更新光源列表中的颜色 1061 | const listContainer = this.modal.querySelector('.light-sources-list'); 1062 | if (listContainer) { 1063 | const lightItem = listContainer.children[index]; 1064 | if (lightItem) { 1065 | // 更新色板颜色 1066 | const colorPicker = lightItem.querySelector('.light-color-picker'); 1067 | if (colorPicker) { 1068 | colorPicker.value = color; 1069 | } 1070 | // 更新指示器颜色 1071 | const indicatorColor = lightItem.querySelector('.light-source-color'); 1072 | if (indicatorColor) { 1073 | indicatorColor.style.backgroundColor = color; 1074 | } 1075 | } 1076 | } 1077 | 1078 | // 更新画布中的光源指示器颜色 1079 | if (source.indicator) { 1080 | source.indicator.style.backgroundColor = color; 1081 | source.indicator.style.boxShadow = `0 0 15px ${color}`; 1082 | } 1083 | 1084 | this.render(); 1085 | } 1086 | } 1087 | 1088 | saveLightConfiguration(nodeId) { 1089 | const config = { 1090 | lights: this.lightSources.map(source => ({ 1091 | screenX: source.indicator.offsetLeft, 1092 | screenY: source.indicator.offsetTop, 1093 | position: { ...source.position }, 1094 | targetPosition: source.targetPosition ? { ...source.targetPosition } : null, 1095 | intensity: source.intensity, 1096 | color: source.lightColor, 1097 | visible: source.light.visible, 1098 | lightType: source.lightType || 'point', 1099 | spotParams: source.spotParams || null, 1100 | pointParams: source.pointParams || null 1101 | })), 1102 | ambientLight: { 1103 | intensity: this.ambientLight.intensity 1104 | }, 1105 | material: { 1106 | normalScale: this.normalStrength || 0, 1107 | shininess: this.material.shininess, 1108 | specularStrength: this.material.specular.r 1109 | }, 1110 | zOffset: this.zOffset 1111 | }; 1112 | if (this.currentNode) { 1113 | this.currentNode.lightConfig = config; 1114 | } 1115 | } 1116 | 1117 | async restoreLightConfiguration(nodeId) { 1118 | const node = app.graph.getNodeById(nodeId); 1119 | if (!node || !node.lightConfig) return false; 1120 | const config = node.lightConfig; 1121 | this.lightSources.forEach(source => { 1122 | this.scene.remove(source.light); 1123 | if (source.lightType === 'spot' && source.light.target) { 1124 | this.scene.remove(source.light.target); 1125 | } 1126 | if (source.indicator && source.indicator.parentNode) { 1127 | source.indicator.remove(); 1128 | } 1129 | if (source.targetIndicator && source.targetIndicator.parentNode) { 1130 | source.targetIndicator.remove(); 1131 | } 1132 | if (source.connectionLine && source.connectionLine.parentNode) { 1133 | source.connectionLine.remove(); 1134 | } 1135 | }); 1136 | this.lightSources = []; 1137 | 1138 | // 设置光源类型选择器为默认值 1139 | const lightTypeSelect = this.modal.querySelector('#lightType'); 1140 | if (lightTypeSelect) { 1141 | lightTypeSelect.value = 'point'; 1142 | } 1143 | 1144 | for (const lightConfig of config.lights) { 1145 | // 临时设置类型选择器的值,这样创建光源时会使用正确的类型 1146 | if (lightConfig.lightType && lightTypeSelect) { 1147 | lightTypeSelect.value = lightConfig.lightType; 1148 | } 1149 | 1150 | const source = this.createLightSource(); 1151 | source.position = { ...lightConfig.position }; 1152 | source.light.position.set( 1153 | lightConfig.position.x, 1154 | lightConfig.position.y, 1155 | lightConfig.position.z 1156 | ); 1157 | source.intensity = lightConfig.intensity; 1158 | source.light.intensity = lightConfig.intensity; 1159 | source.light.visible = lightConfig.visible; 1160 | source.lightType = lightConfig.lightType || 'point'; 1161 | 1162 | // 恢复聚光灯参数 1163 | if (source.lightType === 'spot' && lightConfig.spotParams) { 1164 | source.spotParams = { ...lightConfig.spotParams }; 1165 | source.light.angle = lightConfig.spotParams.angle; 1166 | source.light.penumbra = lightConfig.spotParams.penumbra; 1167 | 1168 | // 恢复目标点位置 1169 | if (lightConfig.targetPosition) { 1170 | source.targetPosition = { ...lightConfig.targetPosition }; 1171 | source.light.target.position.set( 1172 | lightConfig.targetPosition.x, 1173 | lightConfig.targetPosition.y, 1174 | lightConfig.targetPosition.z 1175 | ); 1176 | } else { 1177 | // 如果没有保存目标点,使用默认位置 1178 | source.targetPosition = { 1179 | x: lightConfig.position.x, 1180 | y: lightConfig.position.y - 1, 1181 | z: 0 1182 | }; 1183 | source.light.target.position.set( 1184 | source.targetPosition.x, 1185 | source.targetPosition.y, 1186 | source.targetPosition.z 1187 | ); 1188 | } 1189 | } 1190 | 1191 | // 恢复点光源参数 1192 | if (source.lightType === 'point' && lightConfig.pointParams) { 1193 | source.pointParams = { ...lightConfig.pointParams }; 1194 | source.light.distance = lightConfig.pointParams.radius; 1195 | } 1196 | 1197 | if (lightConfig.color) { 1198 | this.updateLightColor(this.lightSources.length - 1, lightConfig.color); 1199 | } 1200 | 1201 | if (this.canvasContainer && source.indicator) { 1202 | source.indicator.style.left = `${lightConfig.screenX}px`; 1203 | source.indicator.style.top = `${lightConfig.screenY}px`; 1204 | source.indicator.style.opacity = lightConfig.visible ? '0.7' : '0.2'; 1205 | 1206 | // 立即更新聚光灯连接线 1207 | if (source.lightType === 'spot') { 1208 | // 确保DOM元素已完全加载并计算好尺寸 1209 | setTimeout(() => { 1210 | this.updateSpotlightLine(source); 1211 | console.log('[RelightNode] 更新聚光灯连接线Update spotlight connector:', source.name); 1212 | }, 50); 1213 | } 1214 | } 1215 | } 1216 | 1217 | if (config.ambientLight && this.ambientLight) { 1218 | this.ambientLight.intensity = config.ambientLight.intensity; 1219 | const ambientLightSlider = this.modal.querySelector('#ambientLight'); 1220 | if (ambientLightSlider) { 1221 | ambientLightSlider.value = config.ambientLight.intensity; 1222 | } 1223 | } 1224 | if (config.material && this.material) { 1225 | // 恢复法线强度 1226 | if (config.material.normalScale !== undefined) { 1227 | this.normalStrength = config.material.normalScale; 1228 | this.material.normalScale.set(this.normalStrength, this.normalStrength); 1229 | 1230 | const normalStrengthSlider = this.modal.querySelector('#normalStrength'); 1231 | if (normalStrengthSlider) { 1232 | normalStrengthSlider.value = this.normalStrength; 1233 | } 1234 | } else { 1235 | // 默认设置为0 1236 | this.material.normalScale.set(0, 0); 1237 | this.normalStrength = 0; 1238 | } 1239 | 1240 | // 恢复其他材质参数,但固定为0 1241 | this.material.shininess = 0; 1242 | this.material.specular.setRGB(0, 0, 0); 1243 | } 1244 | if (config.zOffset !== undefined) { 1245 | this.zOffset = config.zOffset; 1246 | const zOffsetSlider = this.modal.querySelector('#zOffset'); 1247 | if (zOffsetSlider) { 1248 | zOffsetSlider.value = config.zOffset; 1249 | } 1250 | } 1251 | 1252 | // 确保所有聚光灯连接线都更新 1253 | setTimeout(() => { 1254 | this.lightSources.forEach(source => { 1255 | if (source.lightType === 'spot') { 1256 | this.updateSpotlightLine(source); 1257 | } 1258 | }); 1259 | }, 100); 1260 | 1261 | this.updateLightSourcesList(); 1262 | this.render(); 1263 | return true; 1264 | } 1265 | 1266 | async processWithoutDialog(nodeId, detail) { 1267 | try { 1268 | this.currentNode = app.graph.getNodeById(nodeId); 1269 | if (!this.currentNode) { 1270 | console.error('[RelightNode] 找不到节点Node not found:', nodeId); 1271 | return; 1272 | } 1273 | 1274 | console.log('[RelightNode] 开始无弹窗处理图像Start processing images without pop-up window...'); 1275 | const { bg_image, bg_depth_map, bg_normal_map, has_mask, mask } = detail; 1276 | this.hasMask = has_mask; 1277 | 1278 | // 加载纹理 1279 | const texturePromises = [ 1280 | SceneUtils.base64ToTexture(bg_image), 1281 | SceneUtils.base64ToTexture(bg_depth_map), 1282 | SceneUtils.base64ToTexture(bg_normal_map) 1283 | ]; 1284 | 1285 | if (has_mask && mask) { 1286 | texturePromises.push(SceneUtils.base64ToTexture(mask)); 1287 | } 1288 | 1289 | const loadedTextures = await Promise.all(texturePromises); 1290 | const texture = loadedTextures[0]; 1291 | const depthMap = loadedTextures[1]; 1292 | const normalMap = loadedTextures[2]; 1293 | const maskTexture = has_mask ? loadedTextures[3] : null; 1294 | 1295 | // 设置场景但不显示 1296 | if (!this.scene) { 1297 | // 首次创建场景 1298 | this.scene = new THREE.Scene(); 1299 | const imageWidth = texture.image.width; 1300 | const imageHeight = texture.image.height; 1301 | const imageAspect = imageWidth / imageHeight; 1302 | 1303 | const frustumHeight = 2; 1304 | const frustumWidth = frustumHeight * imageAspect; 1305 | 1306 | this.camera = new THREE.OrthographicCamera( 1307 | frustumWidth / -2, 1308 | frustumWidth / 2, 1309 | frustumHeight / 2, 1310 | frustumHeight / -2, 1311 | 0.1, 1312 | 1000 1313 | ); 1314 | this.camera.position.z = 5; 1315 | 1316 | this.renderer = new THREE.WebGLRenderer({ 1317 | antialias: true, 1318 | preserveDrawingBuffer: true, 1319 | alpha: true 1320 | }); 1321 | 1322 | this.ambientLight = new THREE.AmbientLight(0xffffff, 0.2); 1323 | this.scene.add(this.ambientLight); 1324 | this.isSceneSetup = true; 1325 | 1326 | // 隐藏渲染器设置 1327 | this.renderer.setSize(imageWidth, imageHeight); 1328 | } 1329 | 1330 | // 设置临时场景 1331 | await this.setupTemporaryScene(texture, depthMap, normalMap, maskTexture); 1332 | 1333 | // 尝试恢复配置,如果没有则使用默认配置 1334 | const configRestored = await this.restoreLightConfiguration(nodeId); 1335 | if (!configRestored) { 1336 | // 没有现有配置,创建默认配置 1337 | this.createDefaultLight(); 1338 | } 1339 | 1340 | // 渲染场景 1341 | this.renderer.render(this.scene, this.camera); 1342 | 1343 | // 上传结果 1344 | this.uploadCanvasResult(this.renderer.domElement, nodeId); 1345 | 1346 | // 保存当前配置以供将来使用 1347 | this.saveLightConfiguration(nodeId); 1348 | 1349 | console.log('[RelightNode] 无弹窗处理完成No pop-up window processing completed'); 1350 | } catch (error) { 1351 | console.error('[RelightNode] 无弹窗处理错误No pop-up window processing error:', error); 1352 | } 1353 | } 1354 | 1355 | async setupTemporaryScene(texture, depthMap, normalMap, maskTexture = null) { 1356 | // 类似setupScene但简化版本,仅用于无弹窗处理 1357 | try { 1358 | const imageWidth = texture.image.width; 1359 | const imageHeight = texture.image.height; 1360 | const imageAspect = imageWidth / imageHeight; 1361 | 1362 | // 更新相机视锥体以匹配新图像的宽高比 1363 | const frustumHeight = 2; 1364 | const frustumWidth = frustumHeight * imageAspect; 1365 | 1366 | this.camera.left = frustumWidth / -2; 1367 | this.camera.right = frustumWidth / 2; 1368 | this.camera.top = frustumHeight / 2; 1369 | this.camera.bottom = frustumHeight / -2; 1370 | this.camera.updateProjectionMatrix(); 1371 | 1372 | // 设置渲染器尺寸 1373 | this.renderer.setSize(imageWidth, imageHeight); 1374 | 1375 | // 创建或更新几何体 1376 | const geometry = new THREE.PlaneGeometry(2 * imageAspect, 2, 32, 32); 1377 | 1378 | // 获取法线强度值(如果已存在) 1379 | this.normalStrength = this.normalStrength || 0; 1380 | 1381 | // 创建材质 1382 | let material; 1383 | if (maskTexture) { 1384 | material = SceneUtils.createMaskedMaterial(texture, depthMap, normalMap, maskTexture, this.normalStrength); 1385 | } else { 1386 | material = SceneUtils.createSimpleMaterial(texture, depthMap, normalMap, this.normalStrength); 1387 | } 1388 | 1389 | // 更新或创建网格 1390 | if (this.mesh) { 1391 | this.mesh.geometry.dispose(); 1392 | this.mesh.geometry = geometry; 1393 | this.mesh.material.dispose(); 1394 | this.mesh.material = material; 1395 | } else { 1396 | this.mesh = new THREE.Mesh(geometry, material); 1397 | this.scene.add(this.mesh); 1398 | } 1399 | 1400 | this.material = material; 1401 | this.renderer.setClearColor(0x000000, 0); 1402 | 1403 | return true; 1404 | } catch (error) { 1405 | console.error('[RelightNode] 临时场景设置错误Temporary scene setting error:', error); 1406 | return false; 1407 | } 1408 | } 1409 | 1410 | createDefaultLight() { 1411 | this.lightSources.forEach(source => { 1412 | this.scene.remove(source.light); 1413 | if (source.lightType === 'spot' && source.light.target) { 1414 | this.scene.remove(source.light.target); 1415 | } 1416 | }); 1417 | this.lightSources = []; 1418 | 1419 | const defaultLight = { 1420 | id: Date.now(), 1421 | name: "默认光源", 1422 | // 创建默认点光源 1423 | light: new THREE.PointLight(0xffffff, 1.0, 10, 2), 1424 | position: { x: 0, y: 0, z: 1.0 }, 1425 | intensity: 1.0, 1426 | lightColor: '#ffffff', 1427 | visible: true, 1428 | lightType: 'point', 1429 | pointParams: { 1430 | radius: 10 1431 | } 1432 | }; 1433 | 1434 | // 直接设置点光源位置 1435 | defaultLight.light.position.set(0, 0, 1); 1436 | this.scene.add(defaultLight.light); 1437 | this.lightSources.push(defaultLight); 1438 | 1439 | if (this.ambientLight) { 1440 | this.ambientLight.intensity = 0.2; 1441 | } 1442 | 1443 | return defaultLight; 1444 | } 1445 | 1446 | createTargetIndicator(color) { 1447 | const indicator = document.createElement('div'); 1448 | indicator.className = 'spotlight-target-indicator'; 1449 | indicator.style.backgroundColor = color; 1450 | return indicator; 1451 | } 1452 | 1453 | updateSpotlightLine(source) { 1454 | if (!source.connectionLine || !source.indicator || !source.targetPosition) return; 1455 | 1456 | const lightRect = source.indicator.getBoundingClientRect(); 1457 | const canvasRect = this.canvasContainer.getBoundingClientRect(); 1458 | 1459 | // 计算光源中心点 1460 | const lightX = lightRect.left + lightRect.width/2 - canvasRect.left; 1461 | const lightY = lightRect.top + lightRect.height/2 - canvasRect.top; 1462 | 1463 | // 如果有目标点指示器,使用它的位置 1464 | let targetX, targetY; 1465 | if (source.targetIndicator && source.targetIndicator.style.display !== 'none') { 1466 | const targetRect = source.targetIndicator.getBoundingClientRect(); 1467 | targetX = targetRect.left + targetRect.width/2 - canvasRect.left; 1468 | targetY = targetRect.top + targetRect.height/2 - canvasRect.top; 1469 | } else { 1470 | // 否则使用目标点在3D空间中的位置计算屏幕位置 1471 | // 这需要将3D空间点投影到屏幕空间 1472 | // 简化处理:用已有信息估算 1473 | const displayRect = this.displayRenderer.domElement.getBoundingClientRect(); 1474 | const targetPosNormalized = { 1475 | x: (source.targetPosition.x + 1) / 2, 1476 | y: (1 - source.targetPosition.y) / 2 1477 | }; 1478 | targetX = displayRect.left + displayRect.width * targetPosNormalized.x - canvasRect.left; 1479 | targetY = displayRect.top + displayRect.height * targetPosNormalized.y - canvasRect.top; 1480 | } 1481 | 1482 | // 计算线段长度和角度 1483 | const length = Math.sqrt(Math.pow(targetX - lightX, 2) + Math.pow(targetY - lightY, 2)); 1484 | const angle = Math.atan2(targetY - lightY, targetX - lightX) * 180 / Math.PI; 1485 | 1486 | // 设置线段样式 1487 | source.connectionLine.style.width = `${length}px`; 1488 | source.connectionLine.style.left = `${lightX}px`; 1489 | source.connectionLine.style.top = `${lightY}px`; 1490 | source.connectionLine.style.transform = `rotate(${angle}deg)`; 1491 | source.connectionLine.style.transformOrigin = 'left center'; 1492 | source.connectionLine.style.display = 'block'; 1493 | } 1494 | } 1495 | --------------------------------------------------------------------------------