three.js, LightProbeGrid API 핵심 코드

이 글은 three.js를 이용한 효과적인 광원 효과를 위해 LightProbeGrid를 WebGPU 렌더러에서 사용할 때 빠른 적용을 위해 작성된 글입니다.

먼저 필요한 API에 대한 import 문입니다.

import { LightProbeGrid } from 'three/examples/jsm/lighting/LightProbeGrid.js';
import { LightProbeGridHelper } from 'three/examples/jsm/helpers/LightProbeGridHelper.js';

관련된 필드는 다음과 같습니다.

private probes?: LightProbeGrid;
private probesHelper?: LightProbeGridHelper;

LightProbeGrid는 정적 장면에 대한 광원 효과입니다. 즉, 장면을 구워야 합니다. 아래의 코드는 장면에 대한 광원을 굽는 코드입니다.

async bake(resolution: number) {
  if (this.probes) {
    this.scene.remove(this.probes);
    this.probes.dispose();
  }

  const probes = new LightProbeGrid(
    5.6, 4.7, 5.6,
    resolution, resolution, resolution
  );

  probes.position.set(0, 2.45, 0);

  await probes.bake(
    this.renderer, this.scene,
    { 
      cubemapSize: 32,
      sampleCount: 512, 
      bounces: 0,
      near: 0.05, 
      far: 20
    }
  );

  probes.visible = params.enabled;
  this.scene.add(probes);

  this.probes = probes;

  // Update debug visualization

  if (!this.probesHelper) {
    this.probesHelper = new LightProbeGridHelper(probes);
    this.probesHelper.visible = params.showProbes;
    this.scene.add(this.probesHelper);
  } else {
    this.probesHelper.probes = probes;
    this.probesHelper.update();
  }
}

위의 코드에서 사용된 설정값에 대한 객체는 다음과 같습니다.

const params = {
  enabled: true,
  showProbes: true,
  resolution: 6
};

GUI를 통해 설정값을 조정하기 위한 코드는 다음과 같습니다.

private setupGUI() {
  const gui = this.renderer.inspector.createParameters('Light Probes');
  gui.add(params, 'enabled').name('GI').onChange((value: boolean) => {
    if(this.probes) this.probes.visible = value;
  });

  gui.add(params, 'resolution', 2, 12, 1).name('Resolution').onChange((value: number) => {
    this.bake(value);
  });

  gui.add(params, 'showProbes').name('Show Probes').onChange((value: boolean) => {
    if(this.probesHelper) this.probesHelper.visible = value;
  });
}

three.js, 대규모 장면에서의 그림자 적용을 위한 SunLight

SunLight는 태양광인데, 흔히 알고 있고 있는 DirectionalLight와 비슷합니다. 다만 그림자로써의 DirectionLight와 다르게 처리되는데 훨씬 큰 범위의 장면에 대한 그림자를 Cascade 방식으로 처리하여 표현합니다. SunLight을 적용하기 위한 코드를 순서대로 정리하면 다음과 같습니다.

먼저 필요한 클래스를 import 합니다.

import { SunLight } from 'three/addons/lights/SunLight.js';
import { SunLightNode } from 'three/addons/lights/SunLightNode.js';

SunLight는 그림자 표현을 위해 SunLightNode라는 TSL 노드를 사용합니다. 이 TSL 노드는 기본 노드가 아닌 확장 노드이므로 등록이 필요합니다. 다음처럼 등록할 수 있습니다.

this.renderer.library.addLight(SunLightNode, SunLight);

실제 SunLight를 생성하는 코드는 다음과 같습니다.

private sunLight!: SunLight;

private setupSunLight() {
  this.renderer.library.addLight(SunLightNode, SunLight);

  const sunLight = new SunLight();
  sunLight.position.setFromSphericalCoords(1, THREE.MathUtils.degToRad(90 - params.elevation), THREE.MathUtils.degToRad(params.azimuth));

  sunLight.castShadow = true;
  sunLight.shadow.camera.far = params.far; // maximum shadow distance
  sunLight.shadow.mapSize.setScalar(params.resolution);
  sunLight.shadow.normalBias = 0.05;
  this.scene.add(sunLight);

  this.sunLight = sunLight;
}

위의 코드를 보면 설정을 위해 param 객체를 사용하고 있는데, 해당 객체는 다음 예시와 같습니다.

const params = {
  showCascades: false,
  far: 1000,
  resolution: 2048,
  azimuth: 135,
  elevation: 10
};

이외에 기본적으로 그림자를 위한 렌더러와 매시에 대한 코드도 추가해야 합니다. 즉..

renderer.shadowMap.enabled = true;

...

mesh.castShadow = true;
mesh.receivedShadow = true;

three.js, SkyMesh를 이용한 환경광

SkyMesh는 하늘에 대한 시각적인 표현을 해주는 API입니다. 이 SkyMesh를 이용하여 시각적인 표현 뿐만 아니라 실제 광원으로써 사용하기 위한 코드에 대한 내용입니다.

먼저 SkyMesh를 사용하기 위해 다음과 같은 import 문이 필요합니다.

import { SkyMesh } from 'three/addons/objects/SkyMesh.js';

그리고 SkyMesh 객체를 생성합니다.

private sky!: SkyMesh;

private setupSky() {
  const sky = new SkyMesh();
  sky.scale.setScalar(9000);
  // this.scene.add(sky);

  sky.sunPosition.value.copy(this.sunLight.position);
  sky.turbidity.value = 3;
  sky.rayleigh.value = 2;
  sky.cloudSpeed.value = 0; // keep the clouds static, like the WebGL version
  sky.showSunDisc.value = false; // the sun is represented by the light

  this.sky = sky;
}

위의 코드는 SkyMesh 객체를 생성했고 있는데, SkyMesh의 위치를 잡기 위해 this.sunLight의 position을 참고하고 있습니다. 고도와 방위각을 통해 직접 그 위치를 정하고 싶다면 다음 코드를 참고하시기 바랍니다.

sunLight.position.setFromSphericalCoords(
  1, 
  THREE.MathUtils.degToRad(90 - params.elevation), 
  THREE.MathUtils.degToRad(params.azimuth)
);

이를 scene에 추가하면 하늘이 표현됩니다. 하지만 하늘 표현뿐만 아니라 SkyMesh를 이용해 환경광을 구성해야 합니다. 이를 위한 코드는 다음과 같습니다.

private setupEnvironment() {
  const pmremGenerator = new THREE.PMREMGenerator(this.renderer);
  const sceneEnv = new THREE.Scene();
  sceneEnv.add(this.sky);

  const renderTarget = pmremGenerator.fromScene(sceneEnv);
  this.scene.environment = renderTarget.texture;
  // renderTarget.dispose();

  this.scene.add(this.sky);
}

WebGPU 기능 감지

거의 모든 웹브라우저가 WebGPU를 지원하지만 확인은 다음과 같이 할 수 있습니다.

const adapter = await navigator.gpu?.requestAdapter();
if (!adapter) {
  return;
}

WebGPU 지원과는 별개로 WebGPU에서 제공하는 기능은 그 단위마다 지원 여부를 확인해야할 필요는 있습니다. 이에 대한 코드는 다음과 같습니다.

const hasFloat32Filtering = adapter.features.has('float32-filterable');
const hasTimestamps = adapter.features.has('timestamp-query');

three.js, TSL 노드 varying 예제

vertex shader의 결과를 fragment shader로 전달하기 위한 varying을 TSL 노드로 사용하기 위한 예제입니다. 먼저 varying으로 사용될 데이터를 정의합니다.

const colorVarying = vec3(0).toVarying(); // same as varying(vec3(0))

이 varying 데이터의 값을 재질의 vertex shader에 해당하는 positionNode에서 할당합니다.

material.positionNode = Fn(() => {
  ...

  colorVarying.assign(texture(colorTexture, colorUv).rgb)

  ...
}

varying 데이터를 재질의 다양한 fragment shader 중 하나에 해당하는 colorNode에서 사용합니다.

material.colorNode = colorVarying