追記依頼欄にて思いつきを開陳してしまいましたが、多少なりとも方法検討のご参考になればと思い、スクリーンスペースUVマップ案について試してみました。
実験用プロジェクトは.NET Core 3.1とOpenTK 4.3.0で作成し、半径2の球体の-Z側だけを切り出した半球モデルにテクスチャを投影するというシチュエーションにしました。
半球の描画メソッドは下記のような流れです。テクスチャだののリソース管理のため別途作ったクラスなどを使用しておりますが、字数が足りませんので省略いたしました。ご容赦ください。
C#
1 public void Draw (
2 Matrix4 transform ,
3 [ NotNull ] Camera camera ,
4 [ NotNull ] DirectionalLight light ,
5 ITexture mainTexture = null ,
6 ITexture subTexture = null ,
7 bool debugPositionMap = false ,
8 bool debugUvMap = false ,
9 bool ignoreValidity = false ,
10 Shader alternativeShader = null )
11 {
12 // 座標変換用の行列を計算
13 var normalMatrix = Matrix3 . Transpose ( Matrix3 . Invert ( new Matrix3 ( transform ) ) ) ;
14 var mvpMatrix = transform * camera . ViewProjectionMatrix ;
15
16 // 座標マップ・UVマップを現在のビューポートに合わせてリサイズ
17 this . ResizeIfNeeded ( ) ;
18
19 // 座標マップを0でクリア
20 GL . BindFramebuffer ( FramebufferTarget . Framebuffer , this . positionMap . Name ) ;
21 GL . ClearColor ( 0.0f , 0.0f , 0.0f , 0.0f ) ;
22 GL . Clear ( ClearBufferMask . ColorBufferBit | ClearBufferMask . DepthBufferBit ) ;
23
24 // 座標マップをレンダリング、結果を取得
25 this . positionMapShader . Use ( ) ;
26 GL . UniformMatrix4 ( 0 , false , ref mvpMatrix ) ;
27 GL . BindVertexArray ( this . vertexArray ) ;
28 GL . DrawElements ( PrimitiveType . Triangles , this . indexCount , DrawElementsType . UnsignedInt , IntPtr . Zero ) ;
29 GL . BindVertexArray ( 0 ) ;
30 GL . ReadPixels (
31 0 ,
32 0 ,
33 this . currentViewport [ 2 ] ,
34 this . currentViewport [ 3 ] ,
35 PixelFormat . Rgba ,
36 PixelType . Float ,
37 this . positionMapData ) ;
38 GL . BindFramebuffer ( FramebufferTarget . Framebuffer , 0 ) ;
39
40 // 描画が行われたピクセルについて座標を取得、UV計算関数に通して
41 // 結果をUVマップに書き込む
42 Array . Clear ( this . uvMapData , 0 , this . mapDataLength ) ;
43 var functionCalls = 0 ;
44 var positionData = this . positionMapData ;
45 var uvData = this . uvMapData ;
46 var function = this . Function ;
47 var rangePartitioner = Partitioner . Create ( 0 , this . mapDataLength ) ;
48 Parallel . ForEach (
49 rangePartitioner ,
50 ( range , _ ) = >
51 {
52 var calls = 0 ;
53 var ( fromIndex , toIndex ) = range ! ;
54 for ( var i = fromIndex ; i < toIndex ; i ++ )
55 {
56 if ( positionData [ i ] . W > 0.0f )
57 {
58 uvData [ i ] . Xyz = function ( positionData [ i ] . Xyz ) ;
59 calls ++ ;
60 }
61 }
62 Interlocked . Add ( ref functionCalls , calls ) ;
63 } ) ;
64 this . TotalFunctionCalls += functionCalls ;
65
66 // UVマップデータをアップロードする
67 GL . BindTexture ( this . uvMap . Target , this . uvMap . Name ) ;
68 GL . TexSubImage2D (
69 this . uvMap . Target ,
70 0 ,
71 0 ,
72 0 ,
73 this . currentViewport [ 2 ] ,
74 this . currentViewport [ 3 ] ,
75 PixelFormat . Rgba ,
76 PixelType . Float ,
77 this . uvMapData ) ;
78 GL . BindTexture ( this . uvMap . Target , 0 ) ;
79
80 // 座標マップを全画面に描画(デバッグ用)
81 if ( debugPositionMap )
82 {
83 this . quadShader . Use ( ) ;
84 GL . ActiveTexture ( TextureUnit . Texture0 ) ;
85 GL . BindTexture ( this . positionMap . ColorBufferTarget , this . positionMap . ColorBufferName ) ;
86 GL . Uniform1 ( 0 , 0 ) ;
87 GL . Disable ( EnableCap . DepthTest ) ;
88 this . quadModel . Draw ( ) ;
89 GL . Enable ( EnableCap . DepthTest ) ;
90 return ;
91 }
92
93 // UVマップを全画面に描画(デバッグ用)
94 if ( debugUvMap )
95 {
96 GL . Disable ( EnableCap . DepthTest ) ;
97 this . quadModel . Draw ( this . uvMap ) ;
98 GL . Enable ( EnableCap . DepthTest ) ;
99 return ;
100 }
101
102 // 画面上に本番レンダリングを行う
103 ( alternativeShader ? ? this . defaultShader ) . Use ( ) ;
104 GL . UniformMatrix4 ( 0 , false , ref mvpMatrix ) ;
105 GL . UniformMatrix3 ( 1 , false , ref normalMatrix ) ;
106 GL . Uniform3 ( 2 , - light . Direction ) ;
107 GL . Uniform3 ( 3 , light . Intensity ) ;
108 if ( mainTexture != null )
109 {
110 GL . ActiveTexture ( TextureUnit . Texture0 ) ;
111 GL . BindTexture ( mainTexture . Target , mainTexture . Name ) ;
112 GL . Uniform1 ( 4 , 0 ) ;
113 }
114 if ( subTexture != null )
115 {
116 GL . ActiveTexture ( TextureUnit . Texture1 ) ;
117 GL . BindTexture ( subTexture . Target , subTexture . Name ) ;
118 GL . Uniform1 ( 5 , 1 ) ;
119 }
120 GL . ActiveTexture ( TextureUnit . Texture2 ) ;
121 GL . BindTexture ( this . uvMap . Target , this . uvMap . Name ) ;
122 GL . Uniform1 ( 6 , 2 ) ;
123 GL . Uniform1 ( 7 , ignoreValidity ? 1.0f : 0.0f ) ;
124 GL . BindVertexArray ( this . vertexArray ) ;
125 GL . DrawElements ( PrimitiveType . Triangles , this . indexCount , DrawElementsType . UnsignedInt , IntPtr . Zero ) ;
126 GL . BindVertexArray ( 0 ) ;
127 }
座標マップ生成用シェーダーは下記の通りであり、モデルの頂点座標をフレームバッファに出力しています。出力先はRGBAの4チャンネルで、Aは0でクリアしておいて描画時に1を書き込みました。後のUVマップ作成時には、Aが0のピクセルはスキップすることにしました。
GLSL
1 # version 430 core
2 layout ( location = 0 ) in vec3 modelPosition ;
3
4 layout ( location = 0 ) uniform mat4 modelViewProjectionMatrix ;
5
6 out vec3 localPosition ;
7
8 void main ( )
9 {
10 localPosition = modelPosition ;
11 gl_Position = modelViewProjectionMatrix * vec4 ( modelPosition , 1.0 ) ;
12 }
GLSL
1 # version 430 core
2 layout ( location = 0 ) out vec4 fragColor ;
3
4 in vec3 localPosition ;
5
6 void main ( )
7 {
8 fragColor = vec4 ( localPosition , 1.0f ) ;
9 }
本番レンダリング用シェーダーは下記の通りで、vec3 uv = texture(uvMap, gl_FragCoord.xy).xyz;
といった具合にUVを取得しています。vec3
を使っておりますが、後述のUV計算関数の仕様を「Vector3
を引数としVector3
を返す」としたことによります。返されるVector3
のX、YがテクスチャUVで、ZはUVが有効であれば1、無効なら0となるフラグの役割になっています。
GLSL
1 # version 430 core
2 layout ( location = 0 ) in vec3 modelPosition ;
3 layout ( location = 1 ) in vec3 modelNormal ;
4 layout ( location = 2 ) in vec2 modelUv ;
5
6 layout ( location = 0 ) uniform mat4 modelViewProjectionMatrix ;
7 layout ( location = 1 ) uniform mat3 modelNormalMatrix ;
8
9 out vec3 worldNormal ;
10 out vec2 texCoord ;
11
12 void main ( )
13 {
14 worldNormal = modelNormalMatrix * modelNormal ;
15 texCoord = modelUv ;
16 gl_Position = modelViewProjectionMatrix * vec4 ( modelPosition , 1.0 ) ;
17 }
GLSL
1 # version 430 core
2 layout ( location = 0 ) out vec4 fragColor ;
3
4 layout ( location = 2 ) uniform vec3 worldLightDirection ;
5 layout ( location = 3 ) uniform vec3 worldLightIntensity ;
6 layout ( location = 4 ) uniform sampler2D mainTexture ;
7 layout ( location = 5 ) uniform sampler2D subTexture ;
8 layout ( location = 6 ) uniform sampler2DRect uvMap ;
9 layout ( location = 7 ) uniform float ignoreValidity ;
10
11 in vec3 worldNormal ;
12 in vec2 texCoord ;
13
14 void main ( )
15 {
16 vec3 uv = texture ( uvMap , gl_FragCoord . xy ) . xyz ;
17 uv . z = uv . z + ( 1.0 - uv . z ) * ignoreValidity ;
18 vec3 albedo = mix ( texture ( mainTexture , texCoord ) . rgb , texture ( subTexture , uv . xy ) . rgb , uv . z ) ;
19 vec3 color = ( dot ( worldLightDirection , normalize ( worldNormal ) ) + 1.0 ) * 0.5 * worldLightIntensity * albedo ;
20 fragColor = vec4 ( color , 1.0f ) ;
21 }
通常のレンダリングでは下図のような映像が得られる状態において...
今回の描画フローでは、まず座標マップが作成されます。可視化すると下図のようになりますが、実験に使った半球はZ座標が負の頂点ばかりなので、色としては赤と緑だけの図になってしまいました。
これに対して適用するUV計算関数には、下記のような3種類を試してみました。UVの有効・無効については、いずれもさしあたり求めたUVが(0, 0)~(1, 1)の範囲内ならば有効、さもなければ無効とすることにしました。
C#
1 private static Vector3 F1 ( Vector3 position )
2 {
3 // -Z方向の平行投影
4 // 頂点座標(x, y)の(-1, -1)~(1, 1)をUV座標(0, 0)~(1, 1)にマッピング
5 var ( x , y , _ ) = position ;
6 var u = ( x * 0.5f ) + 0.5f ;
7 var v = ( y * 0.5f ) + 0.5f ;
8 var w = ( 0.0f <= u ) && ( u <= 1.0f ) && ( 0.0f <= v ) && ( v <= 1.0f ) ? 1.0f : 0.0f ;
9 return new Vector3 ( u , v , w ) ;
10 }
11
12 private static Vector3 F2 ( Vector3 position )
13 {
14 // -Z方向を正距円筒図法で展開
15 // 方角(phi, theta)の(-π/4, -π/4)~(π/4, π/4)をUV座標(0, 0)~(1, 1)にマッピング
16 var ( x , y , z ) = position ;
17 var c = Math . Sqrt ( ( x * x ) + ( z * z ) ) ;
18 var theta = Math . Atan2 ( y , c ) ;
19 var phi = Math . Atan2 ( x , - z ) ;
20 var u = ( float ) ( ( ( 2.0 * phi ) / Math . PI ) + 0.5 ) ;
21 var v = ( float ) ( ( ( 2.0 * theta ) / Math . PI ) + 0.5 ) ;
22 var w = ( 0.0f <= u ) && ( u <= 1.0f ) && ( 0.0f <= v ) && ( v <= 1.0f ) ? 1.0f : 0.0f ;
23 return new Vector3 ( u , v , w ) ;
24 }
25
26 private static Vector3 F3 ( Vector3 position )
27 {
28 // ピクセルの方角と(0, 0, -1)のなす角を2倍してタンジェントを求め、
29 // 頂点座標のx, yをタンジェントに応じて遠くへ飛ばす変換
30 // 飛ばされた座標(s, t)の(-1, -1)~(1, 1)をUV座標(0, 0)~(1, 1)にマッピング
31 var ( x , y , z ) = ( ( Vector3d ) position ) . Normalized ( ) ;
32 var ( s , t ) = new Vector2d ( x , y ) . Normalized ( ) * Math . Tan ( 2.0 * Math . Acos ( - z ) ) ;
33 var u = ( float ) ( ( s * 0.5 ) + 0.5 ) ;
34 var v = ( float ) ( ( t * 0.5 ) + 0.5 ) ;
35 var w = ( 0.0f <= u ) && ( u <= 1.0f ) && ( 0.0f <= v ) && ( v <= 1.0f ) ? 1.0f : 0.0f ;
36 return new Vector3 ( u , v , w ) ;
37 }
F1
は単純な平行投影で、行列としても表現可能な部類です。生成されたUVマップ(有効UVの領域には青色が乗っています)、マップに基づいてテクスチャを投影した結果、およびUVの有効・無効を無視してテクスチャを投影した結果はそれぞれ下図のようになりました。
一方、F2
は緯度・経度に基づいてマッピングしており、テクスチャは下地のグリッド模様に沿って貼り付けられました。
F3
は-Z方向に対して45°をなす方角が特異点になっており、45°に近づくほどUV座標の絶対値が大きくなっていきます。
45°を超えると空間が裏返り、90°付近では再びUVが(0, 0)~(1, 1)の範囲におさまります。
UV座標の絶対値が大きいほどテクスチャ貼り付けの精度は落ちるでしょうが、今回の例のように絶対値の大きい領域が小さくごちゃごちゃした状態になるマッピングであれば、粗は目立たないだろうと思います。
ですが、UV座標の飛びがあるとその周辺は汚くなってしまうかもしれません。F3
が下記のように常に0~1のUVを返すような実装だった場合...
C#
1 private static Vector3 F3 ( Vector3 position )
2 {
3 var ( x , y , z ) = ( ( Vector3d ) position ) . Normalized ( ) ;
4 var ( s , t ) = new Vector2d ( x , y ) . Normalized ( ) * Math . Tan ( 2.0 * Math . Acos ( - z ) ) ;
5 var ud = ( s * 0.5 ) + 0.5 ;
6 var vd = ( t * 0.5 ) + 0.5 ;
7 var u = ( float ) ( ud - Math . Floor ( ud ) ) ;
8 var v = ( float ) ( vd - Math . Floor ( vd ) ) ;
9 var w = 1.0f ;
10 return new Vector3 ( u , v , w ) ;
11 }
下図のようにUVの反復境界が無数に生じることになり、境界をまたぐ地点ではUV座標が大きく飛ぶため、そこだけミップマップレベルが大きくなってしまいます。結果として反復境界に見苦しいアーティファクトが生じてしまいました。
速度に関してですが、何とか50FPS台を達成していたものの、だいぶきつめな印象でした。
今回使ったのはIntel Core i3-4030U、Intel HD Graphics 4400の旧式廉価ノートPC、ウィンドウの解像度は640×480、構図は先に図示したような視点で、これら条件でのUV計算関数の実行回数はおよそ毎秒350万~370万回でした。
fanaさんやikadzuchiさんのおっしゃるような、頂点にデータを埋め込む方針で問題なさそうでしたら、やはりそちらがよさそうですね。それなら毎フレーム大量にUV計算関数を実行したり、テクスチャデータ転送で帯域を消費したりする必要もないでしょうから、ずっと高速に実現できそうに思います。
また別の思いつきとして、投影したい画像を事前に処理してポリゴンへの投影に適した形に変形してしまう手があるかもしれません。
関数Fに入力されるVの大きさは関係なく方角だけが計算に使われるのであれば、画像を球面上にマッピングすることができるだろうと思いますので、キューブマップテクスチャに変換できるんじゃないでしょうか?
画像がめったに変化しないのであれば画像自体をキューブマップに、Webカメラ映像みたいに画像が常時変化する場合はUVマップをキューブマップに変換してしまえるように思います。
もし関数Fが毎フレーム変化するようなら利点はないでしょうが、そうでなければだいぶ効率的に描画できそうですね。