CSSスプライトアニメーションをGPUレンダリングさせる
※内容は以前書いたこちらの記事と同じでさらに文章に校正を加えたものになります。jsdo.itをデモとして使っていたのですがjsdo.itがクローズになってデモが動作しなくなったのでデモをcodepenに移したためです。
CSSスプライトアニメーションをGPUレンダリングさせる - Qiita
スプライトアニメーションと再描画
CSSでスプライト画像の連番アニメーションを実装する場合、background-positionをずらす方法を使っていました。しかし、同じ画面内でサイズの大きい画像のスプライトアニメーションをいくつも動かしたい場面があり、パフォーマンスがネックになりました。
実際、background-positionのcss animationはGPUレンダリングではないので再描画処理が走ってしまいます。chromeのデバッガーツールでRendering
のPaint Flashing
を見ると、このように再描画処理が走っているのがわかります。
@keyframes sprite-image { 0% { background-position: 0 0; } 100% { background-position: 0 -1064px; } } .sprite { width: 400px; height: 266px; margin: 15px; background-image: url(https://user-images.githubusercontent.com/947953/77824893-2f663480-7149-11ea-873b-9fda24fe1022.jpg); background-size: 400px 1064px; animation: sprite-image 1s steps(4) infinite; }
css transformを使うことで再描画を防ぐ
そこでGPU処理が適用されるcss transformを使った方法を試してみました。before要素の大きさをスプライト画像と同じ大きさにし、背景にスプライト画像をあて、animationでtransformを操作することにより、background-positionの時と同じDOM構造のままcssを変更するのみで実現できました。
先ほどと同じようにchromeのデバッガーツールを見てみると、再描画が走っていないことがわかります。この方法によって、background-positionを使った時よりも描画負荷を軽減することができました。
こちらがデモになります。
See the Pen 【CSS】Sprite Animation by GPU Rendering by takumifukasawa (@takumifukasawa) on CodePen.
↓ cssの抜粋です。
@keyframes sprite-image { 0% { transform: translate(0, 0); } 100% { transform: translate(0, -100%); } } .sprite { overflow: hidden; width: 400px; height: 266px; margin: 15px; // animationでtransformのtranslateを動かす &:before { content: ""; display: block; width: 400px; height: 1064px; background-image: url(https://user-images.githubusercontent.com/947953/77824893-2f663480-7149-11ea-873b-9fda24fe1022.jpg); background-size: 400px 1064px; animation: sprite-image 1s steps(4) infinite; } }
【WebGL】Google Chrome の Hardware Acceleration 無効時の挙動
WebGL開発をする際に、Chromeのハードウェアアクセラレーションを無効にしたときでもWebGLコンテンツを動作させたい機会があり、挙動を調べる機会があったのでメモとして残しておきます。
設定方法の変更
Chrome の 設定
→ 詳細設定
→ ハードウェア アクセラレーションが使用可能な場合は使用する
で切り替えることができます。
切り替える際はブラウザを再起動する必要があります。
有効・無効時のブラウザの機能の違い
chrome://gpu
にアクセスすることでグラフィック回りの使える機能の状態を確認することができます。
↓ ハードウェアアクセラレーション有効時
↓ ハードウェアアクセラレーション無効時
無効時のWebGL
部分を見るとSoftware only, hardware acceleration unavailable
の状態になっています。ハードウェアアクセラレーションは使えなくなっていますが(= GPUを使わない)なんらかのソフトウェアは動作しているようで、そのソフトウェアとはSwiftShaderを指しています。
Chrome と CPUレンダラー
ChromeにはSwiftShaderというCPUベースのレンダラーが組み込まれています。
SwiftShader is a high-performance CPU-based implementation of the Vulkan, OpenGL ES, and Direct3D 9 graphics APIs12. Its goal is to provide hardware independence for advanced 3D graphics.
つまりSwiftShaderはVulkanやOpenGL ESなどのグラフィックAPI相当の処理をCPUで動作させてくれるソフトウェアということになります。
Googleのこちらの記事によると2009年から組み込まれているようです。
2009 年以来、Chrome はハードウェア アクセラレーションによるレンダリングを完全にはサポートしていないシステムでの 3D レンダリングに SwiftShader を使用しています。WebGL などの 3D コンテンツは GPU 向けに記述されていますが、ユーザーの端末にはこのようなコンテンツを実行できるだけのグラフィック ハードウェアが搭載されていない場合もあります。
WebGLの挙動
ハードウェアアクセラレーションをオフにしてGPUが使えない状態でも SwiftShader が動作することにより、WebGLを使用したコンテンツも表示することができます。ただし、処理速度はどうしてもGPU動作時よりも劣ります。
処理速度
ハードウェアアクセラレーションのオプションを有効・無効にしたときのWebGLコンテンツのFPSを比較してみました。参考にしたのはthree.jsのこちらのサンプルです。
https://threejs.org/examples/#webgl_buffergeometry
- 環境
- OS: windows10
- CPU: Intel(R) Core(TM) i7-9700K
- GPU: GeForce RTX 2070 SUPER
↓ 有効時: 約60FPS
↓ 無効時: 約20FPS
処理の内容にももちろん大きく左右されると思いますが、ハードウェアアクセラレーションを無効にしてもWebGLが使われているページがCPUのみの動作で20FPS近く出るとは驚きでした。見た目にも違いは見受けられませんでした。
注意点
調べきれてはいないのですが、SwiftShaderはあくまでもCPUレンダリングなので処理によってはGPUで動作していたときと同じ挙動・見た目にならないものもあるかもしれません。特にWebGLの拡張機能回りを使いたいときはあらかじめ動作確認をしておいた方が安全、と思いました。
【three.js】背面カリングを用いたアウトライン表現
リアルタイムレンダリングにおいてアウトラインを生成する方法はいくつかあります。 背面カリングを利用した方法や、ポストプロセスによる生成が主かと思います。
今回は前者の、背面カリングを使ったアウトライン表現を行ってみたいと思います。
デモ
最終的なデモはこちらになります。
↓ モデルを使った場合。色と境界線の太さを変えられるようにしています。
See the Pen BackFace Culling Outline: Model by takumifukasawa (@takumifukasawa) on CodePen.
↓ three.jsのTorusGeometryを使った場合。境界線用のメッシュ・境界線内用のメッシュの表示非表示を切り替えられるようにしています。
See the Pen BackFace Culling Outline by takumifukasawa (@takumifukasawa) on CodePen.
手法と流れ
- 描画したいモデルを複製する
- 一つは、頂点シェーダーで各頂点を法線方向にオフセットし(膨らまし)、境界線につけたい色で背面を描画する。もしくはあらかじめ各頂点を法線方向にオフセット済みのモデルを用意する
- もう一つは、任意のマテリアル(境界線の内側に表示したいマテリアル)で通常通りレンダリング
このようにすることで、境界線用に背面描画したモデルの上に、描画したいモデルが乗っかるように描画され、あたかも境界線がついたかのように見えるということになります。
こちらのGIFは、境界線用のMeshと境界線の内側用のMeshの表示・非表示を切り替えてみたものです。
境界線用のMeshは境界線のみを描画しているのではなくベタ塗りされ、その上に境界線内に描画したいMeshが乗っかっているようになっていることが分かるかなと思います。
three.jsの場合
three.jsの場合はこのような流れで実現することができます。
- 描画したいMeshを2つ用意する
- 片方のMeshには境界線用に背面描画のMaterialを割り当てる。このマテリアルはシェーダーを書く
- もう片方のMeshは任意の見た目のMaterial(境界線の内側に描画させたい見た目のマテリアル)
- 描画
コード
こちらが境界線描画回りのコードになります。上のサンプルのTorusGeometryの方から抜粋しました。
コード中にコメントで説明を書いていきたいと思います。
... const outlineVertexShader = ` void main() { // 法線方向に頂点を膨らませる // .04 は任意の膨らませたい大きさ vec3 _position = position + normal * .04; gl_Position = projectionMatrix * modelViewMatrix * vec4(_position, 1.); } `; const outlineFragmentShader = ` precision mediump float; void main() { // 境界線色として黒を指定 gl_FragColor = vec4(0., 0., 0., 1.); } `; // 境界線用の背面描画マテリアル const outlineMaterial = new THREE.ShaderMaterial({ vertexShader: outlineVertexShader, fragmentShader: outlineFragmentShader, side: THREE.BackSide, }); // 境界線の内側に描画する用のPBRマテリアル const surfaceMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // 描画したい形状 const torusGeometry = new THREE.TorusGeometry(1, 0.3, 16, 100); // 境界線用のMeshを作成しSceneに追加 const outlineMesh = new THREE.Mesh(torusGeometry, outlineMaterial); scene.add(outlineMesh); // 境界線の内側用のMeshを作成しSceneに追加 const innerMesh = new THREE.Mesh(torusGeometry, surfaceMaterial); scene.add(innerMesh); ...
こうしてみると、境界線用のコードはそこまで多くないことが分かります。
特徴・注意点
モデルを2回描画する必要がある
- 特に対策をしない場合は単純にポリゴン数が倍になる
- そのためポリゴン数が多いGeometryの場合には採用がしづらく、採用する場合はポリゴン数を減らしたモデルにするなどの対策が必要?になるのかなと思います
カメラの位置によって太さが変わる
- 太さを変えたくない場合は、カメラの位置によって膨らませる大きさを調整するか、ポストプロセスによる境界線の方法を用いる必要があります
- 見せ方によっては、カメラの位置によって太さが変わっても問題ない場合もあるかと思います。
輪郭以外の境界線のディティールを出しづらい
- 下のスクショのように、アボガドを横から見たときは種と身の境目に境界線が生じていません。また、種の左側に浮くように境界線が表示されているかと思います
- 折り目がついているところや段差がある部分(このアボガドの場合は種と身の境目)に境界線をつけやすくしたい場合はポストプロセスの方が向いています
- ポストプロセスであればシーン内の法線方向や色を用いた境界線の生成を行う方法をとることができるからです
【Unity】AzureKinectの環境構築・接続
AzureKinectを使う機会があったので、セットアップとミニマムな疎通確認までをまとめておきたいと思います。
3月18日現在、日本での発売はまだされていません。しかし、マイクロソフトの公式ブログによると3月中には発売可能になる予定とのことです。
Azure Kinect Development Kit を日本、ドイツ、イギリスにて2020年3月より発売! - Windows Blog for Japan
環境
ハードウェア要件はこちらに記載されていました。
手順
1. Azure Kinect SDK をダウンロード
- https://docs.microsoft.com/ja-jp/azure/Kinect-dk/sensor-sdk-download
- Microsoft installer をクリックしてダウンロード
2. SDKのインストール
ダウンロードしたインストーラーを開いてインストール手順に従います
3. Unityプロジェクトを作成
Unity 2019.2.18f で作成しました。パイプラインはビルトインパイプラインにしました。
4. NuGet をUnityPackageからインストール
- ここから version 2.0 の UnityPackage を選択
- UnityPackage直リンクはこちら
5. Nuget UnityPackage を作成したプロジェクトにインストール
UnityPackageをドラッグ・ドロップでUnityにインストールします。
6. NuGetでSDKのモジュールをインストール
menuの
NuGet -> Manage Nuget Packages
を開くNuget のインストール直後だと menuにNuGetが表示されない場合があったので、そのときはUnityを再起動してみます
Microsoft.Azure.Kinect.Sensor
SDKをインストール- 検索バーで
AzureKinect
を入れると上の方に出てきます
- 検索バーで
7. dllのインポート
1でインストールしたAzureKinectSDKからdllを2つ(depthengine_2.0.dll, k4a.dll
)、unityのAssets/Plugins
にドラッグ&ドロップしてインポート
Assets 以下に Pluginsフォルダがなければ作成しておきます
dllのpathは、SDKのインストールのパスを特に変えていない場合はこちらになるはずです
C:\Program Files\Azure Kinect SDK v1.3.0\sdk\windows-desktop\amd64\release\bin
8. AzureKinectをPCにつなぐ
AzureKinectの使用には、ACアダプターと、PCとつなぐUSBの、2つを最低限つなぐ必要がなります。
複数台接続の際はデイジーチェーンでのタイムスタンプの同期のためオーディオケーブルをつなぐことになります。
9. 接続確認用のスクリプトを作成
10. Playした時の挙動確認
エラーなどなくログが表示されている(シリアル番号
とOpen
)かつ、LEDが赤く点灯することを確認します。
11. 止めたときの挙動確認
Close
というログが出るようになっています。
AzureKinectの接続を解除する際は、状態破棄のため Dispose 処理を忘れずに呼ぶ必要があるようです。
以上まで問題なく動作すれば、AzureKinectとUnityの接続はひとまず無事に上手くいっていることが分かります。
注意点
Playした際に missing assembly reference
のエラーが出ることがあります。
その場合は AzureKinect SDK が依存している?モジュールが入っていないためなので、 エラーの出ているモジュールを Nuget でインストールします。
【Unity】CSVを読み込むC#クラス
CSVファイルを読み込んでパースするクラスを、MonoBehaviourではなくC#クラスとして欲しい場面があったので作成してみました。
まず、コードの全文はこちらです。
Assets/Resources/
以下に入っているCSVファイルを読み込みます。
例えば Assets/Resources/test.csv
が
a, b, c, d e, f, g, h
というデータになっている場合、 CSVReader.getPath("test")
を呼ぶことで読み込むことができます。結果はこのようになります。
// 文字列の配列のリスト [ ["a", "b", "c", "d"], ["e", "f", "g", "h"] ]
string.Split 関数を呼ぶ時に System.StringSplitOptions.RemoveEmptyEntries
を指定することで空白の文字列を削除していますが、こちらは扱いたいデータに応じて変えたり、引数で分割方法を指定できるようにするなどの使用方法も有りうると思います。
【SparkAR】リムライト風シェーダー
SparkARは、簡単なシェーディングであればパッチエディターでノードを組み合わせることで実現できるのですが、それだけでは難しい表現の場合はJSでスクリプトを書く必要があります。
なぜスクリプトかと言うと、SparkARではシェーダーを直接書くことができず、SparkARが提供しているモジュールを通してシェーダーの用な計算をスクリプトで書いていく必要があるためです。
リムライト風シェーダーもその一つだったので、メモとして残しておきたいと思います。
プロジェクトはこちらに置きました。SparkARのバージョンはv83です。
ソースの抜粋はこちらになります。
フレネルの計算や考え方はシェーダーで書くときと同じなのですが、ライトの向きをスクリプトで取得することができなさそうだったので、法線ベクトルと視線ベクトルの内積を使う形にしています。
const Shaders = require('Shaders'); const Reactive = require('Reactive'); const Materials = require('Materials'); // vertexのattributeを取得 const localPosition = Shaders.vertexAttribute({ variableName: Shaders.VertexAttribute.POSITION }); const localNormal = Shaders.vertexAttribute({ variableName: Shaders.VertexAttribute.NORMAL }); // 座標変換用の行列を取得 const mvMatrix = Shaders.vertexTransform({ variableName: Shaders.BuiltinUniform.MV_MATRIX }); const normalMatrix = Shaders.vertexTransform({ variableName: Shaders.BuiltinUniform.NORMAL_MATRIX }); const normal = normalMatrix.mul(localNormal).normalize(); const position = Reactive.pack4( localPosition.x, localPosition.y, localPosition.z, 1. ); const mvPosition = mvMatrix.mul(position); const viewPosition = mvPosition.mul(-1); const viewDir = Reactive.normalize(Reactive.pack3(viewPosition.x, viewPosition.y, viewPosition.z)); const dotNV = Reactive.dot(viewDir, normal); const fresnel = Reactive.pow(Reactive.sub(1, Reactive.clamp(dotNV, 0, 1)), 2); const emissiveColor = Reactive.pack4( fresnel, fresnel, fresnel, 1. ); // Emissiveスロットに色を割り当てる const material = Materials.get('RimLight'); material.setTextureSlot(Shaders.DefaultMaterialTextures.EMISSIVE, emissiveColor);
【Javascript】ブラウザでシェーダーを使わずにレイマーチングをしてみる
はじめに
ブラウザでレイマーチングを行うには、WebGLを使ってシェーダーで書くのが一般的なやり方かと思います。むしろ、高いFPSを実現するにはそれ以外の方法は実質難しいはずです。具体的には、描画したい範囲に板ポリを貼りそのマテリアルにレイマーチングを行うシェーダーを割り当てる方法です。
ピクセルごとに複数回レイを飛ばしてシーンを構築するような処理そのものは、負荷は大きいですがGPUの得意とするところで、CPUで1ピクセルずつ順番に計算するよりも圧倒的に高速だからです。
ただ、処理速度は遅くなりますがCPUでは実現できないという訳ではありません。負荷はともかくとして、レイマーチングそのものはあくまでも手法で、処理自体はGPUでもCPUでも実現そのものは出来るはず・・・と思っていたのですが、どのぐらい負荷に差が出るのかまでは予測できなかったので、実際にやってみました。
作ったもの
比較のためにこの2つを作ってみました。見た目は同じです。球にディレクショナルライトが当たり、ライトの位置が回転するように動いています。
環境
CPU版
デモ
Typescriptで書いてみました。Context2Dを使っています。
ベクトルや行列周りの処理は、今回のデモで必要になる最低限の計算を自分で作ってみました。
See the Pen Canvas2D Raymarching by takumifukasawa (@takumifukasawa) on CodePen.
コード
HTML
<canvas id="canvas" width="200" height="200"></canvas>
canvas { display: block; }
const ITERATION_NUM: number = 16; const NORMAL_EPS: number = 0.0001; const STOP_THRESHOLD: number = 0.0001; //--------------------------------------------------------------- // utils //--------------------------------------------------------------- function clamp(x: number, a: number, b: number): number { return Math.min(Math.max(x, a), b); } //--------------------------------------------------------------- // vector //--------------------------------------------------------------- class Vector2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } } class Vector3 { x: number; y: number; z: number; constructor(x: number, y: number, z: number): Vector3 { this.x = x; this.y = y; this.z = z; return this; } get magnitude(): number { return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); } static magnitude(Vector3: v): number { return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } get normalized(): Vector3 { const mag = this.magnitude; return new Vector3( this.x / mag, this.y / mag, this.z / mag ); } static normalize(v: Vector3): Vector3 { const mag = v.magnitude; return new Vector3( v.x / mag, v.y / mag, v.z / mag ); } normalize(): Vector3 { const mag = this.magnitude; return new Vector3( this.x / mag, this.y / mag, this.z / mag ); } static sub(a: Vector3, b: Vector3): Vector3 { return new Vector3( a.x - b.x, a.y - b.y, a.z - b.z ); } static dot(a: Vector3, b: Vector3): number { return a.x * b.x + a.y * b.y + a.z * b.z; } static cross(a: Vector3, b: Vector3): Vector3 { return new Vector3( a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x ); } static add(v1: Vector3, v2: Vector3): Vector3 { return new Vector3( v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, ); } static mul(v: Vector3, m: number): Vector3 { return new Vector3( v.x * m, v.y * m, v.z * m ); } } //--------------------------------------------------------------- // mat3 //--------------------------------------------------------------- class Mat3 { elem: number[3][3]; constructor(row1: Vector3, row2: Vector3, row3: Vector3) { this.elem = [ [ row1.x, row1.y, row1.z ], [ row2.x, row2.y, row2.z ], [ row3.x, row3.y, row3.z ], ]; } mul(v3: Vector3): Vector3 { return new Vector3( this.elem[0][0] * v3.x + this.elem[0][1] * v3.y + this.elem[0][2] * v3.z, this.elem[1][0] * v3.x + this.elem[1][1] * v3.y + this.elem[1][2] * v3.z, this.elem[2][0] * v3.x + this.elem[2][1] * v3.y + this.elem[2][2] * v3.z ); } } //--------------------------------------------------------------- // color //--------------------------------------------------------------- class Color { r: number; g: number; b: number; a: number; constructor(r: number, g: number, b: number, a: number = 255) { this.r = r; this.g = g; this.b = b; this.a = a; } add(color: Color): void { this.r = clamp(this.r + color.r, 0, 255); this.g = clamp(this.g + color.g, 0, 255); this.b = clamp(this.b + color.b, 0, 255); this.a = clamp(this.a + color.a, 0, 255); } } //--------------------------------------------------------------- // raymarch block //--------------------------------------------------------------- function scene(p: Vector3): number { return p.magnitude - 1; } function getCamera(ro: Vector3, rd: Vector3): Mat3 { const dir = Vector3.sub(rd, ro); const forward: Vector3 = dir.normalized; const right: Vector3 = Vector3.cross(forward, new Vector3(0, 1, 0)); const up: Vector3 = Vector3.cross(right, forward); return new Mat3(right, up, forward); } function getNormal(p: Vector3): Vector3 { const e: Vector2 = new Vector2(NORMAL_EPS, 0); const n: Vector3 = new Vector3( scene(Vector3.add(p, new Vector3(e.x, e.y, e.y))) - scene(Vector3.sub(p, new Vector3(e.x, e.y, e.y))), scene(Vector3.add(p, new Vector3(e.y, e.x, e.y))) - scene(Vector3.sub(p, new Vector3(e.y, e.x, e.y))), scene(Vector3.add(p, new Vector3(e.y, e.y, e.x))) - scene(Vector3.sub(p, new Vector3(e.y, e.y, e.x))) ); return n.normalized; } function raymarch(u: number, v: number): Color { const time = performance.now() / 1000, // [s] const uv: Vector2 = new Vector2(u, v); const ro: Vector3 = new Vector3( Math.cos(time) * 5, 0, Math.sin(time) * 5 ); const target: Vector3 = new Vector3(0, 0, 0); const fov: number = 1.5; const camera: Mat3 = getCamera(ro, target); const rd: Vector3 = camera.mul(new Vector3(uv.x, uv.y, fov).normalized); let depth: number = 0; let dist: number = 0; // raymarch for(let i: number = 0; i < ITERATION_NUM; i++) { dist = scene(Vector3.add(ro, Vector3.mul(rd, depth))); if(dist < STOP_THRESHOLD) { break; } depth += dist; } // no hit if(dist >= STOP_THRESHOLD) { return new Color(0, 0, 0, 255); } // hit const color: Color = new Color(0, 0, 0, 255); const position: Vector3 = Vector3.add(ro, Vector3.mul(rd, depth)); const normal: Vector3 = getNormal(position); const lightPos: Vector3 = new Vector3(-1, 1, 1); const lambert: float = Math.max(0, Vector3.dot(normal, lightPos.normalized)); const surfaceColor: Vector3 = Vector3.mul(new Vector3(255, 255, 255), lambert); color.add(new Color( surfaceColor.x, surfaceColor.y, surfaceColor.z )); return color; } //--------------------------------------------------------------- // main block //--------------------------------------------------------------- const canvas = document.getElementById('canvas'); const ctx = canvas.getContext('2d'); const width: number = canvas.offsetWidth; const height: number = canvas.offsetHeight; const stats = new Stats(); stats.domElement.style.position = 'fixed'; stats.domElement.style.top = '0px'; stats.domElement.style.left = '0px'; document.body.appendChild(stats.domElement); function draw() { stats.begin(); const imageData = ctx.createImageData(width, height); const data = imageData.data; for(let y = 0; y < height; y++) { for(let x = 0; x < width; x++) { const w = x; // flip y const h = height - y; const i = (x + y * width) * 4; const coord: Vector2 = new Vector2(w, h); const resolution: Vector2 = new Vector2(width, height); const color: Color = raymarch( (coord.x - width * .5) / Math.min(width, height), (coord.y - height * .5) / Math.min(width, height) ); data[i ] = color.r; data[i + 1] = color.g; data[i + 2] = color.b; data[i + 3] = color.a; } } ctx.putImageData(imageData, 0, 0); stats.end(); requestAnimationFrame(draw); } requestAnimationFrame(draw);
GPU版
せっかくなのでシェーダーで動作するコードも書いてみました。
こちらはShaertoyでのデモになります。
デモ
コード
#define EPS .0001 #define NORMAL_EPS .0001 const float stopThreshold = .0001; float scene(vec3 p) { return length(p) - 1.; } mat3 camera(vec3 o, vec3 t) { vec3 forward = normalize(t - o); vec3 right = cross(forward, vec3(0., 1., 0.)); vec3 up = cross(right, forward); return mat3(right, up, forward); } vec3 getNormal(vec3 p) { vec2 e = vec2(NORMAL_EPS, 0); return normalize( vec3( scene(p + e.xyy) - scene(p - e.xyy), scene(p + e.yxy) - scene(p - e.yxy), scene(p + e.yyx) - scene(p - e.yyx) ) ); } void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (fragCoord.xy - iResolution.xy * .5) / min(iResolution.x, iResolution.y); vec3 ro = vec3( cos(iTime) * 5., 0., sin(iTime) * 5. ); vec3 target = vec3(0.); float fov = 1.5; vec3 rd = camera(ro, target) * normalize(vec3(uv, fov)); // raymarching float depth = 0.; float dist = 0.; for(int i = 0; i < 64; i++) { dist = scene(ro + rd * depth); if(dist < stopThreshold) { break; } depth += dist; } // no hit if(dist >= stopThreshold) { fragColor = vec4(vec3(0.), 1.); return; } vec3 color = vec3(0.); vec3 position = ro + rd * depth; vec3 normal = getNormal(position); // directional lighting vec3 lightPos = vec3(-1., 1., 1.); float lambert = max(0., dot(normal, normalize(lightPos))); color += lambert * vec3(1.); fragColor = vec4(color, 1.); }
比較
CPU版をGPU版と同じ条件にするとFPSが1ぐらいしか出なかったので解像度などを下げました。
- CPU版
- レイを進める最大回数: 16回
- 解像度[px]: 200 x 200
- FPS: 20FPS前後
結果
やはりレイマーチングそのものはCPUでも実現はできましたが、処理速度はGPUには到底及ばないぐらいの差が開きました。