🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
OpenGL

OpenGLは、プラットフォームから独立した、デスクトップやワークステーション、モバイルサービスで使用可能な映像処理用のAPIです。

GLSL

GLSL (OpenGL Shading Language) はC言語をベースとしたシェーディング言語です。

Q&A

解決済

2回答

2403閲覧

ポリゴンの一部分のみにテクスチャを貼る方法

fana

総合スコア11990

OpenGL

OpenGLは、プラットフォームから独立した、デスクトップやワークステーション、モバイルサービスで使用可能な映像処理用のAPIです。

GLSL

GLSL (OpenGL Shading Language) はC言語をベースとしたシェーディング言語です。

0グッド

0クリップ

投稿2020/11/26 05:38

編集2020/11/26 06:08

ポリゴンの一部分にのみテクスチャを貼りたいのですが,どのようにして実現できますか?


■状況:
モデルの表面上の3次元座標Vに対するテクスチャ座標(u,v)を決定する関数Fが実行時に与えられます.
テクスチャ座標を
(u,v) = F( V )
として計算しますが,関数FはVの値域全域で計算を行えるわけではなく,Vの値によっては(u,v)の計算自体を行うことができません.


■質問内容:
「(u,v)を計算できる/できない」の境界線がポリゴン内を通るときに,計算できる側の領域にのみテクスチャを貼りたいのですが,
関数F自体をシェーダ側で扱わずにこのことを実現する方法があればご教示いただきたいです.

頂点のデータとして(u,v)を与える場合,(u,v)をFで計算できない頂点には何かしらのダミーの(u,v)値を与えることになるでしょうが,このダミー値を何か工夫するとか,頂点に他の何らかの補足情報を持たせる等して達成できるでしょうか?


■試したこと:
方法論自体がわからない状態にありますので,何かを具体的に試すことはできていません.

以下のような手段を実装できれば問題に対応できるように思いますが,(私の技量に対しての)難易度の面で問題がありそうですので,
前記の質問内容のような方向での方法があれば,と…

  • F(V)の計算をバーテックスシェーダ内で行うならば,その場で 貼る/貼らない を判断すればよい.

→Fをシェーダ用のコードとして与えることができる場合ならばシンプルな解だと思う.

  • Fが与えられた時点で,モデルを「(u,v)を計算できる/できない」の境界線に沿って切断して,テクスチャを貼るモデルと貼らないモデルに分割する.

→処理実装難易度が高い…

  • 質問内容ような方向での話として,

ポリゴン平面上に引かれる境界線をこのポリゴン平面上で定義されるある固定自由度の関数g(t)=0として表現(近似)することで,gの符号により判定する…
(頂点毎のデータとして2次元の座標tを与える)
…的な方向の話をぼんやりと考えていますが,具体的な話にはできていません.

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Bongo

2020/11/26 10:56

関数Fはどのような形で与えられるでしょうか?また、何か例をご提示いただくことは可能でしょうか。 数式自体に違いはなく、関数で使われているパラメーターがちょっと違う程度なら、関数はシェーダー上で実装してパラメーターをuniform変数として与えてやることができるでしょう(ですが、おそらくそうではないんでしょうね...)。 あまり複雑でない数式を文字列として受け取るのであれば、シェーダー側の関数実装部分にはダミーの文字列を仕込んでおいて、何とか数式をパースしてGLSLの文字列にしてしまい、プリプロセッサがマクロを展開するのと同じようなイメージでGLSL数式をシェーダーコード中に埋め込んでやることで、シェーダー側へ数式を持ち込めるかもしれません。 この手が使えそうなら、おそらくおっしゃるようにシンプルに解決できそうです。埋め込み先もフラグメントシェーダーであってもかまわないでしょう。そちらならピクセル単位で関数を評価でき、かなりきれいな境界線にできそうですね。 関数Fが「Vを受け取って(u,v)を返す」ということしか規定されておらず、どんな中身のものが与えられるか予想できないとなるとやっかいですね。第2案とか第3案の方針で進めないといけないかもしれません。 モデル分割案についてですが、ポリゴン内を通過する境界線は曲線になる可能性があるでしょうか?曲線になりうるとしたら、分割線はおそらくある程度の粒度の折れ線にせざるを得ないでしょうから、多少はマシになるでしょうがやはり有効なUVと無効なUVが入り混じったポリゴンが生じてしまいそうです。 座標t案も面白そうですが、ポリゴンの内部の各フラグメントは各頂点のtが線形補間された値を持つということでしょうかね?tを頼りにくっきりした境界直線を描かせることができそうですが、UVについてはどうでしょうかね...正常なUVとダミーUVの間で線形補間されて、テクスチャがひずんでしまいそうな気も...私の頭では第3案のイメージを掴みきれず、フワフワした意見で申しわけないです。ダミーUVになるべく自然な値を与えてやりたいですが、これもどうしたものでしょうか(有効なUVを外挿してUVを予測する?でもUV計算不能な領域へ外挿したとしてもまともなUVが出てくるのか?)... 有効なUVと無効なUVが入り混じったポリゴンが生じてしまうのはしょうがないものと妥協して、見栄えの不自然さの軽減を目指すのもありなんじゃないでしょうか。頂点数が増えてもかまわないなら、各ポリゴンを多数のポリゴンに細分割してしまえば、メッシュの密度が上がってけっこうきれいに見えるかもしれません。
ikadzuchi

2020/11/27 12:59

いくつか分からない点があります。 ・「計算自体を行うことができない」という状況が何を指すのか分かりません。コンピュータで計算できる形で式が与えられるのでしょうけれど、そこに「計算自体を行うことができない」座標Vを与えると具体的に何が起こりますか? ・計算を行うことができるか否かを、計算を行ってみることなく簡単に判定する方法はないのですか? ・「頂点のデータとして(u,v)を与える」ということは、F(V)は(少なくとも計算できる部分について)連続的という前提なのですか? (つまり十分に細かい間隔で値を与えて線形補間することが可能かどうか)
fana

2020/11/27 15:28 編集

(お二方からの話が入り混じる形になりますが) 関数Fに関して: > 関数Fが「Vを受け取って(u,v)を返す」ということしか規定されておらず、どんな中身のものが与えられるか予想できない に近いです. 関数Fは数式かもしれませんが,例えば,LUT的なデータを用いて決定するような"何らかの処理"となる可能性もあるかもしれません. 解析的な解自体が無い(とか,それを導出するのがつらい)ために数値計算で都度求めるかもしれません. (実装上のFの与えられ方としては,仮想関数だとか,dllの先に実装がある,みたいな形を想定しています.) > ・「計算自体を行うことができない」という状況が何を指すのか分かりません > ・計算を行うことができるか否かを、計算を行ってみることなく簡単に判定する方法はないのですか? 「計算結果が保証されるVの値域が制限されている」といった感じです. 範囲外のVの値で強引に計算させたならば,何かしらの値が出てくるとしても,それがまともな値かどうかはわかりません.(最悪の場合は0割り等が発生しても文句が言えないかもしれません.) なので,「与えられた保証範囲でのみFを用いた計算をする」といった運用になるかと思います. > F(V)は(少なくとも計算できる部分について)連続的という前提なのですか? そうですね.連続関数(あるいはそうだと思っても差し支えないようなもの)であって,ある程度の間隔内であれば線形補間した場合の誤差はまぁ無視できるだろう的な.
fana

2020/11/27 15:24 編集

境界線に関して: > ポリゴン内を通過する境界線は曲線になる可能性があるでしょうか? 一般には曲線でしょうが,ここはポリゴンをある程度小さく与えるならば,ほぼ直線に近いと考えてもよいだろう,と思います.(ポリゴンをその直線に近い線が2分割する形)
fana

2020/11/27 15:23

ダミーUVに関して: > 正常なUVとダミーUVの間で線形補間されて、テクスチャがひずんでしまいそうな気も... おっしゃるとおりで,ダミー値が補間に用いられてしまうのが悩みどころです. (この点が質問内容に含まれている感じ)
fana

2020/11/27 15:35

> 見栄えの不自然さの軽減を目指すのもあり 最終的に良い方法が無い状態で実装に臨まねばならない場合には,そのような方向にならざるを得ないだろう…と思います.
Bongo

2020/11/27 20:49

「計算結果が保証されるVの値域」というものはどのように決まるのでしょうか?関数Fによって値域は変化するでしょうが、それでもたとえば(a < x < b, c < y < d, e < z < f)みたいな直方体の領域になると想定してもいいのでしょうか。 それともこれも関数Fの実装によってさまざまで予想がつかず、ものによってはUVを計算してみるまで分からない(関数から結果のUVとともに結果が有効か無効かを示す真偽値が添えられてくる...とか?)ようなケースもあり得るのでしょうか。
fana

2020/11/28 02:54

本件,質問としては想定している特定のアプリケーション仕様に依らない形とさせていただいておりますが, 話がぼんやりしすぎているので,想定されるアプリケーションについて記述します: ・モデルとは「投影面」であり ・画像をその面に投影する という話が想定にあります. モデル形状は未知ですが,いずれにせよこの場合であれば,Vに対応する(u,v)は,|V|には依存せず,原点からVに向かうベクトルの「向き」によってのみ定まるので,「Vの値域」というのは「向きに関する値域」となります. 向きを天頂角と方位角で表す場合,通常は前者の範囲に制限がつく想定で,判定自体は 「天頂角<=保証される天頂角最大値」といった簡単な処理(単一のパラメタとの比較)になります.
fana

2020/11/28 03:14 編集

この話で想定される「画像」は,広角なカメラによる撮影画像であり, Vの向きと(u,v)との対応関係というのは相応に面倒な形の式(カメラモデル)で表され得るものです. カメラモデルは使用したカメラキャリブレーション手法次第であり,(そして,手法を制限したくないので)「未知」です. 多くの場合,関数Fは内側に(3~9次くらいの)多項式をいくつか含んでおり,その係数はキャリブレーションによって与えられる,すなわち,キャリブレーションに用いたデータに対する関数フィッティングの結果であるので,データが存在しなかった範囲では振る舞いが未知となります. 激しく振動していたり,絶対値が巨大な値にすっ飛んでしまうかもしれません.
Bongo

2020/11/28 03:26

なるほど...射影マッピング( 第6回 投影マッピング - 床井研究室 http://marina.sys.wakayama-u.ac.jp/~tokoi/?date=20040920 )に似たようなものなのでしょうか。 ただし映像を投影するプロジェクターの投影方法はこちらからは指定できず、単純な4x4投影行列では表現できない可能性があり、関数Fとして表現しなければならない...というような状況ですかね?
fana

2020/11/28 03:43 編集

そんなイメージでよいかもしれません. 「投影」するための計算処理が未知で,非線形関数だったり,「なんかもう解けないからニュートン法で頑張るわ」みたいな実装なのかもしれない,という… 超単純な話をすれば,関数Fの中に tan(θ) なる項が「θ<<90度 を前提として」含まれていたとしたら,Vに対するθが120度のときの計算結果値は無意味な何かになりえますし,それ以前に90度付近では計算が成り立たない. なので,「θ<閾値」かどうかを見て「Fによる計算自体ができる/できない」を判定する,という感じです.(Vからθは簡単に求められるとして)
Bongo

2020/11/28 03:58

一回レンダリングするのにかける時間はどの程度まで許容できそうでしょうか? 多少時間がかかってもかまわないならば...という前提なのですが、いったんモデルを画面と同サイズの浮動小数点テクスチャ上にオフスクリーンレンダリングしてしまうのはどうでしょう。 バーテックスシェーダーからはモデルの頂点座標をそのままフラグメントシェーダーに伝達、フラグメントシェーダーでは受け取った頂点座標をそのまま出力することにすれば、実際に画面に映る各ピクセルにおける頂点座標が記録された「スクリーンスペースモデル座標マップ」的なものができるかと思います。 このデータをGPU側からCPU側に取得してきて、その全ピクセルに関数Fを適用して(ここがきつそうですね...並列化すればいくらか速度を稼げるかもしれませんが、GPUの速度にはかなわなそうです)「スクリーンスペースUVマップ」を作成し、本番レンダリングではフラグメントシェーダー上でそれを参照してUVを取得、テクスチャを貼る...なんてことを思いついたのですが、可能そうでしょうかね?
fana

2020/11/28 04:25

なるほど,画素単位のVを得るための並列計算機としてのシェーダを用意するわけですね. (シェーダ関係の経験があまりなく,この質問にも初心者マークを貼っている有様ですので)すぐさま実装してみることはできそうにありませんが… そのような発想が無かったので,とてもありがたいです. 処理時間の面については, 目の前に具体的な要求仕様が来ているような段階にはありませんので,レンダリング速度の許容範囲は不明ですが, 感覚としては,Fの重さが問題になり得そうではあります. (各画素位置のVを求めることと,(u,v)を求めることのどちらがボトルネックか? という話になるのかな? 前者が軽い場合にはGLを使ううまみがあまり無いのかも?)
ikadzuchi

2020/11/28 04:26

なるほど、だいたい状況が分かりました。 Vの値域が「天頂角<=保証される天頂角最大値」といった単純な式で表せるようなので、それをピクセルシェーダーで計算させるのが楽かなと思います。 ポリゴンは十分に細かいようなので、Vから角度まで求めておいて頂点に角度を与えて、補間された角度を使って計算する方針で。 不正な値域でも、θ=90°などの極端な条件は滅多に無く、境界外側1ポリゴン程度ならわりと連続になっているのではないかと思えるので、その場合、 ・値域の境界を計算してテクスチャ側で境界の外を透明にしておき、 ・有効な値域内と、加えてその外側の1頂点を含む範囲にF(V)の解を与える でどうでしょう。 式は毎回変わるようなものではなくテクスチャは事前に用意できますよね? なお外側の1頂点を含む範囲の計算が面倒かもしれません。 (これは回答な気もするがBongoさんもこっちでやってるので…)
fana

2020/11/28 04:42 編集

内容の漠然さ故にタイミングをつかみ難いかとは思いますが, 回答(方法論,アイデア)となりそうな段階で回答としていただければ良いかな,と存じます. > 加えてその外側の1頂点を含む範囲にF(V)の解を与える これは,初めて内側から外側に出る位置となった頂点に対しては,そこから最も近い「内側の」頂点から得られる(u,v)を,(質問内でいうところの「ダミーの(u,v)」として)与えるといった形ですか?
ikadzuchi

2020/11/28 04:49

いえ、値域を超えたVの値をF(V)に与える形です。 それでもそれなりに自然な値が出てくれるのではないかと感じています。
fana

2020/11/28 05:01

把握しました. ・角度を頂点に持たせて補間 → ポリゴンが相応に小さければ補間値を用いて判定しても境界を十分に近似できるであろう ・ちょっと保証範囲外に出たVでのF(V) → これもポリゴンサイズが小さければ,まだ極端に外れた値にはならないだろうから,ポリゴン内での(u,v)の補間に用いることはできるであろう ということですね.
ikadzuchi

2020/11/28 05:02

はい、そういうことです。
fana

2020/11/28 05:24

ふむ. 「ちょっと保証範囲外に出たVでのF(V)」に関しては,そのままそれを使うのか,それとも,Bongoさんが書かれているように外挿してやるのかを,Fの特性で選ぶ(Fを与える側は,どちらがより良いかを判断できるハズ)とかすれば良いのかも.
fana

2020/12/29 07:03

非常に時間が空いてしまいましたが…… 現実的に直面気味であった特定用途(2020/11/28 11:54)に関しては, ikadzuchi氏にコメント頂いた内容(2020/11/28 13:26 )で十分であろうと実際に確認できましたので,その旨をここに述べておきます. θと(u,v)が独立ではなく強い関係性がある(というか,(u,v)がθを用いて計算されている)ため,ポリゴン内でθを(u,v)と一緒に線形補間することに何も問題が無い的な. 閾値を余裕をもって(ぎりぎりな値よりも小さく)与えてもよい状況ならば, > ちょっと保証範囲外に出たVでのF(V) が用いられた補間結果(u,v)を実際に用いること自体を避けることができますね.
guest

回答2

0

ベストアンサー

追記依頼欄にて思いつきを開陳してしまいましたが、多少なりとも方法検討のご参考になればと思い、スクリーンスペースUVマップ案について試してみました。
実験用プロジェクトは.NET Core 3.1とOpenTK 4.3.0で作成し、半径2の球体の-Z側だけを切り出した半球モデルにテクスチャを投影するというシチュエーションにしました。

半球の描画メソッドは下記のような流れです。テクスチャだののリソース管理のため別途作ったクラスなどを使用しておりますが、字数が足りませんので省略いたしました。ご容赦ください。

C#

1public 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 2layout (location = 0) in vec3 modelPosition; 3 4layout (location = 0) uniform mat4 modelViewProjectionMatrix; 5 6out vec3 localPosition; 7 8void main() 9{ 10 localPosition = modelPosition; 11 gl_Position = modelViewProjectionMatrix * vec4(modelPosition, 1.0); 12}

GLSL

1#version 430 core 2layout (location = 0) out vec4 fragColor; 3 4in vec3 localPosition; 5 6void 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 2layout (location = 0) in vec3 modelPosition; 3layout (location = 1) in vec3 modelNormal; 4layout (location = 2) in vec2 modelUv; 5 6layout (location = 0) uniform mat4 modelViewProjectionMatrix; 7layout (location = 1) uniform mat3 modelNormalMatrix; 8 9out vec3 worldNormal; 10out vec2 texCoord; 11 12void main() 13{ 14 worldNormal = modelNormalMatrix * modelNormal; 15 texCoord = modelUv; 16 gl_Position = modelViewProjectionMatrix * vec4(modelPosition, 1.0); 17}

GLSL

1#version 430 core 2layout (location = 0) out vec4 fragColor; 3 4layout (location = 2) uniform vec3 worldLightDirection; 5layout (location = 3) uniform vec3 worldLightIntensity; 6layout (location = 4) uniform sampler2D mainTexture; 7layout (location = 5) uniform sampler2D subTexture; 8layout (location = 6) uniform sampler2DRect uvMap; 9layout (location = 7) uniform float ignoreValidity; 10 11in vec3 worldNormal; 12in vec2 texCoord; 13 14void 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}

通常のレンダリングでは下図のような映像が得られる状態において...

図1

今回の描画フローでは、まず座標マップが作成されます。可視化すると下図のようになりますが、実験に使った半球はZ座標が負の頂点ばかりなので、色としては赤と緑だけの図になってしまいました。

図2

これに対して適用するUV計算関数には、下記のような3種類を試してみました。UVの有効・無効については、いずれもさしあたり求めたUVが(0, 0)~(1, 1)の範囲内ならば有効、さもなければ無効とすることにしました。

C#

1private 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 12private 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 26private 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の有効・無効を無視してテクスチャを投影した結果はそれぞれ下図のようになりました。

図3

一方、F2は緯度・経度に基づいてマッピングしており、テクスチャは下地のグリッド模様に沿って貼り付けられました。

図4

F3は-Z方向に対して45°をなす方角が特異点になっており、45°に近づくほどUV座標の絶対値が大きくなっていきます。
45°を超えると空間が裏返り、90°付近では再びUVが(0, 0)~(1, 1)の範囲におさまります。

図5

UV座標の絶対値が大きいほどテクスチャ貼り付けの精度は落ちるでしょうが、今回の例のように絶対値の大きい領域が小さくごちゃごちゃした状態になるマッピングであれば、粗は目立たないだろうと思います。
ですが、UV座標の飛びがあるとその周辺は汚くなってしまうかもしれません。F3が下記のように常に0~1のUVを返すような実装だった場合...

C#

1private 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座標が大きく飛ぶため、そこだけミップマップレベルが大きくなってしまいます。結果として反復境界に見苦しいアーティファクトが生じてしまいました。

図6

速度に関してですが、何とか50FPS台を達成していたものの、だいぶきつめな印象でした。
今回使ったのはIntel Core i3-4030U、Intel HD Graphics 4400の旧式廉価ノートPC、ウィンドウの解像度は640×480、構図は先に図示したような視点で、これら条件でのUV計算関数の実行回数はおよそ毎秒350万~370万回でした。
fanaさんやikadzuchiさんのおっしゃるような、頂点にデータを埋め込む方針で問題なさそうでしたら、やはりそちらがよさそうですね。それなら毎フレーム大量にUV計算関数を実行したり、テクスチャデータ転送で帯域を消費したりする必要もないでしょうから、ずっと高速に実現できそうに思います。

また別の思いつきとして、投影したい画像を事前に処理してポリゴンへの投影に適した形に変形してしまう手があるかもしれません。
関数Fに入力されるVの大きさは関係なく方角だけが計算に使われるのであれば、画像を球面上にマッピングすることができるだろうと思いますので、キューブマップテクスチャに変換できるんじゃないでしょうか?
画像がめったに変化しないのであれば画像自体をキューブマップに、Webカメラ映像みたいに画像が常時変化する場合はUVマップをキューブマップに変換してしまえるように思います。
もし関数Fが毎フレーム変化するようなら利点はないでしょうが、そうでなければだいぶ効率的に描画できそうですね。

投稿2020/12/02 20:34

Bongo

総合スコア10811

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

fana

2020/12/03 01:37

わざわざ御検討いただき恐縮であります. 事情と技量の両者の都合上, 方法論を実際にこちらで試行できるのは大分先のことになってしまいそうですが, 描画解像度が比較的小さくてよい場面においては十分使えそうな感じを受けました. (解像度が大きい場合(加えて,F次第),要求FPS次第では厳しくなるのかも?しれません.)
fana

2020/12/03 02:15

キューブマップに関しては,大昔に(固定パイプラインの機能で)一度触れたことはあるのですが… その後ずっとOpenGLに触れない期間が10年~とかいう状況で,現代のOpenGLに関してド素人状態ですので, シェーダで使う方法や可否(使えるバージョンの制限があるのか?とか)あたりを調査するところから開始する必要がありそうですが, 言われてみれば,キューブマップはまさに「投影」の手段ですから,投影処理を想定する場合であれば真っ先に(?)検討すべきようにも思えます.
fana

2021/01/28 09:13

なかなか 検討可能な時間 と 検討可能な環境 の両者が揃う機会が得られず, 且つ,自身の力量不足により,検討自体も進まず… という状況にて,いただいた回答の内容を消化できていない状況ではございますが,(これ以上保留していても検討が早急に進むような目途が立たない感がありますので) 勝手ながら,BAを選択することで質問を閉じる形とさせていただきます.
guest

0

「質問への追記・修正、ベストアンサー選択の依頼」欄にて述べた

fana 2020/12/29 16:03
非常に時間が空いてしまいましたが……

現実的に直面気味であった特定用途(2020/11/28 11:54)に関しては,
ikadzuchi氏にコメント頂いた内容(2020/11/28 13:26 )で十分であろうと実際に確認できましたので,その旨をここに述べておきます.
θと(u,v)が独立ではなく強い関係性がある(というか,(u,v)がθを用いて計算されている)ため,ポリゴン内でθを(u,v)と一緒に線形補間することに何も問題が無い的な.
閾値を余裕をもって(ぎりぎりな値よりも小さく)与えてもよい状況ならば,

ちょっと保証範囲外に出たVでのF(V)

が用いられた補間結果(u,v)を実際に用いること自体を避けることができますね.

の原理確認結果を示しておく.

方法の概要

テクスチャ座標を
(u,v) = F( V )
として計算可能か否かが「Vから算出できる値θが閾値以下なら可能,閾値より大きければ不可能」という単純な話である場合において,
バーテックスシェーダに渡す頂点のデータとしてθの値を与え,フラグメントシェーダで(補間された)θの値と閾値を比較することによって,(補間された)テクスチャ座標(u,v)を用いるか否かを判断する.

上記の「計算可否」の境界線が内側を通るポリゴンに関しては,
「(補間された)テクスチャ座標(u,v)」が異常値とならないようにするために,
そのポリゴンを構成する頂点群のうち(u,v)が関数F()で計算不可能な頂点に対して,何かしらの補間に用いられても問題が無い(u,v)値を与えておく必要がある.
その手段としては,

  • 外挿等で求めた,「それらしい」(u,v)値を用いる.
  • フラグメントシェーダに与える「閾値」を,本来よりも小さくしてやることで,この補間問題自体を避ける.

等が考えられる.

行った原理確認

単純に,空間に平面を配置し,前記方法によって平面の一部分のみにテクスチャを適用した形の結果が得られることを確認した.

テストに用いたテクスチャ用の画像:
等距離射影方式 r=fθ の画像を模した.赤と緑の境界が上記の「閾値」に相当.
(つまり,ポリゴンに,この外周の緑の部分が貼られないことを目指す)
イメージ説明

この画像生成処理コードはこんな.(示しても意味ない気もするが…)
画像データとして OpenCV の cv::Mat を使用.

C++

1//テクスチャに使うための画像の生成. 2//等距離射影方式(r = Fθ)を模した絵: 3// {θ=0~引数で示した最大値まで の範囲が青~赤,それ以外の箇所が緑} 4//を作る. 5cv::Mat CreateTestImg( 6 double MaxTheta, //θの最大値[rad] 7 double F, //θと画像中心からの距離r[pixel]との関係を r = Fθ とするときの係数Fの値. 8 int Size //画像のサイズ[pixel].縦と横で共用 9) 10{ 11 cv::Mat Img = cv::Mat::zeros( Size, Size, CV_8UC3 ); 12 const double C = (Size-1)*0.5f; 13 for( int y=0; y<Size; ++y ) 14 { 15 double y2 = ( y-C )*( y-C ); 16 cv::Vec3b *p = Img.ptr< cv::Vec3b >( y ); 17 for( int x=0; x<Size; ++x, ++p ) 18 { 19 double x2 = ( x-C )*( x-C ); 20 double r = sqrt( x2+y2 ); 21 double theta = r / F; 22 if( theta <= MaxTheta ) 23 { 24 unsigned char c = cvRound( 255*theta / MaxTheta ); 25 *p = cv::Vec3b( ~c, 0, c ); 26 } 27 else 28 { *p = cv::Vec3b(128,255,128); } //てきとーな緑色 29 } 30 } 31 return Img; 32}

平面側のデータの生成コードはこんな.

C++

1//テスト用のモデル定義に必要なデータ 2struct ModelData 3{ 4 std::vector< float > Vtx; //頂点座標群.先頭から3個ずつが1個の頂点の座標(X,Y,Z) 5 std::vector< float > TexCoord; //頂点のテクスチャ座標とθ.先頭から3個ずつが1頂点のデータで,(u,v,θ[rad]) 6 std::vector< unsigned int > Idx; //インデックス配列.先頭から3個ずつで1個の三角形を示す 7}; 8 9//(X-Y平面に平行で中心がZ軸上にある) 正方形モデルデータの生成 10ModelData CreateTestPlane( 11 double PlaneSize,//正方形のサイズ(一辺の長さ) 12 float Z, //矩形のZ座標 13 int nDiv, //縦と横を何等分するか 14 std::function<double(double)> r_from_theta //θに対応する,正規化されたテクスチャ座標系におけるテクスチャ中心からの距離を求める関数 15) 16{ 17 const double HalfSize = 0.5 * PlaneSize; 18 const int nVtx_of_1dir = nDiv + 1; 19 const int nVtx = nVtx_of_1dir * nVtx_of_1dir; 20 21 ModelData Data; 22 //頂点座標とテクスチャ座標データの計算 23 Data.Vtx.resize( nVtx * 3 ); 24 Data.TexCoord.resize( nVtx * 3 ); 25 { 26 int index=0; 27 for( int iy=0; iy<nVtx_of_1dir; ++iy ) 28 { 29 double Y = iy * PlaneSize/(nVtx_of_1dir-1) - HalfSize; 30 for( int ix=0; ix<nVtx_of_1dir; ++ix ) 31 { 32 double X = ix * PlaneSize/(nVtx_of_1dir-1) - HalfSize; 33 Data.Vtx[ index ] = float(X); 34 Data.Vtx[ index+1 ] = float(Y); 35 Data.Vtx[ index+2 ] = Z; 36 37 double theta = atan2( sqrt(Y*Y+X*X), Z ); 38 double r = r_from_theta( theta ); 39 double Phi = atan2( Y,X ); 40 Data.TexCoord[ index ] = float( 0.5 + cos(Phi) * r ); 41 Data.TexCoord[ index+1 ] = float( 0.5 + sin(Phi) * r ); 42 Data.TexCoord[ index+2 ] = float(theta); 43 44 index += 3; 45 } 46 } 47 } 48 //頂点インデックス配列内容の計算 49 const int nTriangle = nDiv*nDiv*2; 50 Data.Idx.resize( 3*nTriangle ); 51 { 52 int iIdx = 0; 53 for( int iy=0; iy+1<nVtx_of_1dir; ++iy ) 54 { 55 for( int ix=0; ix+1<nVtx_of_1dir; ++ix ) 56 { 57 Data.Idx[ iIdx++ ] = ix + iy*nVtx_of_1dir; 58 Data.Idx[ iIdx++ ] = (ix+1) + iy*nVtx_of_1dir; 59 Data.Idx[ iIdx++ ] = ix + (iy+1)*nVtx_of_1dir; 60 61 Data.Idx[ iIdx++ ] = (ix+1) + iy*nVtx_of_1dir; 62 Data.Idx[ iIdx++ ] = (ix+1) + (iy+1)*nVtx_of_1dir; 63 Data.Idx[ iIdx++ ] = ix + (iy+1)*nVtx_of_1dir; 64 } 65 } 66 } 67 68 return Data; 69}

本テストにおいては,これらの関数を以下のように使用した.

C++

1//画像生成 2//θの閾値を30度とした画像を作った. 3const int TexImgSize = 256; 4const double MaxTheta = glm::radians<double>( 30 ); 5const double F = 120.0/MaxTheta; 6 7auto TestImg = CreateTestImg( MaxTheta, F, TexImgSize ); //この画像をテクスチャとして使用 8 9//平面モデル用データ生成 10//平面のサイズは,平面の各辺の中心のθが40度となるようなサイズとした. 11const double TexRate = F / (TexImgSize-1); 12const double PlaneZ = 10; 13const double PlaneSize = 2 * tan( glm::radians<double>(40) ) * PlaneZ; 14 15auto TP = TestModel::CreateTestPlane( 16 PlaneSize, float(PlaneZ), 17 16,//←ポリゴン分割数 18 [TexRate](double theta)->double{ return theta*TexRate; } 19); 20 21//あとは,このTPが持っているデータ(頂点の座標とか)を然るべく OpenGL に与えて使用

処理結果例:

  • 非常にポリゴン分割が荒い場合(平面の1辺を2分割)の処理結果はこんな.

(下図で黄色線がポリゴンの境界線のイメージ.ただしこの黄色線は処理結果画像に後から手で描いたもの)
・フラグメントシェーダは,θが閾値より大きい場所では(u,v)を無視して単に暗いグレーを結果とする形としたので,テクスチャが貼られない箇所は暗いグレーになっている.
・テクスチャ画像のうち,緑色の領域が用いられていないことがわかる.
・ちゃんと平面の幅(および高さ)の3/4くらいの範囲にテクスチャが貼られている(上記のコード内コメント参照:平面のサイズが40度相当で,θの閾値が30度).
イメージ説明

  • ポリゴンの分割数が足りない場合(5分割).

当たり前だが,三角形の切り方の非対称さの影響が目立つ.
イメージ説明

  • 相応に細かく分割した結果(16分割).

若干,緑色が見えるが,これは閾値ぎりぎりを攻めているためだと思う.
イメージ説明

投稿2021/01/28 09:09

fana

総合スコア11990

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問