Cyberpunk 2077をGeForce NOWでプレイする

前々から楽しみだったCyberpunk 2077を買った。

だがさすがに10年前に組んで以来、ちょこちょこパーツを継ぎ足ししてきたPCだと動作が怪しかった。
Core i7 3770k, メモリ8G, GTX970にSSDを積んだPCなので、最低スペックにはギリギリ届いてるんじゃないかと思ったけど、画質落としても動作がもっさりする。
起動時に自動設定された中程度のグラフィックの設定だと動作が厳しかったので、グラフィックの設定は最低にし、群衆の密度の設定を中間に、解像度を1920×1080から1366×768に落としてギリギリ我慢できるぐらいの動作になった。
ただ、これでもマップの状況次第では重くなってしまった。

ということでGeForce NOWというクラウドでゲームを動かすサービスを試してみた。
https://cloudgaming.mb.softbank.jp/
大本のサービスだと大体月額500円だが、日本だと月額1800円かかるのが気にかかるが、1ヶ月の無料期間がある。
クラウドでゲームを動かすとなるとラグが気になるが、もともと自分のPCのスペックだと重たいし、1ヶ月の無料期間あるしで試すだけ試して見ようと考えた。
Steamクラウドにセーブデータを保存しているなら、セーブデータはGeForce NOWでも引き継げる。

アパートに備え付けの回線(JCOMのだった気がする)に無線LANという舐めた環境だったが、これが思いの外サクサク動いてくれた。
DL速度は100Mbps程度で、応答速度も40ms程度の環境なので、最低限その程度の回線があれば大丈夫なのかもしれない。
グラフィックスの設定でデフォルト設定を適用してみたら、レイトレースONの最高設定に自動でなった。

4時間程度プレイしてみたが、動作はかなり安定しているように思えた。
2, 3回ほど通信の影響で、5秒ほどのフリーズがあったが、おそらく通信の環境が整ってる人ならそれも起こらないはず。戦闘中などにこれが起こると辛いが、今の所あまり困ってはいない。
RPG要素の強いCyberpunk 2077のゲーム性と、自分があんまりガチのアクションプレイヤーじゃないのもあって、ラグは全く気にならなかった。

これで値段がもう少し安ければすごい良いサービスなんだけどなあ……
別会社通してサービス展開するので、値段が多少上がるのは仕方ないとは思うが、3倍以上本家と値段が違うってのはぼり過ぎだと思う。

Cyberpunk 2077自体は今の所、街中歩いてるだけで楽しい非常にいいゲーム。
オープンワールドは歩いてて楽しいかどうかで面白さの8割が決まる気がする。
車を足場にしたり、微妙な凹凸を利用して移動したりで、見えている所にはそれなりに行けるのが楽しい。意味もなく建物よじ登ったりしてしまうね。

Unity Hubが起動しない

仕事で使うので、Unityをインストールしてみる。
せっかくなのでUnity Hubもインストールしたら起動しない。

ダブルクリックしてもうんともすんとも言わず、タスクマネージャーを見たら一応はタスクは立ち上がってるが、ウィンドウ等は全く開かない状態。
C:\Users\ユーザー名\AppData\Roaming\UnityHub\logsのログを見たら、
「{“level”:”error”,”message”:”Unhandled promise rejection, reason: The specified module could not be found.\r\n\\?\C:\Program Files\Unity Hub\~~~~」
みたいな感じのログが出てる。

どうもVisual C++ 再頒布可能パッケージがないのが原因のようで、以下からインストールしたら起動するようになった。
https://www.microsoft.com/ja-JP/download/details.aspx?id=48145

参考 :
https://forum.unity.com/threads/unity-hub-window-does-not-appear.784892/

アクションゲームの要素

酔いに任せて駄文を書く。

ショートコードで書かれるゲームや、プリミティブな要素だけでできているゲームなんかを見ていて、ふとアクションゲームの根幹をなすものとは何だろうかと考えたりした。
色々考えた結果、アクションゲームの根っこには2つの要素があるのではないかと考えた。

①ある対象を適切な位置に移動させること。
②タイミングよくボタンを押すこと。

この2つの組み合わせ、あるいは片方のみの利用で、アクションゲームは成り立っているのではないかと考えた。

①に関しては、抽象化しすぎてる感があるので、もう少し詳しく説明すると、
・ある対象から逃げる
・ある対象の位置に移動する
の二点がメインの要素であるように思う。
「ある対象から逃げる」というのはわかりやすいと思う。
避けゲーなどはこれのみで成り立っているといってもいい。
シューティングなども、弾幕ゲーなどはこの要素がメインで成り立っているし、
マリオなどのアクションゲームもこの要素が強いだろう。
「ある対象の位置に移動する」は少しわかりにくいが、アイテムを拾ったり、
シューティングなどで自弾を当てるために、敵に対して適切な位置に移動したり、
カーソルを敵に合わせたりすることを言う。
逃げることに比べると、こちらの要素をメインにしてるゲームは少ないように思う。

②に関しては、音ゲーなどが典型だと思う。
音ゲーはタイミングよくボタンを押すことがメインの要素となりゲームが成り立っている。
マリオなどでもファイアボールや、マントで飛ぶ際などはこの要素が出てくるし、
シューティングなどでも、タイミングよくボタンを押すことで、ボムや自弾を適切に使うことが出来る。

FPSなど複雑なアクションゲームも、①の要素、つまり敵に対して有利なポジションに移動し、敵にカーソルを合わせること、
②のタイミングよくボタンを押し、敵を倒すことの2つがゲームの根幹をなしているように思う。

もちろんRPGやシミュレーションなどはこの2要素に当てはまらないが、
アクションに関してはかなりのところを語れるのではないかと考える。

特にミニゲーム的なものを考えるとき、この2点から出発し、
どのようにフレーバーを加えるか考えるというのも一つのやり方じゃないかと思ったりした。
(と言ってもフレーバーがミニゲームの本質を決めるので、
この手順でゲームを考えてもあまり意味はない気はする)

Return of the Obra Dinn(オブラディン号の帰港)をプレイ

Papers, pleaseの作者が開発したReturn of the Obra Dinnをクリア。
非常に良いゲームだった。

主人公の保険調査官を操作し、船員の消えたオブラディン号を調査するゲーム。
船内にある死体から、その死体の死に際の記憶を見ることが出来て、その記憶から次の記憶へ辿りながら船員全員の安否や死因を探っていく。

絵、音楽、ストーリー、ゲーム性どれを取ってもレベルが高い。
単にレベルが高いだけでなく、互いが互いの魅力を引き出すように設計されているのが素晴らしい。
白黒の2値で描かれた絵は、クトゥルフを基調にしたような1800年代舞台のストーリの雰囲気との相性が非常に良い。それを場面場面で流れるクラシカルな音楽が非常に盛り上げてくれる。そのストーリーに浸りながら、船員に何があったかを推測していくのはとても上質な体験だった。ここまで高いレベルでゲームの個々の要素をまとめ上げられる人は中々いないように思う。

作者の前作のPapers, pleaseも同じく、ゲームの個々の要素を高いレベルでまとめてたな。
ひたすら単純作業繰り返してるうちに、徐々に倫理観が消えて行って、いつのまにか入国審査官になりきってしまったのは思わず自分で笑ってしまった。

Return of the Obra Dinn、ゲームの難易度もちょうどよくて、途中で詰まっても別の人の調査してるうちにふっと問題が解決したりして気持ちが良い。詰まったら服装とか装飾品をしっかり見ることが大事やね。
ただ、最後2人残った甲板員の正解が分からんかったんだよね。2択だから、推理外れたら別の方が正解といった感じでクリアしてしまった。この人らについては、再度プレイして納得いく証拠を探しておきたい。

高いゲーム性のおかげで、魅力的な世界観に浸れるゲームだった。
サントラ出ないかな……

物理ベースにのっとった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のテクスチャ用意したのに、トーンマッピングやらガンマ補正やらは全くやっていない。出力されている画像に違和感があるのはそれが原因かも。
そこら辺の話は難しいものでもないので、気が向いたら実装したい。

キューブマップとミップマップを持つddsファイルのビューア

キューブマップ、ミップマップを持つddsファイルを閲覧できるビューアが無いか探してみた。
Windows Texture Viewerってソフトで、上記の要件を満たせたが、若干動作が不安定なんだよな……
しかし動作が軽くてシンプルなので、しばらくはこれ使ってみようと思う。
zとxキーで表示するキューブマップの側面の変更、左右の矢印キーでミップマップレベルの変更が可能。

HDRIのEquirectangular形式の画像からCubeMapを生成するメモ

Three.jsでIBLを行う際に、HDRIの画像があったら便利だと思ったので調べてみた。
以下のサイトでフリーのHDRIのEquirectangularの画像を手に入れられる。
http://www.hdrlabs.com/sibl/archive.html
また、以下のサイトでHDRIのEquirectangular画像からCubeMapを生成できる。
HDRI-to-CubeMap(https://github.com/matheowis/HDRI-to-CubeMap)
ありがてえ……😂

また、 EquirectangularのままだとCubeMapとしては使えないが、Three.jsのサンプルにはEquirectangularの画像をCubeMapとして利用する例や、そのまま環境マップやスカイドームとして利用する例があった(非HDRI画像)。
①Three.jsにて Equirectangularの画像からCubeMapを作成する例(非HDRI)
https://threejs.org/examples/#webgl_materials_cubemap_dynamic

② Equirectangularの画像をそのまま環境マップとして利用する例 (非HDRI)
https://threejs.org/examples/#webgl_materials_envmaps
②に関して自分でシェーダー書くなら以下のリンクも参考になりそう
パノラマ画像からの環境マッピング


HDRI-to-CubeMapにてHDRIのCubeMapを作成した場合、以下のサンプルを参考にHDRIのCubeMapを読み込める。
③HDRIのテクスチャを読み込む例
https://threejs.org/examples/#webgl_loader_texture_hdr

④HDRIのCubeMapを読み込む例
https://threejs.org/examples/#webgl_materials_envmaps_hdr
gitのthree.js/examples/js/loaders/にあるRGBELoader.jsにてHDRI画像が読み込めるようになるようだ。
また、同フォルダのHDRCubeTextureLoader.jsは、RGBELoader.jsを使いHDRIのCubeMapを作成するためのクラスとして利用できる。

HDRIのEquirectangularを環境マップとして利用したい場合、RGBELoader.jsを使いテクスチャを読み込み、①や②のサンプルと同じことをすると環境マップとして使えるようになるかも?時間があるときに調べてみる。

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とか実装すると、レンダリング結果に凄い説得力が増すから実装してみたいんだけど、鏡面反射以上に数式が難しいんだよなぁ……
今までごまかしごまかしやってきたけど、そろそろ本腰いれて数学学ばないといけんかもな。

Three.jsにて自作シェーダーを試す際のサンプル Part.2

前回の続き。
GUIにてパラメータの調整ができると楽なので、今回はdat.guiによるパラメータの調整と、stats.jsによるリソースの状態の確認について書く。
サンプルは以下のURLを参照。
http://nktk-tech.com/example/three-template2/main.html

解説

dat.guiは以下のように用いる。

dat.gui周りの処理抜粋// dat.guiにて操作したいパラメータを持つオブジェクト
const lightProperty = {
    "color" : "#ffffff",
    "pos_x" : 3.0,
    "pos_y" : 4.0,
    "pos_z" : 0.0
};

// dat.guiの項目を初期化
const gui = new dat.GUI({name: 'light_property'});
gui.addColor(lightProperty, 'color').onChange( () => light.color.set(lightProperty.color) );
gui.add(lightProperty, 'pos_x', -5.0, 5.0, 0.1).onChange( 
    () => light.position.setX(lightProperty.pos_x ));
gui.add(lightProperty, 'pos_y', -5.0, 5.0, 0.1).onChange( 
    () => light.position.setY(lightProperty.pos_y ))
gui.add(lightProperty, 'pos_z', -5.0, 5.0, 0.1).onChange( 
    () => light.position.setZ(lightProperty.pos_z ));

まず、dat.guiにて変更したいパラメータを持つオブジェクトを用意する。
色に関してはThree.jsもdat.guiもCSSの書き方に対応しているのでそれで初期化すると楽かも。
そしてdat.guiのクラスに対して、パラメータのオブジェクトを登録していくと、パラメータをGUIで操作できるようになる。

色については、dat.GUI.addColor, それ以外は dat.GUI.addにて項目を追加できる。
第一引数にパラメータのオブジェクト 、第二引数は操作したいパラメータを文字列を指定する。
パラメータが数値の場合は、第三引数以降に最小値、最大値、刻み幅を指定できる。
詳しくは下記の公式ドキュメントを参照。
https://github.com/dataarts/dat.gui/blob/master/API.md#GUI+add

追加したパラメータの型によってdat.guiがよしなにgui作成してくれる。
詳しくは下記チュートリアル参照。これ見とけば大体のこと解決できる気がする。
http://workshop.chromeexperiments.com/examples/gui

.addにてdat.guiの項目を追加すると、dat.guiのControllerクラスが返される。
https://github.com/dataarts/dat.gui/blob/master/API.md#Controller
これの.onChangeにて、値が変化したときのコールバック関数を設定できる。
アニメーションループ内でフレームごとにプロパティ再設定するなら、.onChangeに関数設定する必要ないかも。

前回の記事のにそのままdat.guiを加えると、dat.guiの操作と同時に表示している3Dオブジェクトが動いちゃうので注意する。
OrbitControlsの第二引数に、表示用のcanvasのdomを渡してやれば、canvas範囲外でのマウスドラッグは無視できるようになる。

OrbitControlsの設定例const controls = new THREE.OrbitControls( camera, document.querySelector('#myCanvas') );

stats.jsによるfpsの表示は以下のコードで可能。

stats.js周りの処理を抜粋// stats.jsによりfpsを表示
const stats = new Stats();
stats.dom.style.left = "10px";
stats.dom.style.top = "10px";
document.body.appendChild( stats.dom );

// 毎フレーム時に実行されるループイベント
function tick() {
    controls.update();
    renderer.render(scene, camera);
    stats.update(); // 毎フレームごとにstats.update()を呼ぶ必要がある。
    requestAnimationFrame(tick);
}

クラスを作成して、適当なdomの配下に加えて、毎フレームupdateを呼び出すだけなので簡単。
他のリソースを表示する拡張なんかもあるみたい。
参考 : https://qiita.com/dockurage/items/13b71f86c7ac92dfb4c

最終的に、全体のコードは以下のようになった。

続きを読む

Three.jsにて自作シェーダーを試す際のサンプル Part.1

Three.jsでカスタムシェーダーを扱う際の自分用メモ。
下の記事とか非常に参考になる 。
https://qiita.com/mebiusbox2/items/8a4734ab5b0854528789

Three.jsのライトとかそのヘルパー凄い使いやすいんだけど、その情報を自分のシェーダーに組み込む方法が分からなかったんで、そこらへん含めて調べてみた。
結論を言うと、以下のようなコードを基本にするとかなり楽に色々実験できそう。

ファイル構成main.html
main.js
js/
 └─three.js
   OrbitControls.js
   LoaderSupport.js
   OBJLoader2.js

サンプル : http://nktk-tech.com/example/three-template1/main.html

ソースコード

main.html<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8"/>
    <script src="js/three.js"></script>
    <script src="js/LoaderSupport.js"></script>
    <script src="js/OBJLoader2.js"></script>
    <script src="js/OrbitControls.js"></script>
    <script src="main.js"></script>

    <script id="vs" type="x-shader/x-vertex">
        // PIの定義などがあり、ライト情報を使うのに必要
        #include <common>
        // punctualLightIntensityToIrradianceFactor関数がライトの定義の読み込みに必要
        #include <bsdfs>
        // ライトの構造体、uniform変数などが定義されている
        #include <lights_pars_begin>

        varying vec4 fragColor;

        void main() {
            vec4 tempFragColor = vec4(0.0, 0.0, 0.0, 0.0);
            for (int i = 0; i < NUM_POINT_LIGHTS; i++) {
                vec4 vertexToLight = normalize(vec4(pointLights[i].position, 1.0) - modelViewMatrix * vec4(position, 1.0));
                tempFragColor += vec4(pointLights[i].color, 1.0) * max(dot(vertexToLight.xyz, normalMatrix * normal), 0.0);
            }
            fragColor = tempFragColor;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
         }
    </script>
    <script id="fs" type="x-shader/x-fragment">
        varying vec4 fragColor;
        void main() {
            gl_FragColor = fragColor;
        }
    </script>
</head>

<body>
    <canvas id="myCanvas"></canvas>
</body>

</html>
main.jswindow.addEventListener('load', init);

function init() {

    // サイズを指定
    const width = 960;
    const height = 540;

    // レンダラーを作成
    const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    renderer.setClearColor(new THREE.Color( 0.5, 0.5, 0.5 ))

    // シーンを作成
    const scene = new THREE.Scene();

    // カメラを作成
    const camera = new THREE.PerspectiveCamera(45, width / height);
    camera.position.set(0, 5, +10);

    // マウスドラッグによるコントーロールを有効化
    const controls = new THREE.OrbitControls( camera );

    // 点光源追加
    const light = new THREE.PointLight( 0xffffee );
    light.position.set( 5, 3, 5 );
    scene.add( light );

    const sphereSize = 1;
    const pointLightHelper = new THREE.PointLightHelper( light, sphereSize );
    scene.add( pointLightHelper );

    // モデルのロードが終わった際のコールバック
    const callbackOnLoad = (event) => {
        let rootNode = event.detail.loaderRootNode;
        rootNode.children[0].material = new THREE.ShaderMaterial({
            uniforms: THREE.UniformsLib['lights'],
            vertexShader: document.getElementById('vs').textContent,
            fragmentShader: document.getElementById('fs').textContent,
            lights: true,
        });
        scene.add(rootNode);
        renderer.render(scene, camera);
        tick();
    };

    // .obj形式のモデルをロード
    const objLoader = new THREE.OBJLoader2();
    objLoader.setUseIndices(true);
    objLoader.load( 'model/teapot.obj', callbackOnLoad, null, null, null, false );

    // 毎フレーム時に実行されるループイベント
    function tick() {
        controls.update();
        renderer.render(scene, camera); // レンダリング
        requestAnimationFrame(tick);
    }

}
続きを読む