support panoramic image in 3d node (#6638)

## Summary

Adds panoramic image support to the 3D node viewer, allowing users to
display equirectangular panoramic images as immersive backgrounds
alongside the existing tiled image mode.

## Changes

- Toggle between tiled and panorama rendering modes for background
images
- Field of view (FOV) control for panorama mode
- Refactored FOV slider into reusable PopupSlider component

## Screenshots


https://github.com/user-attachments/assets/8955d74b-b0e6-4b26-83ca-ccf902b43aa6

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6638-support-panoramic-image-in-3d-node-2a56d73d365081b98647f988130e312e)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2025-11-11 04:02:12 -05:00
committed by GitHub
parent c94cedf8ee
commit 879cb8f1a8
15 changed files with 310 additions and 84 deletions

View File

@@ -162,7 +162,11 @@ class Load3DConfiguration {
return
}
this.load3d.setBackgroundImage(config.backgroundImage)
void this.load3d.setBackgroundImage(config.backgroundImage)
if (config.backgroundRenderMode) {
this.load3d.setBackgroundRenderMode(config.backgroundRenderMode)
}
}
}

View File

@@ -510,6 +510,11 @@ class Load3d {
this.forceRender()
}
setBackgroundRenderMode(mode: 'tiled' | 'panorama'): void {
this.sceneManager.setBackgroundRenderMode(mode)
this.forceRender()
}
toggleCamera(cameraType?: 'perspective' | 'orthographic'): void {
this.cameraManager.toggleCamera(cameraType)

View File

@@ -3,6 +3,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Load3dUtils from './Load3dUtils'
import {
type BackgroundRenderModeType,
type EventManagerInterface,
type SceneManagerInterface
} from './interfaces'
@@ -16,6 +17,8 @@ export class SceneManager implements SceneManagerInterface {
backgroundMesh: THREE.Mesh | null = null
backgroundTexture: THREE.Texture | null = null
backgroundRenderMode: 'tiled' | 'panorama' = 'tiled'
backgroundColorMaterial: THREE.MeshBasicMaterial | null = null
currentBackgroundType: 'color' | 'image' = 'color'
currentBackgroundColor: string = '#282828'
@@ -89,6 +92,10 @@ export class SceneManager implements SceneManagerInterface {
}
}
if (this.scene.background) {
this.scene.background = null
}
this.scene.clear()
}
@@ -104,6 +111,15 @@ export class SceneManager implements SceneManagerInterface {
this.currentBackgroundColor = color
this.currentBackgroundType = 'color'
if (this.scene.background instanceof THREE.Texture) {
this.scene.background = null
}
if (this.backgroundRenderMode === 'panorama') {
this.backgroundRenderMode = 'tiled'
this.eventManager.emitEvent('backgroundRenderModeChange', 'tiled')
}
if (!this.backgroundMesh || !this.backgroundColorMaterial) {
this.initBackgroundScene()
}
@@ -168,36 +184,41 @@ export class SceneManager implements SceneManagerInterface {
this.backgroundTexture = texture
this.currentBackgroundType = 'image'
if (!this.backgroundMesh) {
this.initBackgroundScene()
}
const imageMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
depthWrite: false,
depthTest: false,
side: THREE.DoubleSide
})
if (this.backgroundMesh) {
if (
this.backgroundMesh.material !== this.backgroundColorMaterial &&
this.backgroundMesh.material instanceof THREE.Material
) {
this.backgroundMesh.material.dispose()
if (this.backgroundRenderMode === 'panorama') {
texture.mapping = THREE.EquirectangularReflectionMapping
this.scene.background = texture
} else {
if (!this.backgroundMesh) {
this.initBackgroundScene()
}
this.backgroundMesh.material = imageMaterial
this.backgroundMesh.position.set(0, 0, 0)
}
const imageMaterial = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
depthWrite: false,
depthTest: false,
side: THREE.DoubleSide
})
this.updateBackgroundSize(
this.backgroundTexture,
this.backgroundMesh,
this.renderer.domElement.clientWidth,
this.renderer.domElement.clientHeight
)
if (this.backgroundMesh) {
if (
this.backgroundMesh.material !== this.backgroundColorMaterial &&
this.backgroundMesh.material instanceof THREE.Material
) {
this.backgroundMesh.material.dispose()
}
this.backgroundMesh.material = imageMaterial
this.backgroundMesh.position.set(0, 0, 0)
}
this.updateBackgroundSize(
this.backgroundTexture,
this.backgroundMesh,
this.renderer.domElement.clientWidth,
this.renderer.domElement.clientHeight
)
}
this.eventManager.emitEvent('backgroundImageChange', uploadPath)
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
@@ -213,6 +234,35 @@ export class SceneManager implements SceneManagerInterface {
this.eventManager.emitEvent('backgroundImageLoadingEnd', null)
}
setBackgroundRenderMode(mode: BackgroundRenderModeType): void {
if (this.backgroundRenderMode === mode) return
this.backgroundRenderMode = mode
if (this.currentBackgroundType === 'image' && this.backgroundTexture) {
try {
if (mode === 'panorama') {
this.backgroundTexture.mapping =
THREE.EquirectangularReflectionMapping
this.scene.background = this.backgroundTexture
} else {
this.scene.background = null
if (
this.backgroundMesh &&
this.backgroundMesh.material instanceof THREE.MeshBasicMaterial
) {
this.backgroundMesh.material.map = this.backgroundTexture
this.backgroundMesh.material.needsUpdate = true
}
}
} catch (error) {
console.error('Error set background render mode:', error)
}
}
this.eventManager.emitEvent('backgroundRenderModeChange', mode)
}
updateBackgroundSize(
backgroundTexture: THREE.Texture | null,
backgroundMesh: THREE.Mesh | null,
@@ -254,7 +304,11 @@ export class SceneManager implements SceneManagerInterface {
}
renderBackground(): void {
if (this.backgroundMesh) {
if (
(this.backgroundRenderMode === 'tiled' ||
this.currentBackgroundType === 'color') &&
this.backgroundMesh
) {
const currentToneMapping = this.renderer.toneMapping
const currentExposure = this.renderer.toneMappingExposure

View File

@@ -13,6 +13,7 @@ import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
export type CameraType = 'perspective' | 'orthographic'
export type BackgroundRenderModeType = 'tiled' | 'panorama'
export interface CameraState {
position: THREE.Vector3
@@ -25,6 +26,7 @@ export interface SceneConfig {
showGrid: boolean
backgroundColor: string
backgroundImage?: string
backgroundRenderMode?: BackgroundRenderModeType
}
export interface ModelConfig {
@@ -77,6 +79,7 @@ export interface SceneManagerInterface extends BaseManager {
setBackgroundColor(color: string): void
setBackgroundImage(uploadPath: string): Promise<void>
removeBackgroundImage(): void
setBackgroundRenderMode(mode: BackgroundRenderModeType): void
handleResize(width: number, height: number): void
captureScene(width: number, height: number): Promise<CaptureResult>
}