物理ベースレンダリング」タグアーカイブ

物理ベースにのっとったImage Based Lightingを実装してみた

Three.jsでIBL実装してみたいけど理屈が難しいんだよなぁ、と考えながら色々調べていたらJoey de Vriesさんが素晴らしい記事を公開してくれているのを発見。

読んだ限りだと、拡散光の事前計算は法線ごとに積分方向と内積を取りながら半球積分して、結果をキューブマップに保存するようなイメージなのかな。これはキューブマップにガウシアンフィルタをかけるようなもんだし、処理も想像しやすい。

鏡面反射の事前計算がやっぱ難しいねんな……
入射光と鏡面BRDFに分けて半球積分する感じかな。
入射光の半球積分は、NDFでいい感じに重みづけした上で、上手いことラフネスごとにキューブマップに積分結果を保存する感じだろうか(語彙力)。
鏡面BRDFはパラメータに法線、頂点から視点へのベクトル、ラフネスなんかが絡んで、単純に半球積分を事前計算できないっぽい。なので式を上手いこと変形してやったり、パラメータとしてラフネスと、法線と視点ベクトル の角度とかを取るようにしてやったり、なんやかんやしたら事前計算結果を2次元のLUTとして保存できるようになるようだ(語彙力)。

と、何となくの概要をつかんだ?のでツールに頼りながら実装に挑戦してみる。
シェーダーのコードは Joey de Vriesさんの以下の記事を参考にしています。
Learn OpenGL : Lighting
Learn OpenGL : Diffuse irradiance
Learn OpenGL : Specular IBL

0. 実装結果

動作サンプルは下記URLを参照
http://nktk-tech.com/example/ibl-test/main.html

1. 事前計算

事前計算のツールとして、今回はIBL Bakerというツールを使ってみた。
公式のgitから落とせば、Windows用のバイナリも付いてくる。

環境マップは、以下のサイトから拝借した。
http://www.hdrlabs.com/sibl/archive.html

IBL Bakerの使い方は簡単で、まずhdrlabs.comから取得したファイルを、「Load Environment」ボタンでロードする。
ロードが終わったら、Filteringメニューで良い感じにキューブマップを調整する。
自分はファイルサイズが気になったので、 「Environment Format」をRGBA16に設定した。
また、なるべくJoey de Vriesさんの記事に合わせておきたかったのでbrdfをsmith.brdfにしておいた。
設定が終わったら、「Save Environment」ボタンで事前計算を行い結果を保存する。
ファイル名に「.dds」を付けないと正しく保存できないという情報があったので注意すること。
出力されたファイルの内、「〇〇Brdf.dds」「〇〇DiffuseHDR.dds」「〇〇SpecularHDR.dds」「〇〇EnvHDR.dds」が必要なファイル。(今回の例だと鏡面反射の事前計算キューブマップをそのまま環境マップとして使ったので、 〇〇EnvHDR.dds は使っていない)

2. Three.jsで.ddsファイルを読み込み

Three.jsには.ddsを読み込むためのローダーが付属しているので、それを用いて読み込めばいいかと考えた。しかし、IBL Bakerが吐き出す.ddsファイルのフォーマットにローダーが対応していないかった。
場当たり的だけど、勘で以下みたいな感じにThree.jsのローダーを書き換えた。(全コードは動作サンプルを参照)

・fourCCに応じた処理を追加

switch ( fourCC ) {
	case FOURCC_DXT1:
		blockBytes = 8;
		dds.format = THREE.RGB_S3TC_DXT1_Format;
		break;
	case FOURCC_DXT3:

       ......
       
	default:
       
       .......
       // fourCC === 116(1チャネル4バイト)、fourCC === 113(1チャネル2バイト)の場合の処理を追加
		} else if ( header[ off_RGBBitCount ] === 128 && fourCC === 116) { //A32B32G32R32F
			var channelBytes = 4;
			isRGBAUncompressed = true;
			blockBytes = 256;
			dds.format = THREE.RGBAFormat;
		} else if ( header[ off_RGBBitCount ] === 64 && fourCC === 113) { //A16B16G16R16F 
			var channelBytes = 2;
			isRGBAUncompressed = true;
			blockBytes = 128;
			dds.format = THREE.RGBAFormat;
		} else {
			console.error( 'THREE.DDSLoader.parse: Unsupported FourCC code ', int32ToFourCC( fourCC ) );
			return dds;
		}
}

・1チャンネルがマルチバイトだった場合は、自作の関数でバッファにデータを詰めるよう変更
 (Float16Arrayがなかったので、Uint16Arrayで代用)

function loadFloatARGBMip( buffer, dataOffset, width, height, channelBytes) {
	var dataLength = width * height * 4;
	if (channelBytes === 4) {
		var srcBuffer = new Float32Array( buffer, dataOffset, dataLength );
		var byteArray = new Float32Array( dataLength );
	} else {
		var srcBuffer = new Uint16Array( buffer, dataOffset, dataLength );
		var byteArray = new Uint16Array( dataLength );
	}
	var dst = 0;
	var src = 0;
	for ( var y = 0; y < height; y ++ ) {
		for ( var x = 0; x < width; x ++ ) {
			var r = srcBuffer[ src ]; src ++;
			var g = srcBuffer[ src ]; src ++;
			var b = srcBuffer[ src ]; src ++;
			var a = srcBuffer[ src ]; src ++;
			byteArray[ dst ] = r; dst ++;	//r
			byteArray[ dst ] = g; dst ++;	//g
			byteArray[ dst ] = b; dst ++;	//b
			byteArray[ dst ] = a; dst ++;	//a
		}
	}
	return byteArray;
}


......


for ( var face = 0; face < faces; face ++ ) {

	var width = dds.width;
	var height = dds.height;

	for ( var i = 0; i < dds.mipmapCount; i ++ ) {

		if ( isRGBAUncompressed && channelBytes === 4) {

			var byteArray = loadFloatARGBMip( buffer, dataOffset, width, height, channelBytes);
			var dataLength = byteArray.length * 4;

		} else if ( isRGBAUncompressed && channelBytes === 2) {

			var byteArray = loadFloatARGBMip( buffer, dataOffset, width, height, channelBytes );
			var dataLength = byteArray.length * 2;

		} else if ( isRGBAUncompressed ) {

.ddsファイルの読み込みは以下みたいな感じで行う

// 1チャンネルが2バイトのテクスチャの場合、.typeにTHREE.HalfFloatTypeを指定
this.diffuseCubeMap = ddsLoader.load( dirPath + 'texDiffuseHDR.dds', (loadedCubeMap) => {
    loadedCubeMap.type = THREE.HalfFloatType;                        
    this.uniforms.diffuseCubeMap.value = loadedCubeMap;
});

// 1チャンネルが4バイトのテクスチャの場合、.typeにTHREE.FloatTypeを指定
ddsLoader.load( dirPath + 'texBrdf.dds', (loadedMap) => {
    loadedMap.type = THREE.FloatType;                        
    this.uniforms.brdfLUT.value = loadedMap;
});

3. 読み込んだテクスチャを元にレンダリングを行う

※この記事で説明していないThree.js周りの処理とかはhttps://blog.nktk-tech.com/2019-03-09-01/で解説してるかも

読み込んだ事前計算マップを元にレンダリングを行うフラグメントシェーダのコードは以下のようになった。
基本的にJoey de Vriesさんのコードのまんまだから、解説はあちらの記事を参照。

#version 300 es
in vec3 vViewPosition;
in vec3 vNormal;
in vec3 vViewDir;

in vec3 wNormal;
in vec4 wPosition;

out vec4 out_FragColor;

// uniforms
uniform float metallic;
uniform float roughness;
uniform vec3 albedo;
uniform samplerCube specularCubeMap;
uniform samplerCube diffuseCubeMap;
uniform sampler2D brdfLUT;
uniform float maxLodLevel;

vec3 fresnelSchlickRoughness(float NdotV, vec3 F0, float roughness)
{
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - NdotV, 5.0);
}   

void main() {
    vec3 wVertexToCamera = normalize(cameraPosition.xyz - wPosition.xyz);
    vec3 wReflect = normalize(reflect(-wVertexToCamera, wNormal));

    vec3 F0 = vec3(0.04); 
    F0 = mix(F0, albedo, metallic);
    vec3 F = fresnelSchlickRoughness(max(dot(vNormal, vViewDir), 0.0), F0, roughness);

    vec3 kS = F;
    vec3 kD = 1.0 - kS;
    kD *= 1.0 - metallic;	

    vec3 irradiance = texture(diffuseCubeMap, wNormal).rgb;
    vec3 diffuse    = irradiance * albedo * kD;

    vec3 prefilteredColor = textureLod(specularCubeMap, wReflect,  roughness * maxLodLevel).rgb;   
    vec2 envBRDF  = texture(brdfLUT, vec2(max(dot(vNormal, vViewDir), 0.0), roughness)).rg;
    vec3 specular = prefilteredColor * (kS * envBRDF.x + envBRDF.y);

    out_FragColor = vec4(diffuse + specular, 1.0);
}

4. まとめ

パラメータを変えてみたりすると、見た目は以下のように変化した。
若干違和感があるが、まあまあそれっぽいように思う。

せっかくHDRのテクスチャ用意したのに、トーンマッピングやらガンマ補正やらは全くやっていない。出力されている画像に違和感があるのはそれが原因かも。
そこら辺の話は何とか理解できそうなので、気が向いたら実装したい。

しかし事前計算周りの理解がふわっとしてるせいで、コードの解説もすごいふわっとしたものになってしまった……🤔
きっちり理解した上で事前計算も実装したいけど自分の頭だと理屈が中々難しくてなぁ……
しかも情報が英語に偏ってるので厳しい。
やっぱ英語と数学がっつり学び直さんとなー(ということを3年くらい前からずっと言ってる)

JavaScriptで物理ベースレンダリングを実装してみる

3D触ってるのに物理ベースレンダリングについて全然知らないのはいかんよなー、と考えていたところ、@mebiusboxさんが以下の素晴らしい記事を公開しているのを見つけたので実装にチャレンジしてみる。
基礎からはじめる物理ベースレンダリング
他3DCG系ドキュメント

基本的には基礎からはじめる物理ベースレンダリングの実装編を参考にし、補足的に以下の記事なども見ながら実装してみた。
超雑訳 Real Shading in Unreal Engine 4
脱・完全鏡面反射~GGXについて調べてみた~

実装結果 :
動作サンプルは下記URLを参照
http://nktk-tech.com/example/pbr-test/main.html

割とそれっぽく動いていると思う。
standard_materialのチェックボックスをonにすると、Three.jsが用意したシェーダーと切り替えられる。
自分で実装した方が若干暗いけど、これはたぶんUnreal Engineの式を参考にした個所があるからだと思う。

鏡面反射が難しくて理解が大分怪しい……
雰囲気だけで理解して、式の導出とかはついていけてない感じが。
IBLとか実装すると、レンダリング結果に凄い説得力が増すから実装してみたいんだけど、鏡面反射以上に数式が難しいんだよなぁ……
今までごまかしごまかしやってきたけど、そろそろ本腰いれて数学学ばないといけんかもな。