TSL(Three.js Shading Language)Part1

Shader

WebGPUでThree.JSを描画できるようになり、実務でもテストで導入したところです。
導入にあたり、仕様のほうも以前のWebGLRendererから大幅に変わったところがあります。以前までShaderMaterialで使用していた箇所がNodeMaterialに代わり、vertex.glsl、fragment.glslをTSL(Three.js Shading Language)に書き換える必要が出てきました。
そこで、ちょうど仕様が定まってきたので、備忘録をかねて、コードを変換する方法をサンプルコード付きで紹介します。

Three.js Shading Language
JavaScript 3D Library. Contribute to mrdoob/three.js development by creating an account on GitHub.

webgpu_tsl_transpiler (GLSLコードをTSLに変換してくれるサンプルサイト)
*精度がいまいちですが、これをベースに書き換えをおこなえば大丈夫!

three.js examples

WebGPU 対象のブラウザは以下より確認できます

WebGPU | Can I use... Support tables for HTML5, CSS3, etc
"Can I use" provides up-to-date browser support tables for support of front-end web technologies on desktop and mobile w...

WebGPUの基本コード

Three.JSをWebGPUに対応させていきます。以下のCubeを表示させていきます。

typescriptになりますが、npmでthree.jsを読み込んで使用できるようにします。

npm install three --save-dev
npm install @types/three --save-dev

HTMLに以下のスクリプトを挿入してください。WebGPUで必須のようです。

<script type="importmap">
{
    "imports": {
        "three": "/build/three.webgpu.js",
        "three/webgpu": "/build/three.webgpu.js",
        "three/tsl": "/build/three.tsl.js",
        "three/addons/": "/jsm/"
    }
}
</script>

Typescriptですが、以下をコピペしていただければ描画できると思います

import * as THREE from 'three/webgpu'
import {
  color
} from 'three/tsl'
/* 基本構文 */

const scene = new THREE.Scene()

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  100
)
camera.position.x = 0
camera.position.y = 0
camera.position.z = 4

// WebGPURenderer
const renderer = new THREE.WebGPURenderer({
  antialias: true,
  alpha: true,
})
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2.0))
renderer.setSize( window.innerWidth, window.innerHeight  )
renderer.outputColorSpace  = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement)
// renderer.shadowMap.enabled = true
// renderer.toneMapping = THREE.NoToneMapping
// renderer.toneMappingExposure = 1.0

// レンダリングループの設定
renderer.setAnimationLoop(renderLoop)

// OrbitControls
// const controls = new OrbitControls(camera, renderer.domElement)
// controls.enableDamping = true

// Helper
const axesHelper = new THREE.AxesHelper( 1 );
scene.add( axesHelper );

/* TSL */

/*
NodeMaterial()
MeshBasicNodeMaterial
MeshStandardNodeMaterial()
MeshPhysicalNodeMaterial()
*/
const material = new THREE.NodeMaterial();
material.fragmentNode = color('crimson')

const mesh = new THREE.Mesh(new THREE.BoxGeometry(), material)
scene.add(mesh)

/* Animation */

function renderLoop() {
  //controls.update()

  renderer.renderAsync(scene, camera)
  //stats.update()
}

/* ユーティリティ */

window.addEventListener('resize', function () {
  let currentWidth = window.innerWidth;
  let currentHeight = window.innerHeight;

  camera.aspect = currentWidth / currentHeight
  camera.updateProjectionMatrix()
  renderer.setSize(currentWidth, currentHeight)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2.0))
})

以上で、WebGPUの基本的なコードが完了です。余談ですが、ブラウザがWebGPUに非対応の場合は、WebGLRendererに自動的に変換されますので、ご安心ください。

こちらの記事が、非常に参考になりました

Getting Started - Three.js Shading Language Tutorials

TSL(Three.js Shading Language)

時間ごとに色が変わるCubeをshaderMaterialで書きました。そのコードをTSLに書き換えてみます。

shaderMaterial のコード (WebGL)

typscript

import vertexShader from './glsl/vertex.glsl';
import fragmentShader from './glsl/fragment.glsl';

~~

const material = new THREE.ShaderMaterial({
  uniforms: {
    time: { value: 0.0 },
    resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
  },
  vertexShader: vertexShader,
  fragmentShader: fragmentShader,
  side: THREE.DoubleSide,
})

~~
const clock = new THREE.Clock();

function renderLoop() {

  const elapsedTime = clock.getElapsedTime()

  // シェーダーのtime uniformを更新
  material.uniforms.time.value = elapsedTime
  material.needsUpdate = true;

  //renderer.renderAsync(scene, camera)
  renderer.render(scene, camera)

  requestAnimationFrame(renderLoop)
}
renderLoop()

vertex.glsl

precision mediump float;

uniform float time;
uniform vec2 resolution;

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;

void main() {
    // UVをフラグメントシェーダーに渡す
    vUv = uv;

    // 法線をワールド座標系に変換してフラグメントシェーダーに渡す
    vNormal = normalMatrix * normal;

    // 頂点位置をワールド座標系に変換してフラグメントシェーダーに渡す
    vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;

    // 最終的な頂点位置を計算(クリップ座標系)
    //vec4 worldPosition = modelMatrix * vec4(position, 1.0);
    //vec4 mvPosition =  viewMatrix * worldPosition;
    //gl_Position = projectionMatrix * mvPosition;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

fragment.glsl

precision mediump float;

uniform float time;
uniform vec2 resolution;

varying vec2 vUv;
varying vec3 vNormal;
varying vec3 vPosition;

void main() {
    // UVをフラグメントシェーダーに渡す
    vUv = uv;

    // 法線をワールド座標系に変換してフラグメントシェーダーに渡す
    vNormal = normalMatrix * normal;

    // 頂点位置をワールド座標系に変換してフラグメントシェーダーに渡す
    vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz;

    // 最終的な頂点位置を計算(クリップ座標系)
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

TSLで書き換えたコード (WebGPU)

TSLを使うには、使うプロパティをおのおの読み込ませる必要があります。必要があるものを追加していく感じです。

import * as THREE from 'three/webgpu'
import {
  positionLocal,
 positionGeometry,
  Fn,
  time,
  uv,
  vec3,
  color,
  sin,
  add,
  mul,
  cameraProjectionMatrix,
  modelViewMatrix,
  vec4,
  uniform,
  varying,
  vec2,
  normalLocal,
  modelNormalMatrix,
} from 'three/tsl'

vertexとfragmentに相当するものを別々のファイルでなく、関数として定義します。NodeMaterialに関数を付与することで実現していきます。今回、uTimeは使わずに、TSLにあるtimeを使って時間変化を与えています。

const utime = uniform( 'float' );
const resolution = uniform( 'vec2' );
const vUv = varying( vec2(), 'vUv' );
const vNormal = varying( vec3(), 'vNormal' );
const vPosition = varying( vec3(), 'vPosition' );

const vertexShader = Fn(( ) => {
  // UVをフラグメントシェーダーに渡す

	vUv.assign( uv() );

	// 法線をワールド座標系に変換してフラグメントシェーダーに渡す

	vNormal.assign( modelNormalMatrix.mul( normalLocal  ) );

	// 頂点位置をワールド座標系に変換してフラグメントシェーダーに渡す

	vPosition.assign( modelViewMatrix.mul( vec4( positionGeometry, 1.0 ) ).xyz );

	// 最終的な頂点位置を計算(クリップ座標系)
    return cameraProjectionMatrix.mul(modelViewMatrix).mul( vec4(positionGeometry, 1.0));
});

const fragmentShader = Fn(( ) => {
  // 基本的な色の設定
	const baseColor = vec3( 1.0, 0.5, 0.2 );

	// オレンジ色
	// 時間を使ったアニメーション
	const timeEffect = sin( time ).mul( 0.5 ).add( 0.5 );

	// UVを使ったグラデーション

	const finalColor = baseColor.mul( timeEffect ).add( vec3( vUv, 0.0 ).mul( 0.3 ) );
	return vec4( finalColor, 1.0 );
});
const material = new THREE.NodeMaterial();
material.vertexNode = vertexShader();
material.fragmentNode = fragmentShader();

以上が、基本的な書き換えになります。
リンクのwebgpu_tsl_transpilerでglslのコードをペーストして結果をベースに書き換えています。そのままのコードだとエラーします。gl_FragColorなどのプロパティはmaterial.fragmentNodeに変わっていますので、公式リファレンスで検索して、おのおのを置き換えていく必要があります。

varyingで渡した変数は、fragmentShaderの関数内で参照されて使用できます。

*はmul()、/ はdiv()、+はadd()、- はsub()などに置き換えなければなりません。読みずらいコードですが、ノードベースなので仕方ないかもです。glslを書けるのあれば、上記のやり方で変換できると思います。
uniformで定義した変数は、utime.value = ○○ に値を入れれば問題ありません。以下のコードはtimeをutimeに置き換えた場合のサンプルで、ループ内でutime.value = elapsedTime することで時間変化を与えられます。

/* Animation */
const clock = new THREE.Clock();

function renderLoop() {
  //controls.update()
  const elapsedTime = clock.getElapsedTime()

  //@ts-ignore
  utime.value = elapsedTime;

  // シェーダーのtime uniformを更新
  //material.uniforms.time.value = elapsedTime

  renderer.renderAsync(scene, camera)
  //renderer.render(scene, camera)

  //requestAnimationFrame(renderLoop)
  //stats.update()
}

Part1はここまでになります。
まだまだリファレンスが少ないですが、以下のリンクが参考になります。ぜひご覧ください

Getting to grips with ThreeJS Shading Language (TSL) – NiksCourses
PositionLocal - Three.js Shading Language Tutorials
タイトルとURLをコピーしました