Codrops 원본

Relighting Images with Depth Maps and Three.js

섹션 · MIT

섹션 더 보기
ORIGINAL PREVIEW
Relighting Images with Depth Maps and Three.js 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

20개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

demo2.html
파일 저장

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Relightning Images — demo 2</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/src/style.css">
  </head>
  <body>
    <div id="app"></div>
    <aside id="info">
      <h1>Relightning Images by Dominik Fojcik</h1>
      <nav class="project-links" aria-label="Project links">
        <span>Article</span>
        <a href="https://github.com/DGFX/codrops-relightning-images">Code</a>
        <a href="https://tympanus.net/codrops/demos/">All Demos</a>
      </nav>
      <nav class="tags" aria-label="Tags">
        <a href="https://tympanus.net/codrops/demos/?tag=3d">#3d</a>
        <a href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
      </nav>
    </aside>
    <nav id="nav" aria-label="Relightning demos">
      <a href="/">demo 1</a>
      <a href="/demo2.html" class="active">demo 2</a>
      <a href="/demo3.html">demo 3</a>
    </nav>
    <script type="module" src="/src/app.js"></script>
  </body>
</html>
demo3.html
파일 저장

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Relightning Images — demo 3</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/src/style.css">
  </head>
  <body>
    <div id="app"></div>
    <aside id="info">
      <h1>Relightning Images by Dominik Fojcik</h1>
      <nav class="project-links" aria-label="Project links">
        <span>Article</span>
        <a href="https://github.com/DGFX/codrops-relightning-images">Code</a>
        <a href="https://tympanus.net/codrops/demos/">All Demos</a>
      </nav>
      <nav class="tags" aria-label="Tags">
        <a href="https://tympanus.net/codrops/demos/?tag=3d">#3d</a>
        <a href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
      </nav>
    </aside>
    <nav id="nav" aria-label="Relightning demos">
      <a href="/">demo 1</a>
      <a href="/demo2.html">demo 2</a>
      <a href="/demo3.html" class="active">demo 3</a>
    </nav>
    <script type="module" src="/src/app.js"></script>
  </body>
</html>
함께 쓰는 파일 18개 보기
index.html
파일 저장

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Relightning Images</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="/src/style.css">
  </head>
  <body>
    <div id="app"></div>
    <aside id="info">
      <h1>Relightning Images by Dominik Fojcik</h1>
      <nav class="project-links" aria-label="Project links">
        <span>Article</span>
        <a href="https://github.com/DGFX/codrops-relightning-images">Code</a>
        <a href="https://tympanus.net/codrops/demos/">All Demos</a>
      </nav>
      <nav class="tags" aria-label="Tags">
        <a href="https://tympanus.net/codrops/demos/?tag=3d">#3d</a>
        <a href="https://tympanus.net/codrops/demos/?tag=three-js">#three.js</a>
      </nav>
    </aside>
    <nav id="nav" aria-label="Relightning demos">
      <a href="/" class="active">demo 1</a>
      <a href="/demo2.html">demo 2</a>
      <a href="/demo3.html">demo 3</a>
    </nav>
    <script type="module" src="/src/app.js"></script>
  </body>
</html>
src/app.js
파일 저장

import {
  AgXToneMapping,
  Color,
  OrthographicCamera,
  Scene,
  WebGPURenderer,
} from 'three/webgpu'
import { inspector } from './debug.js'
import { setupDemo } from './demos.js'
import { setupLight } from './effect/light.js'
import { createPlane } from './effect/plane.js'
import { setupTextures } from './effect/textures.js'

const MAX_PIXEL_RATIO = 2
const VIEW_HEIGHT = 4

const demo = setupDemo()

const renderer = new WebGPURenderer({ antialias: true })
renderer.setPixelRatio(Math.min(devicePixelRatio, MAX_PIXEL_RATIO))
renderer.setSize(innerWidth, innerHeight)
renderer.toneMapping = AgXToneMapping
renderer.inspector = inspector

document.getElementById('app').append(renderer.domElement)
await renderer.init()

await setupTextures(demo)

const scene = new Scene()
scene.background = new Color('#000000')

const camera = new OrthographicCamera()
camera.position.set(0, 0, 5)

setupLight(scene, camera)

const plane = createPlane()
scene.add(plane)

function onResize() {
  const aspect = innerWidth / innerHeight

  camera.top = VIEW_HEIGHT * 0.5
  camera.bottom = -camera.top
  camera.right = camera.top * aspect
  camera.left = -camera.right
  camera.updateProjectionMatrix()
  plane.scale.set(VIEW_HEIGHT * aspect, VIEW_HEIGHT, 1)
}

onResize()

addEventListener('resize', () => {
  renderer.setSize(innerWidth, innerHeight)
  onResize()
})

renderer.setAnimationLoop(() => renderer.render(scene, camera))
src/debug.js
파일 저장

import { Inspector } from 'three/addons/inspector/Inspector.js'
import { int, output, select, uniform, vec3, vec4 } from 'three/tsl'

export const inspector = new Inspector()
export const gui = inspector.createParameters('Light plane')

const uDebugView = uniform(0, 'int')

const DEBUG_VIEWS = [
  { label: 'depth', node: (_material, depth) => vec3(depth) },
  {
    label: 'normal',
    node: (material) => material.normalNode.mul(0.5).add(0.5),
  },
  { label: 'diffuse', node: (material) => material.colorNode },
  { label: 'shadow', node: (material) => vec3(material.aoNode) },
]

export function setDebugView(material, depth) {
  material.outputNode = vec4(
    DEBUG_VIEWS.reduceRight(
      (fallback, { node }, index) =>
        select(
          uDebugView.equal(int(index + 1)),
          node(material, depth),
          fallback,
        ),
      output.rgb,
    ),
    output.a,
  )

  const debugView = gui.addFolder('Debug View').close()
  debugView
    .add(
      uDebugView,
      'value',
      Object.fromEntries([
        ['final', 0],
        ...DEBUG_VIEWS.map(({ label }, index) => [label, index + 1]),
      ]),
    )
    .name('output')
}
src/demos.js
파일 저장

import { depthSmoothing } from './effect/depth-map.js'
import { ambientLight, pointLight } from './effect/light.js'
import {
  uDetailScale,
  uDisplacementScale,
  uNormalScale,
} from './effect/nodes/normal.js'
import { uShadowIntensity, uShadowSoftness } from './effect/nodes/shadow.js'

const DEMOS = {
  '/demo2': {
    map: '/textures/relief.jpg',
    depth: '/textures/relief-depth.webp',
    setUniforms() {
      depthSmoothing.percent = 0.6
      uDisplacementScale.value = 0.905
      uNormalScale.value = 0.86
      uDetailScale.value = 0.83
      uShadowIntensity.value = 0.55
      uShadowSoftness.value = 0.164
      pointLight.color.set('#ffcb8f')
      pointLight.decay = 2.95
      pointLight.position.z = 0.77
      ambientLight.intensity = 0
    },
  },
  '/demo3': {
    map: '/textures/mother.jpg',
    depth: '/textures/mother-depth.webp',
    setUniforms() {
      depthSmoothing.percent = 0.5
      uDisplacementScale.value = 1.9
      uNormalScale.value = 2.16
      uDetailScale.value = 0.7
      uShadowSoftness.value = 0.152
      pointLight.color.set('#e7dcd0')
      pointLight.intensity = 10.1
      pointLight.decay = 3.8
      pointLight.position.z = 1.97
      ambientLight.intensity = 0.02
    },
  },
}

const DEFAULT_DEMO = {
  map: '/textures/wall.jpg',
  depth: '/textures/wall-depth.webp',
}

export function setupDemo() {
  const pathname = location.pathname.replace(/\.html$/, '')
  const demo = DEMOS[pathname] ?? DEFAULT_DEMO
  demo.setUniforms?.()
  return demo
}
src/effect/depth-map.js
파일 저장

import { texture } from 'three/tsl'
import {
  DataTexture,
  DataUtils,
  HalfFloatType,
  LinearFilter,
  RedFormat,
} from 'three/webgpu'
import { gui } from '../debug.js'
import { smoothBands } from '../lib/blur.js'

export const depthSmoothing = { percent: 1.3 }

const BAKE_DELAY_MS = 150
const WORKING_WIDTH = 1024

const smoothDepthMap = new DataTexture(
  new Uint16Array([DataUtils.toHalfFloat(0.5)]),
  1,
  1,
  RedFormat,
  HalfFloatType,
)
smoothDepthMap.minFilter = LinearFilter
smoothDepthMap.magFilter = LinearFilter
smoothDepthMap.needsUpdate = true

export const smoothDepthNode = texture(smoothDepthMap)

const canvas = document.createElement('canvas')
const context = canvas.getContext('2d', { willReadFrequently: true })

let depthImage = null

export function setDepthImage(image) {
  depthImage = image
  if (!depthImage) return

  const scale = Math.min(1, WORKING_WIDTH / depthImage.width)
  const width = Math.max(1, Math.round(depthImage.width * scale))
  const height = Math.max(1, Math.round(depthImage.height * scale))

  canvas.width = width
  canvas.height = height
  context.setTransform(1, 0, 0, -1, 0, height)
  context.drawImage(depthImage, 0, 0, width, height)

  const { data } = context.getImageData(0, 0, width, height)
  const values = new Float32Array(width * height)
  for (let i = 0; i < values.length; i++) {
    values[i] = data[i * 4] / 255
  }

  smoothBands(
    { values, width, height },
    Math.round((depthSmoothing.percent / 100) * width),
  )

  const halfFloats = new Uint16Array(values.length)
  for (let i = 0; i < halfFloats.length; i++) {
    halfFloats[i] = DataUtils.toHalfFloat(values[i])
  }

  smoothDepthMap.dispose()
  smoothDepthMap.image = { data: halfFloats, width, height }
  smoothDepthMap.needsUpdate = true
}

let bakeTimer = 0

export const depthFolder = gui.addFolder('Depth').close()
depthFolder
  .add(depthSmoothing, 'percent', 0.1, 12, 0.1)
  .name('smooth depth')
  .addEventListener('change', () => {
    clearTimeout(bakeTimer)
    bakeTimer = setTimeout(() => setDepthImage(depthImage), BAKE_DELAY_MS)
  })
src/effect/light.js
파일 저장

import { uniform } from 'three/tsl'
import { AmbientLight, PointLight } from 'three/webgpu'
import { gui } from '../debug.js'

const MAX_DECAY = 4

export const pointLight = new PointLight('#e1ded1', 2.35, 0, 1)
pointLight.position.set(1.2, 0.8, 0.9)
Object.defineProperty(pointLight.userData, 'radius', {
  get: () => MAX_DECAY - pointLight.decay,
  set: (radius) => {
    pointLight.decay = MAX_DECAY - radius
  },
})

export const ambientLight = new AmbientLight('#ffffff', 0.3)

export const uLightPosition = uniform(pointLight.position)

export function setupLight(scene, camera) {
  scene.add(pointLight, ambientLight)

  addEventListener('pointermove', (event) => {
    const ndcX = (event.clientX / innerWidth) * 2 - 1
    const ndcY = -((event.clientY / innerHeight) * 2 - 1)
    pointLight.position.x = ndcX * camera.right
    pointLight.position.y = ndcY * camera.top
  })

  const light = gui.addFolder('Light').close()
  light.addColor(pointLight, 'color').name('color')
  light.add(pointLight, 'intensity', 0, 12, 0.05).name('brightness')
  light.add(pointLight.userData, 'radius', 0, MAX_DECAY, 0.05).name('radius')
  light.add(pointLight.position, 'z', -0.5, 4, 0.01).name('elevation')
  light.add(ambientLight, 'intensity', 0, 4, 0.05).name('ambient')
}
src/effect/nodes/diffuse.js
파일 저장

import { Fn, step, uniform } from 'three/tsl'
import { depthFolder } from '../depth-map.js'
import { mapNode } from '../textures.js'

const uDepthThreshold = uniform(0)

export const diffuseNode = Fn(([vUv, depth]) =>
  mapNode.sample(vUv).rgb.mul(step(uDepthThreshold, depth)),
)

depthFolder.add(uDepthThreshold, 'value', 0, 1, 0.01).name('depth threshold')
src/effect/nodes/normal.js
파일 저장

import {
  Fn,
  float,
  luminance,
  modelScale,
  uniform,
  vec2,
  vec3,
} from 'three/tsl'
import { gui } from '../../debug.js'
import { smoothDepthNode } from '../depth-map.js'
import { mapNode } from '../textures.js'
import { coverScaleNode } from './texture-fit.js'

export const uDisplacementScale = uniform(4)
export const uNormalScale = uniform(3)
export const uDetailScale = uniform(3)

const DETAIL_GAIN = 4
const DETAIL_LOD = 3
const DETAIL_STEP_TEXELS = 8
const GRADIENT_TEXELS = 3

const depthGradient = Fn(([vUv, step]) => {
  const alongX = vec2(step.x, 0)
  const alongY = vec2(0, step.y)

  const left = smoothDepthNode.sample(vUv.sub(alongX)).r
  const right = smoothDepthNode.sample(vUv.add(alongX)).r
  const bottom = smoothDepthNode.sample(vUv.sub(alongY)).r
  const top = smoothDepthNode.sample(vUv.add(alongY)).r

  return vec2(right.sub(left), top.sub(bottom)).mul(0.5)
})

const detailGradient = Fn(([vUv, step]) => {
  const alongX = vec2(step.x, 0)
  const alongY = vec2(0, step.y)

  const map = (uvNode) => mapNode.sample(uvNode).level(DETAIL_LOD).rgb

  const left = luminance(map(vUv.sub(alongX)))
  const right = luminance(map(vUv.add(alongX)))
  const bottom = luminance(map(vUv.sub(alongY)))
  const top = luminance(map(vUv.add(alongY)))

  return vec2(right.sub(left), top.sub(bottom)).mul(0.5)
})

export const normalNode = Fn(([vUv]) => {
  const step = vec2(GRADIENT_TEXELS).div(vec2(smoothDepthNode.size()))

  const slope = depthGradient(vUv, step)
    .mul(coverScaleNode())
    .div(step.mul(modelScale.xy))
    .mul(uDisplacementScale)
    .mul(uNormalScale)

  const detail = detailGradient(
    vUv,
    vec2(DETAIL_STEP_TEXELS).div(vec2(mapNode.size())),
  )
    .mul(uDetailScale)
    .mul(DETAIL_GAIN)

  const shape = vec3(slope.x.negate(), slope.y.negate(), float(1))

  return shape.add(vec3(detail.x.negate(), detail.y.negate(), 0)).normalize()
})

const normal = gui.addFolder('Normal').close()
normal.add(uDisplacementScale, 'value', 0, 4, 0.005).name('displacement scale')
normal.add(uNormalScale, 'value', 0, 3, 0.01).name('normal scale')
normal.add(uDetailScale, 'value', 0, 10, 0.01).name('normal details')
src/effect/nodes/shadow.js
파일 저장

import {
  Fn,
  Loop,
  float,
  modelScale,
  positionWorld,
  uniform,
  vec3,
} from 'three/tsl'
import { gui } from '../../debug.js'
import { smoothDepthNode } from '../depth-map.js'
import { uLightPosition } from '../light.js'
import { uDisplacementScale } from './normal.js'
import { coverScaleNode } from './texture-fit.js'

export const uShadowIntensity = uniform(0.86)
export const uShadowSoftness = uniform(0.092)

const SHADOW_STEPS = 12
const MIN_LIGHT_ANGLE = 0.15
const SOFTNESS_GROWTH = 3

export const shadowNode = Fn(([vUv, depth]) => {
  const surfacePosition = vec3(
    positionWorld.xy,
    depth.sub(1).mul(uDisplacementScale),
  )
  const surfaceToLight = uLightPosition.sub(surfacePosition)
  const lightDirection = surfaceToLight.div(surfaceToLight.length().max(0.001))
  const surfaceDepth = smoothDepthNode.sample(vUv).r
  const remainingDepth = surfaceDepth.oneMinus()

  const rayOffset = lightDirection.xy
    .div(lightDirection.z.max(MIN_LIGHT_ANGLE))
    .mul(uDisplacementScale)
    .mul(coverScaleNode())
    .div(modelScale.xy)
    .mul(remainingDepth)

  const maxOcclusion = float(0).toVar()

  Loop(SHADOW_STEPS, ({ i }) => {
    const rayProgress = float(i).add(1).div(SHADOW_STEPS)
    const rayDepth = surfaceDepth.add(remainingDepth.mul(rayProgress))
    const blockerDepth = smoothDepthNode.sample(
      vUv.add(rayOffset.mul(rayProgress)),
    ).r
    const sampleSoftness = uShadowSoftness.mul(
      rayProgress.mul(SOFTNESS_GROWTH).add(1),
    )
    const sampleOcclusion = blockerDepth
      .sub(rayDepth)
      .div(sampleSoftness)
      .clamp(0, 1)

    maxOcclusion.assign(maxOcclusion.max(sampleOcclusion))
  })

  const lightFacingMask = lightDirection.z.greaterThan(0).select(1, 0)

  return maxOcclusion.mul(uShadowIntensity).mul(lightFacingMask).oneMinus()
})

const shadow = gui.addFolder('Shadow').close()
shadow.add(uShadowIntensity, 'value', 0, 1, 0.01).name('shadow intensity')
shadow.add(uShadowSoftness, 'value', 0.002, 0.2, 0.002).name('shadow softness')
src/effect/nodes/texture-fit.js
파일 저장

import { Fn, screenSize, screenUV, vec2 } from 'three/tsl'
import { mapNode } from '../textures.js'

export const coverScaleNode = Fn(() => {
  const viewAspect = screenSize.x.div(screenSize.y).toVar()
  const mapSize = vec2(mapNode.size()).toVar()
  const imageAspect = mapSize.x.div(mapSize.y).toVar()

  return imageAspect
    .greaterThan(viewAspect)
    .select(
      vec2(viewAspect.div(imageAspect), 1),
      vec2(1, imageAspect.div(viewAspect)),
    )
})

export const coverUv = Fn(() =>
  screenUV.flipY().sub(0.5).mul(coverScaleNode()).add(0.5),
)
src/effect/plane.js
파일 저장

import {
  cameraFar,
  cameraNear,
  positionView,
  viewZToOrthographicDepth,
} from 'three/tsl'
import { Mesh, MeshPhongNodeMaterial, PlaneGeometry } from 'three/webgpu'
import { setDebugView } from '../debug.js'
import { diffuseNode } from './nodes/diffuse.js'
import { normalNode, uDisplacementScale } from './nodes/normal.js'
import { shadowNode } from './nodes/shadow.js'
import { coverUv } from './nodes/texture-fit.js'
import { depthNode } from './textures.js'

export function createPlane() {
  const vUv = coverUv()
  const depth = depthNode.sample(vUv).r

  const material = new MeshPhongNodeMaterial({ specular: 0x000000 })
  material.colorNode = diffuseNode(vUv, depth)
  material.normalNode = normalNode(vUv)
  material.aoNode = shadowNode(vUv, depth)
  material.depthNode = viewZToOrthographicDepth(
    positionView.z.add(depth.sub(1).mul(uDisplacementScale)),
    cameraNear,
    cameraFar,
  )

  setDebugView(material, depth)

  return new Mesh(new PlaneGeometry(1, 1), material)
}
src/effect/textures.js
파일 저장

import { texture } from 'three/tsl'
import { SRGBColorSpace, Texture, TextureLoader } from 'three/webgpu'
import { setDepthImage } from './depth-map.js'

export const mapNode = texture(new Texture())
export const depthNode = texture(new Texture())

const loader = new TextureLoader()

async function setMap(url) {
  const map = await loader.loadAsync(url)
  map.colorSpace = SRGBColorSpace
  mapNode.value.dispose()
  mapNode.value = map
}

async function setDepth(url) {
  const map = await loader.loadAsync(url)
  depthNode.value.dispose()
  depthNode.value = map
  setDepthImage(map.image)
}

export function setupTextures({ map, depth }) {
  return Promise.all([setMap(map), setDepth(depth)])
}
src/lib/blur.js
파일 저장

const TOLERANCE = 1.5 / 255
const SMOOTH_RADIUS = 2

function blurAxis(source, target, { width, height, radius }) {
  const span = radius * 2 + 1
  for (let row = 0; row < height; row++) {
    const offset = row * width
    let sum = source[offset] * radius
    for (let column = 0; column <= radius; column++) {
      sum += source[offset + Math.min(column, width - 1)]
    }
    for (let column = 0; column < width; column++) {
      target[column * height + row] = sum / span
      sum -= source[offset + Math.max(column - radius, 0)]
      sum += source[offset + Math.min(column + radius + 1, width - 1)]
    }
  }
}

function boxBlur({ values, width, height }, radius) {
  if (radius < 1) return
  const transposed = new Float32Array(values.length)
  blurAxis(values, transposed, { width, height, radius })
  blurAxis(transposed, values, { width: height, height: width, radius })
}

export function smoothBands(field, radius) {
  const original = field.values.slice()
  boxBlur(field, radius)
  for (let i = 0; i < field.values.length; i++) {
    field.values[i] = Math.min(
      Math.max(field.values[i], original[i] - TOLERANCE),
      original[i] + TOLERANCE,
    )
  }
  boxBlur(field, SMOOTH_RADIUS)
}
src/style.css
파일 저장

html,
body {
  margin: 0;
  height: 100%;
  overflow: hidden;
  background: #0b0b0c;
  font-family: "Instrument Serif", serif;
}

#app {
  position: fixed;
  inset: 0;
}

canvas {
  display: block;
}

#info {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 1;
  box-sizing: border-box;
  width: min(532px, calc(100vw - 24px));
  padding: 32px 14px 38px;
  color: #fff;
}

#info h1 {
  margin: 0 0 16px;
  font-size: 18px;
  font-weight: 500;
  line-height: 1.1;
  letter-spacing: -0.035em;
}

#info nav {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
}

#info a,
#info span {
  color: rgb(255 255 255 / 65%);
  text-decoration: none;
}

#info a:hover {
  color: #fff;
}

.project-links {
  font-size: 12px;
  font-weight: 400;
  line-height: 1.2;
}

#info .tags {
  margin-top: 18px;
  gap: 8px 12px;
  font-size: 12px;
  font-weight: 300;
  line-height: 1.2;
}

#nav {
  position: fixed;
  bottom: 16px;
  left: 16px;
  z-index: 1;
  display: flex;
  gap: 16px;
  font-size: 13px;
  line-height: 1;
}

#nav a {
  color: rgb(255 255 255 / 58%);
  text-decoration: none;
}

#nav a.active,
#nav a:hover {
  color: #fff;
}

@media (max-width: 600px) {
  #info {
    padding: 20px 14px 24px;
  }

  #info h1 {
    margin-bottom: 12px;
  }

  #info .tags {
    margin-top: 12px;
  }
}
vite.config.js
파일 저장

import { cp } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { dirname, join, resolve } from 'node:path'
import { defineConfig } from 'vite'

const extensionsDir = join(
  dirname(
    createRequire(import.meta.url).resolve(
      'three/addons/inspector/Inspector.js',
    ),
  ),
  'extensions',
)

function inspectorExtensions() {
  let extensionsOutDir = 'dist/extensions'

  return {
    name: 'inspector-extensions',
    apply: 'build',
    configResolved({ root, build }) {
      extensionsOutDir = join(resolve(root, build.outDir), 'extensions')
    },
    async closeBundle() {
      await cp(extensionsDir, extensionsOutDir, { recursive: true })
    },
  }
}

export default defineConfig({
  optimizeDeps: {
    entries: ['index.html', 'demo2.html', 'demo3.html'],
    exclude: ['three'],
  },
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        demo2: 'demo2.html',
        demo3: 'demo3.html',
      },
    },
  },
  plugins: [inspectorExtensions()],
})
Original author attribution실행 안내·자료
파일 저장

Relighting Images with Depth Maps and Three.js
Original author: Dominik Fojcik
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
Media credits and license evidence실행 안내·자료
파일 저장

README.md
## Credits

- [Three.js](https://threejs.org/) — 3D rendering and TSL/WebGPU.
- [Depth Anything 3](https://github.com/ByteDance-Seed/Depth-Anything-3) — depth estimation.
- [Depth Generation Tool](https://depth.fojcikdominik.com/) — used to generate the depth maps.


## License

[MIT](LICENSE)
Bundled dependency licenses실행 안내·자료
파일 저장

three@0.184.0 — LICENSE
The MIT License

Copyright © 2010-2026 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.