부동산 가상 투어: WebGL 및 3D 웹 기술
통계는 스스로를 말해줍니다: 가상 투어를 제공하는 부동산은 조회수 87% 증가 정적인 사진만 있는 기관에 비해 이를 채택한 기관은 무단 물리적 방문을 줄입니다. 60% 증가하고 적격 리드는 40% 증가합니다. 그러나 대부분의 구현이 존재합니다. 비싸고 사용자 정의 가능성이 낮은 SaaS 솔루션에 의존합니다. 이 기사에서는 완전한 시스템을 구축하겠습니다. 부동산 가상 투어는 전적으로 브라우저 내에서 가능합니다. WebGL, Three.js 확장 가능하고 모든 목록 플랫폼과 통합될 수 있는 몰입형 경험을 만드는 최신 웹 기술입니다.
360도 파노라마 렌더링부터 방 간 탐색, 정보 핫스팟부터 측정까지 대화형: 프로덕션에 즉시 사용 가능한 TypeScript 코드로 모든 기술적인 측면을 다룰 것입니다.
무엇을 배울 것인가
- Three.js 및 React Three Fiber를 사용한 가상 투어를 위한 WebGL 뷰어 아키텍처
- 정방형 파노라마(360도 사진/비디오) 로드 및 최적화
- 원활한 전환과 대화형 핫스팟을 갖춘 멀티룸 탐색
- 레이 캐스팅 및 3D 형상을 사용한 대화형 측정
- 성능 최적화: LOD, 텍스처 스트리밍 및 지연 로딩
- 호환 장치에서 VR/AR용 WebXR과 통합
- 자동 360 이미지 처리를 위한 CI/CD 파이프라인
- 기존 목록 플랫폼과의 임베딩 및 API 통합
부동산을 위한 WebGL 및 Three.js의 기초
WebGL 고성능 2D 및 3D 렌더링을 직접 가능하게 하는 JavaScript API 플러그인 없이 브라우저에서 장치의 GPU를 활용합니다. Three.js 복잡성을 추상화 장면, 카메라, 조명, 재료 및 기하학을 관리하는 고급 API를 갖춘 WebGL. 투어용 가상 부동산의 기본 패턴과 구형 파노라마: 거꾸로 뒤집힌 거대한 구체 정방형 텍스처가 안쪽으로 투영되고 카메라가 중앙에 위치합니다.
권장 기술 스택
- Three.js 0.170+: 핵심 3D 렌더링
- 세 가지 섬유 반응: Three.js에 대한 선언적 React 바인딩
- 드라이: 도우미 및 사전 구축된 추상화(PointerLockControls, Html 등)
- GSAP: 부드러운 전환 애니메이션
- 패널룸: 순수한 파노라마를 위한 가벼운 대안
- 날카로운 (Node.js): 서버 측 360 이미지 처리
- FFmpeg: 360 영상 처리
시스템 아키텍처
프로덕션 가상 투어 시스템은 세 가지 수준으로 구성됩니다. 처리 파이프라인 서버측(변환, 최적화, 타일 생성) 전달 계층 (CDN, 스트리밍 적응형) 그리고 렌더링 엔진 클라이언트 측. 이러한 분리는 다음을 보장하는 데 필수적입니다. 모든 장치에서 최적의 성능을 발휘합니다.
// Struttura dati per un tour virtuale
interface VirtualTour {
id: string;
propertyId: string;
name: string;
rooms: TourRoom[];
startRoomId: string;
metadata: TourMetadata;
}
interface TourRoom {
id: string;
name: string; // "Soggiorno", "Camera principale"
panoramaUrl: string; // URL immagine equirettangolare (8192x4096px)
thumbnailUrl: string;
hotspots: Hotspot[];
floorPlanPosition: { x: number; y: number };
cameraDefaultYaw: number; // orientamento iniziale (gradi)
cameraDefaultPitch: number;
}
interface Hotspot {
id: string;
type: 'navigation' | 'info' | 'measurement' | 'media';
position: { yaw: number; pitch: number }; // coordinate sferiche
targetRoomId?: string; // per type='navigation'
label?: string;
description?: string;
mediaUrl?: string;
}
interface TourMetadata {
property: {
address: string;
squareMeters: number;
price: number;
currency: string;
};
capturedAt: Date;
camera: string;
floorPlanUrl?: string;
measurementsEnabled: boolean;
}
Three.js 뷰어 구현
시스템의 핵심은 WebGL 뷰어입니다. 하나 써보자 SphereGeometry 큰 반경으로,
반전된 기하학(내부 텍스처) e OrbitControls 시뮬레이션하도록 구성
파노라마 카메라의 동작(회전만 가능, 과도한 팬/줌 없음)
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
export class PanoramaViewer {
private scene: THREE.Scene;
private camera: THREE.PerspectiveCamera;
private renderer: THREE.WebGLRenderer;
private controls: OrbitControls;
private sphere: THREE.Mesh;
private hotspotGroup: THREE.Group;
private currentRoom: TourRoom | null = null;
private textureLoader: THREE.TextureLoader;
constructor(private container: HTMLElement) {
this.scene = new THREE.Scene();
// Camera con FOV tipico per panoramiche (75-90 gradi)
this.camera = new THREE.PerspectiveCamera(
75,
container.clientWidth / container.clientHeight,
0.1,
1000
);
this.camera.position.set(0, 0, 0.1);
// Renderer con antialiasing e supporto HDR
this.renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: false,
});
this.renderer.setSize(container.clientWidth, container.clientHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.outputColorSpace = THREE.SRGBColorSpace;
container.appendChild(this.renderer.domElement);
// Controlli orbitali configurati per panoramiche
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enablePan = false;
this.controls.enableZoom = true;
this.controls.zoomSpeed = 0.3;
this.controls.rotateSpeed = -0.3; // Inverso per comportamento naturale
this.controls.minDistance = 0;
this.controls.maxDistance = 0;
this.controls.minPolarAngle = Math.PI * 0.1;
this.controls.maxPolarAngle = Math.PI * 0.9;
// Sfera panoramica
const geometry = new THREE.SphereGeometry(500, 60, 40);
// Capovolgi la geometria per renderizzare verso l'interno
geometry.scale(-1, 1, 1);
const material = new THREE.MeshBasicMaterial({
side: THREE.BackSide, // Faccia interna visibile
});
this.sphere = new THREE.Mesh(geometry, material);
this.scene.add(this.sphere);
this.hotspotGroup = new THREE.Group();
this.scene.add(this.hotspotGroup);
this.textureLoader = new THREE.TextureLoader();
this.setupResizeObserver();
this.animate();
}
async loadRoom(room: TourRoom): Promise<void> {
this.currentRoom = room;
// Carica texture con progressive loading (prima bassa ris, poi alta)
const lowResTexture = await this.loadTexture(
room.panoramaUrl.replace('.jpg', '_low.jpg')
);
(this.sphere.material as THREE.MeshBasicMaterial).map = lowResTexture;
(this.sphere.material as THREE.MeshBasicMaterial).needsUpdate = true;
// Carica alta risoluzione in background
const highResTexture = await this.loadTexture(room.panoramaUrl);
(this.sphere.material as THREE.MeshBasicMaterial).map = highResTexture;
(this.sphere.material as THREE.MeshBasicMaterial).needsUpdate = true;
// Pulisci e ricrea hotspot
this.clearHotspots();
room.hotspots.forEach(hotspot => this.addHotspot(hotspot));
// Ruota la camera alla posizione di default della stanza
this.setInitialOrientation(room.cameraDefaultYaw, room.cameraDefaultPitch);
}
private loadTexture(url: string): Promise<THREE.Texture> {
return new Promise((resolve, reject) => {
this.textureLoader.load(url, resolve, undefined, reject);
});
}
private sphericalToCartesian(yaw: number, pitch: number, radius = 100): THREE.Vector3 {
const phi = (90 - pitch) * (Math.PI / 180);
const theta = yaw * (Math.PI / 180);
return new THREE.Vector3(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta)
);
}
private addHotspot(hotspot: Hotspot): void {
const position = this.sphericalToCartesian(
hotspot.position.yaw,
hotspot.position.pitch
);
// Sprite per hotspot (sempre orientato verso la camera)
const spriteMaterial = new THREE.SpriteMaterial({
map: this.getHotspotTexture(hotspot.type),
transparent: true,
});
const sprite = new THREE.Sprite(spriteMaterial);
sprite.position.copy(position);
sprite.scale.set(8, 8, 1);
sprite.userData = { hotspot };
this.hotspotGroup.add(sprite);
}
private animate = (): void => {
requestAnimationFrame(this.animate);
this.controls.update();
this.renderer.render(this.scene, this.camera);
};
private setupResizeObserver(): void {
const observer = new ResizeObserver(() => {
const w = this.container.clientWidth;
const h = this.container.clientHeight;
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
this.renderer.setSize(w, h);
});
observer.observe(this.container);
}
dispose(): void {
this.renderer.dispose();
this.controls.dispose();
}
}
전환 기능이 있는 멀티룸 내비게이션 시스템
룸 간 탐색은 부드럽고 직관적이어야 합니다. 페이드아웃/페이드인 전환을 구현해 보겠습니다. 인지된 대기 시간을 없애기 위해 애니메이션을 적용하고 배경에서 다음 방을 미리 로드합니다.
export class TourNavigator {
private viewer: PanoramaViewer;
private tour: VirtualTour;
private roomCache: Map<string, THREE.Texture> = new Map();
private isTransitioning = false;
constructor(viewer: PanoramaViewer, tour: VirtualTour) {
this.viewer = viewer;
this.tour = tour;
// Preloading preventivo delle stanze adiacenti
this.preloadAdjacentRooms(tour.startRoomId);
}
async navigateToRoom(
roomId: string,
transition: 'fade' | 'blur' | 'zoom' = 'fade'
): Promise<void> {
if (this.isTransitioning) return;
this.isTransitioning = true;
const room = this.tour.rooms.find(r => r.id === roomId);
if (!room) {
this.isTransitioning = false;
return;
}
// Fase 1: fade out
await this.animateOverlay(0, 1, 300, transition);
// Fase 2: carica nuova stanza (probabilmente già in cache)
await this.viewer.loadRoom(room);
// Fase 3: preloading stanze adiacenti in background
this.preloadAdjacentRooms(roomId);
// Fase 4: fade in
await this.animateOverlay(1, 0, 300, transition);
this.isTransitioning = false;
// Analytics: traccia navigazione
this.trackRoomVisit(roomId);
}
private async preloadAdjacentRooms(currentRoomId: string): Promise<void> {
const currentRoom = this.tour.rooms.find(r => r.id === currentRoomId);
if (!currentRoom) return;
const navigationHotspots = currentRoom.hotspots
.filter(h => h.type === 'navigation' && h.targetRoomId)
.map(h => h.targetRoomId!);
// Preload in parallelo con bassa priorità
for (const roomId of navigationHotspots) {
if (!this.roomCache.has(roomId)) {
const room = this.tour.rooms.find(r => r.id === roomId);
if (room) {
// Usa Intersection Observer API per priorità basata sulla visibilità
requestIdleCallback(() => {
this.preloadRoomTexture(room);
});
}
}
}
}
private animateOverlay(
fromOpacity: number,
toOpacity: number,
durationMs: number,
effect: string
): Promise<void> {
return new Promise(resolve => {
const overlay = document.getElementById('tour-overlay')!;
overlay.style.transition = `opacity ${durationMs}ms ease-in-out`;
overlay.style.opacity = String(toOpacity);
setTimeout(resolve, durationMs);
});
}
private trackRoomVisit(roomId: string): void {
// Integrazione con Google Analytics 4
if (typeof gtag !== 'undefined') {
gtag('event', 'virtual_tour_room_visit', {
tour_id: this.tour.id,
room_id: roomId,
property_id: this.tour.propertyId,
});
}
}
}
대화형 핫스팟 및 측정
핫스팟은 경험의 기본 요소입니다. 이를 통해 방 사이를 탐색하고 쇼를 표시할 수 있습니다. 상황에 맞는 정보를 제공하고 고급 구현에서는 공간 측정을 수행합니다. 파노라마 구에서 정확한 클릭 감지를 위해 레이 캐스팅 시스템을 구현합니다.
// Sistema di interazione con hotspot via raycasting
export class HotspotInteractionManager {
private raycaster = new THREE.Raycaster();
private mouse = new THREE.Vector2();
private onHotspotClick: (hotspot: Hotspot) => void;
constructor(
private camera: THREE.PerspectiveCamera,
private hotspotGroup: THREE.Group,
private canvas: HTMLCanvasElement,
onHotspotClick: (hotspot: Hotspot) => void
) {
this.onHotspotClick = onHotspotClick;
this.setupEventListeners();
}
private setupEventListeners(): void {
// Supporto touch e mouse
this.canvas.addEventListener('click', this.handleClick);
this.canvas.addEventListener('touchend', this.handleTouch);
}
private handleClick = (event: MouseEvent): void => {
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
this.checkIntersection();
};
private handleTouch = (event: TouchEvent): void => {
if (event.changedTouches.length === 0) return;
const touch = event.changedTouches[0];
const rect = this.canvas.getBoundingClientRect();
this.mouse.x = ((touch.clientX - rect.left) / rect.width) * 2 - 1;
this.mouse.y = -((touch.clientY - rect.top) / rect.height) * 2 + 1;
this.checkIntersection();
};
private checkIntersection(): void {
this.raycaster.setFromCamera(this.mouse, this.camera);
const intersects = this.raycaster.intersectObjects(
this.hotspotGroup.children,
true
);
if (intersects.length > 0) {
const hotspotData = intersects[0].object.userData['hotspot'] as Hotspot;
if (hotspotData) {
this.onHotspotClick(hotspotData);
}
}
}
}
// Misurazione interattiva: calcolo distanza tra due punti sulla sfera
export class MeasurementTool {
private measurePoints: THREE.Vector3[] = [];
private measureLines: THREE.Line[] = [];
addMeasurePoint(yaw: number, pitch: number, realWorldRadius: number): void {
const point = this.sphericalToCartesian(yaw, pitch, 1);
this.measurePoints.push(point);
if (this.measurePoints.length === 2) {
const [p1, p2] = this.measurePoints;
// Angolo tra i due vettori in radianti
const angle = p1.angleTo(p2);
// Distanza approssimativa in metri (richiede calibrazione con dati reali)
const estimatedDistance = angle * realWorldRadius * 2;
console.log(`Distanza stimata: ${estimatedDistance.toFixed(2)} m`);
this.measurePoints = [];
}
}
private sphericalToCartesian(yaw: number, pitch: number, radius: number): THREE.Vector3 {
const phi = (90 - pitch) * (Math.PI / 180);
const theta = yaw * (Math.PI / 180);
return new THREE.Vector3(
radius * Math.sin(phi) * Math.cos(theta),
radius * Math.cos(phi),
radius * Math.sin(phi) * Math.sin(theta)
);
}
}
서버측 이미지 처리 파이프라인
360도 이미지의 품질은 경험의 기본입니다. Node.js 파이프라인을 구현해 보겠습니다. 다음과 같은 카메라의 원시 이미지를 자동으로 최적화합니다. 리코 세타 Z1 또는 인스타360 X4, 적응형 스트리밍을 위한 여러 해상도를 생성합니다.
import sharp from 'sharp';
import path from 'path';
import fs from 'fs/promises';
interface ProcessingConfig {
inputPath: string;
outputDir: string;
qualities: Array<{ width: number; suffix: string; quality: number }>;
}
export async function processPanoramaImage(config: ProcessingConfig): Promise<void> {
const { inputPath, outputDir, qualities } = config;
await fs.mkdir(outputDir, { recursive: true });
const baseName = path.basename(inputPath, path.extname(inputPath));
// Genera versioni multiple per progressive loading
for (const quality of qualities) {
const outputPath = path.join(outputDir, `${baseName}_${quality.suffix}.webp`);
await sharp(inputPath)
.resize(quality.width, quality.width / 2, {
fit: 'fill',
kernel: 'lanczos3',
})
.webp({
quality: quality.quality,
effort: 6, // Bilanciamento velocità/compressione
lossless: false,
})
.toFile(outputPath);
console.log(`Generated: ${outputPath} (${quality.width}x${quality.width / 2})`);
}
// Genera thumbnail per la mappa del piano
await sharp(inputPath)
.resize(400, 200, { fit: 'fill' })
.webp({ quality: 70 })
.toFile(path.join(outputDir, `${baseName}_thumb.webp`));
}
// Configurazione per produzione
const productionConfig: ProcessingConfig = {
inputPath: './raw/living-room.jpg',
outputDir: './processed/living-room',
qualities: [
{ width: 1024, suffix: 'low', quality: 65 }, // ~200KB - caricamento iniziale
{ width: 4096, suffix: 'med', quality: 80 }, // ~1.5MB - qualità standard
{ width: 8192, suffix: 'high', quality: 90 }, // ~5MB - massima qualità
],
};
VR/AR을 위한 WebXR 통합
통합 WebXR API 호환되는 헤드셋(Meta Quest, 브라우저, HTC Vive를 통해 Apple Vision Pro를 사용하면 해당 속성에 완전히 몰입할 수 있습니다. 추가와 기본 렌더러 지원 덕분에 Three.js를 사용하면 놀라울 정도로 간단합니다.
// Abilitazione WebXR nel renderer Three.js
export function enableWebXR(
renderer: THREE.WebGLRenderer,
container: HTMLElement
): void {
renderer.xr.enabled = true;
// Verifica supporto WebXR nel browser
if ('xr' in navigator) {
navigator.xr?.isSessionSupported('immersive-vr').then(supported => {
if (supported) {
// Mostra pulsante "Entra in VR"
const vrButton = createVRButton(renderer);
container.appendChild(vrButton);
}
});
}
}
function createVRButton(renderer: THREE.WebGLRenderer): HTMLButtonElement {
const button = document.createElement('button');
button.textContent = 'Visita in VR';
button.style.cssText = `
position: absolute;
bottom: 20px;
right: 20px;
padding: 12px 24px;
background: #0066cc;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
`;
button.addEventListener('click', async () => {
try {
const session = await navigator.xr!.requestSession('immersive-vr', {
optionalFeatures: ['local-floor', 'bounded-floor'],
});
await renderer.xr.setSession(session);
button.textContent = 'Uscita VR';
} catch (err) {
console.error('Errore avvio sessione VR:', err);
}
});
return button;
}
// Nel loop di animazione, gestisci sia VR che non-VR
function animate(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.PerspectiveCamera): void {
renderer.setAnimationLoop(() => {
// In VR: il renderer gestisce automaticamente i due occhi
// Fuori VR: rendering standard
renderer.render(scene, camera);
});
}
API 내장 및 상장 플랫폼과의 통합
채택을 극대화하려면 뷰어를 모든 플랫폼에 쉽게 통합해야 합니다. 간단한 HTML 태그나 iframe을 통해 존재합니다. Custom Elements API를 사용하여 표준 웹 구성 요소를 만들어 보겠습니다.
// Web Component per embedding universale
class RealEstateTourElement extends HTMLElement {
private viewer: PanoramaViewer | null = null;
static get observedAttributes(): string[] {
return ['tour-id', 'api-key', 'start-room'];
}
connectedCallback(): void {
this.render();
this.initTour();
}
disconnectedCallback(): void {
this.viewer?.dispose();
}
private render(): void {
this.innerHTML = `
<div style="width:100%;height:100%;position:relative;">
<div id="tour-container" style="width:100%;height:100%;"></div>
<div id="tour-overlay" style="position:absolute;top:0;left:0;width:100%;height:100%;
background:black;opacity:0;pointer-events:none;"></div>
<div id="tour-controls" style="position:absolute;bottom:16px;left:16px;"></div>
</div>
`;
}
private async initTour(): Promise<void> {
const tourId = this.getAttribute('tour-id');
const apiKey = this.getAttribute('api-key');
if (!tourId || !apiKey) return;
const response = await fetch(
`https://api.realestate-tours.com/v1/tours/${tourId}`,
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
);
const tour: VirtualTour = await response.json();
const container = this.querySelector('#tour-container') as HTMLElement;
this.viewer = new PanoramaViewer(container);
const startRoom = tour.rooms.find(r => r.id === tour.startRoomId)!;
await this.viewer.loadRoom(startRoom);
}
}
// Registrazione come Custom Element
customElements.define('realestate-tour', RealEstateTourElement);
// Utilizzo in qualsiasi HTML:
// <realestate-tour tour-id="prop-12345" api-key="your-key" style="height:500px"></realestate-tour>
성능 최적화
성능이 중요합니다. 로드하는 데 3초 이상 걸리는 투어는 53%의 사용자를 잃습니다. (구글에서 제공). 기본적인 전략은 다음과 같습니다.
최적화 전략
- 적응형 스트리밍 텍스처: 먼저 저해상도(200KB)를 로드한 다음 고해상도(5MB)로 교체하세요.
- 스마트 사전 로딩: 유휴 시간 동안 현재 방에서 접근할 수 있는 방을 미리 로드합니다.
- 대체 기능이 있는 WebP: WebP는 동일한 품질의 JPEG에 비해 30-35% 감소합니다.
- 에지 캐싱이 포함된 CDN: 이미지를 지리적으로 배포(Cloudflare 이미지, AWS CloudFront)
- LOD(세부 수준): devicePixelRatio를 기반으로 하는 모바일 장치의 낮은 해상도
- GPU 인스턴스화: 여러 핫스팟의 경우 별도의 Sprite 대신 InstancedMesh를 사용하세요.
- requestIdle콜백: 브라우저가 유휴 상태일 때만 사전 로드를 수행합니다.
// Selezione risoluzione adattiva basata su dispositivo
function getOptimalResolution(
devicePixelRatio: number,
connectionType: string
): 'low' | 'med' | 'high' {
// Navigator Connection API (dove disponibile)
const connection = (navigator as any).connection;
const effectiveType = connection?.effectiveType ?? '4g';
if (effectiveType === '2g' || effectiveType === 'slow-2g') {
return 'low';
}
if (devicePixelRatio <= 1 || effectiveType === '3g') {
return 'med';
}
return 'high';
}
// Utilizzo
const resolution = getOptimalResolution(
window.devicePixelRatio,
''
);
const panoramaUrl = room.panoramaUrl.replace('.webp', `_${resolution}.webp`);
비즈니스 지표 및 분석
가상여행의 실제 가치는 구체적인 데이터를 통해 측정됩니다. 우리는 분석 시스템을 통합합니다 부동산 판매 팀의 주요 지표를 추적하는 투어별 서비스입니다.
| 미터법 | 정의 | 벤치마크 |
|---|---|---|
| 투어 완료율 | 최소 3개 이상의 방을 방문한 사용자 % | >45% |
| 평균 세션 시간 | 가상 방문의 평균 지속 시간 | 4~8분 |
| 핫스팟 참여율 | 세션당 클릭된 핫스팟 % | >30% |
| 리드 전환 | 연락으로 이어지는 세션 % | 8-15% |
| 물리적 방문 감소 | 무자격 실제 방문 감소 | 50-60% |
경고: 저작권 이미지 360
파노라마 이미지에는 저작권이 적용됩니다. 계약서가 있는지 확인하세요. 자료를 제작하는 사진작가/대행사에게 명확하게 알려주세요. 이용약관에 포함됩니다. 이미지 다운로드 및 재배포에 대한 플랫폼 명시적 제한. 기술적으로는 저해상도 워터마크를 적용하고 WebGL 캔버스에서 마우스 오른쪽 버튼 클릭을 비활성화합니다.
결론 및 다음 단계
우리는 표준 웹 기술인 WebGL/Three.js를 기반으로 완전한 가상 투어 시스템을 구축했습니다. 렌더링용, 범용 임베딩용 Custom Elements, 최적화용 Sharp 파이프라인 이미지. 이 접근 방식은 값비싼 SaaS 공급업체에 대한 의존성을 제거하고 사용자 경험의 개인화.
자연스러운 다음 단계는 생성된 사실적인 3D 모델과의 통합입니다. 가우스 스플래팅 o NeRF (신경 복사 장), 이는 간단한 것부터 완전한 3D 재구성으로 2025~2026년 부동산 시각화에 혁명을 일으킬 것입니다. 스마트폰으로 영상.







