2つの三角形の球

このデモのストーリーは次のとおりです。かつて私の友人が自分のゲーム用の惑星マップジェネレータを作成し、この方法で作成されたカードを回転する球体の形で表示したかったのです。 しかし、彼は3Dグラフィックスを使用したくありませんでしたが、代わりにこの同じ球体を異なる角度で回転させた多くのフレームを生成しました。 使用されたメモリの量は...過剰でしたが、フレーム生成速度(および実行の品質)が大幅に低下しました。 少し考えて、私は彼がこのプロセスを最適化するのを手伝うことができましたが、全体として、これがOpenGLのタスクであり、2Dグラフィックスのタスクではないという公正な気持ちを残しませんでした。



そして、かつて、私が不眠症に苦しんでいたときに、私はこれらの2つのアプローチを組み合わせることを試みることにしました:OpenGLを介して回転する球体(その上に惑星地図を伸ばして)を描くが、同時にそれを平らにしておきます。



そして、私はそれをやったと言わなければなりません。 しかし、まず最初に。



プロセス数学



最初に、実際のタスクを決定しましょう。 画面上の各ポイントについて、デカルト座標系に2つの画面座標があり、そのために、球座標(実際には緯度と経度)を見つける必要があります。これは本質的に、惑星の地図のテクスチャ座標です。



だから。 球面座標系からデカルト座標系への遷移は、方程式系( ウィキペディアから取得)によって定義されます。





および逆遷移-次の方程式を使用:





半径がわかれば、 X座標とY座標からZ座標を簡単に取得でき、半径自体を1に等しくすることができます。

将来、 Y (画面を垂直にする)とZ (これがシーンの深さになる)の概念を交換することにより、上記の方程式をわずかに変更することに同意します。



技術部



このアイデアを実装するには、クワッド(既にその使用方法について書いているので、特にプロジェクトの完全なソースコードへのリンクを以下に示しますので繰り返しません)と2つのテクスチャ:惑星の実際のマップ(サイズの地球のテクスチャを使用しました) 2048x1024)およびテクスチャ座標マップ。 2番目のテクスチャを生成するコードは、デカルト座標から球面への変換の数学を正確に繰り返します。



int texSize = 1024; double r = texSize * 0.5; int[] pixels = new int[texSize * texSize]; for (int row = 0, idx = 0; row < texSize; row++) { double y = (r - row) / r; double sin_theta = Math.sqrt(1 - y*y); double theta = Math.acos(y); long v = Math.round(255 * theta / Math.PI); for (int col = 0; col < texSize; col++) { double x = (r - col) / r; long u = 0, a = 0; if (x >= -sin_theta && x <= sin_theta) { double z = Math.sqrt(1 - y*y - x*x); double phi = Math.atan2(z, x); u = Math.round(255 * phi / (2 * Math.PI)); a = Math.round(255 * z); } pixels[idx++] = (int) ((a << 24) + (v << 8) + u); } } GLES20.glGenTextures(1, genbuf, 0); offsetTex = genbuf[0]; if (offsetTex != 0) { GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, offsetTex); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_NONE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_NONE); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, texSize, texSize, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, IntBuffer.wrap(pixels)); }
      
      





座標XYは範囲[0..texSize]から範囲[-1..1]に転送され、テクスチャ座標UVはラジアンから範囲[0..255]に転送されます。その後、それぞれ赤で書き込まれます。 32ビットテクスチャの緑のコンポーネント。 アルファチャネルは「深さ」( Z座標)を保持するために使用されますが、青色チャネルは今のところ使用されていません。 バイリニアフィルタリングを無効にすることも偶然ではありません。この段階では効果はありません(いずれにしても、隣接するポイントは同じ値を持ち、かなり急なジャンプを伴います)が、次に示す内容では完全に有害です。 しかし、それについては以下で詳しく説明します。



両方のテクスチャは、単純なピクセルシェーダーの入力に送られます(ここと下で、写真をクリックできます)。



 private final String quadFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "uniform sampler2D uTexture1;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" + " vec3 vCol = texture2D(uTexture1, vTex.xy).rgb;\n" + " gl_FragColor = vec4(vCol, (vTex.w > 0.0 ? 1.0 : 0.0));\n" + "}\n";
      
      





シーンレンダリングコードを提供していません。なぜなら、 その中のすべては非常に些細なものであり(また、完全なソースで見ることができます)、シェーダー自体は非常に原始的です。 彼にとって最も興味深いのは、おそらく、アルファチャネルは陽性のテストのみであり、照明効果に使用できることです。



すでにかなり良い結果が得られましたが、どういうわけかフラットであり、それに加えて、惑星の軸を中心とした実際の回転を追加したいと思います。



シェーダーにもう1つのパラメーターを含め(範囲[0..1]の時間に応じて変更します)、さらに「深さ」を追加します(アルファチャネルからの値で色を乗算します)。



 private final String quadFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "uniform sampler2D uTexture1;\n" + "uniform float uOffset;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" + " vTex.x += uOffset;\n" + " vec3 vCol = texture2D(uTexture1, vTex.xy).rgb;\n" + " gl_FragColor = vec4(vCol * vTex.w, (vTex.w > 0.0 ? 1.0 : 0.0));\n" + "}\n";
      
      





さて、球体自体について不満はありませんが、写真はどうやら... 8ビットか何かに見えます。 また、当然のことです。[0..255]の範囲でテクスチャ座標を記録しました(通常の色成分で使用できる最大値)。つまり、テクスチャの高さは256ポイント(および回転を考慮すると幅512ポイント)しか持てません。 十分ではありませんが、少なくとも10ビットの精度が必要です。



解像度を上げる



すぐに警告します。ここで説明したコードは一部のデバイスで正しく動作しますが、手に持つことができるすべてのデバイスで通常のレンダリングを達成できました。 いずれにせよ、ここで説明するのは通常のハックです。



したがって、これまでのところ、3つの色成分のうち2つがあります。 24ビットのうち16ビットです。各テクスチャ座標のサイズが12ビットになるようにデータをパックしましょう。これにより、最大4096ピクセルのテクスチャを操作できるようになります。 これを行うには、プログラムの3行を文字通り変更します。



 ... long v = Math.round(4095 * theta / Math.PI); ... u = Math.round(4095 * phi / (2 * Math.PI)); ... pixels[idx++] = (int) ((a << 24) + (v << 12) + ((u & 15) << 8) + (u >> 4)); ...
      
      





そして、12ビットのアドレス指定スキームを考慮に入れた新しいシェーダーを作成します(この場所で双線形フィルタリングを無効にする必要があります!):



 private final String quadFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "uniform sampler2D uTexture1;\n" + "uniform float uOffset;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " vec4 vTex = texture2D(uTexture0, TexCoord0.xy);\n" + " vec3 vOff = vTex.xyz * 255.0 + vec3(0.5, 0.5, 0.5);\n" + " float hiY = floor(vOff.y / 16.0);\n" + " float loY = vOff.y - 16.0 * hiY;\n" + " vec2 vCoord = vec2(\n" + " (vOff.x * 16.0 + loY) / 4095.0 + uOffset,\n" + " (vOff.z * 16.0 + hiY) / 4095.0);\n" + " vec3 vCol = texture2D(uTexture1, vCoord).rgb;\n" + " gl_FragColor = vec4(vCol * vTex.w, (vTex.w > 0.0 ? 1.0 : 0.0));\n" + "}\n";
      
      





まあ、これはまったく別の問題です! 小さな変更(ピンチズームと指の回転の追加)を行って、このプログラムを友人や同僚に見せ、同時にこのシーンに三角形がいくつあるかを尋ねました。 結果はさまざまで、質問自体が汚いトリックの疑いを引き起こしました(この場合、回答者は「1」を冗談で言いましたが、これは真実からそれほど遠くありませんでした)が、正しい答えは安定して驚くべきものでした。 そして誰もが、1つの質問として、なぜ球体は1つの軸の周りでねじれるが、傾かないのか?..うーん。



チルト



しかし、実際には、このスキームの勾配を実装するのははるかに困難です。 実際、このタスクは不溶性ではなく、私もそれに対処しましたが、ニュアンスなしではできませんでした。



本質的に、タスクはオフセット座標Vを取得する一方で、座標Uは変化しません。これは、 X軸の周りに回転を追加するためです。 計画は次のとおりです。テクスチャ座標をスクリーンに変換し([-1..1]の範囲)、水平軸の周りに回転行列を適用します(このため、傾斜角の正弦と余弦を新しい定数uTiltに記述します )、次に新しいY座標を使用しますテンプレートテクスチャでサンプリングします。 「回転した」 Z座標は、ボールの背面の経度をミラー化するのに役立ちます。 スクリーン座標Zは、同じテクスチャから2つのテクスチャサンプルを作成しないように明示的に計算する必要がありますが、同時に精度が向上します。



 private final String quadFS = "precision mediump float;\n" + "uniform sampler2D uTexture0;\n" + "uniform sampler2D uTexture1;\n" + "uniform float uOffset;\n" + "uniform vec2 uTilt;\n" + "varying vec4 TexCoord0;\n" + "void main() {\n" + " float sx = 2.0 * TexCoord0.x - 1.0;\n" + " float sy = 2.0 * TexCoord0.y - 1.0;\n" + " float z2 = 1.0 - sx * sx - sy * sy;\n" + " if (z2 > 0.0) {;\n" + " float sz = sqrt(z2);\n" + " float y = (sy * uTilt.y - sz * uTilt.x + 1.0) * 0.5;\n" + " float z = (sy * uTilt.x + sz * uTilt.y);\n" + " vec4 vTex = texture2D(uTexture0, vec2(TexCoord0.x, y));\n" + " vec3 vOff = vTex.xyz * 255.0 + vec3(0.5, 0.5, 0.5);\n" + " float hiY = floor(vOff.y / 16.0);\n" + " float loY = vOff.y - 16.0 * hiY;\n" + " vec2 vCoord = vec2(\n" + " (vOff.x * 16.0 + loY) / 4095.0,\n" + " (vOff.z * 16.0 + hiY) / 4095.0);\n" + " if (z < 0.0) { vCoord.x = 1.0 - vCoord.x; }\n" + " vCoord.x += uOffset;\n" + " vec3 vCol = texture2D(uTexture1, vCoord).rgb;\n" + " gl_FragColor = vec4(vCol * sz, 1.0);\n" + " } else {\n" + " gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);\n" + " }\n" + "}\n";
      
      





やあ、坂道は成功だった! これは、半球の境界での奇妙なノイズです。 明らかに、問題は境界点でアドレス指定の精度が不十分であることにあります(円自体の点は座標の範囲が大きすぎることに対応し、1つのテクセルがかなり長い間隔で広がります)。 最終的には、1つではなく2つの生成されたテクスチャを使用して、これを何とかすることができました。



その結果、Google Earthの場合とほぼ同じ方法で、ズームインしてボールを回転させることができます。 ここには2つの三角形しかないという違いがあります。



そして最後に、約束された。 プロジェクトのソースコードはGitHubで入手できます

完成した.apkファイルをダウンロードすることもできます



ところで、私の過去の投稿のソースはそこにあります。



更新:私はまだすべてのデバイスで正確なテクスチャリングを達成できたようです。これには、少しのビット変更と、パターンテクスチャの色の丸みの調整が少し必要でした。 これで、強制的に圧縮されたテクスチャでさえ、ほぼ正常に機能するはずです。 GitHubコードが更新され、新しい.apkファイルがアップロードされました。



更新2:それでも、半球のアーティファクトは敗北しました。 ソースと既製の.apkが更新されました。

さらに、別のボーナスを追加しまし 。このデモのWebGLバージョンは、こちらから入手できます



All Articles