Androidで.3DSファイルをダウンロードする

この記事は、グロー効果に関する前回の投稿の前編のようなものです。 .3dsファイルをアップロードして、そこで使用されるシェーダーを使用してレンダリングする方法を説明すると約束しました。



ファイル形式に関するいくつかの一般的な情報は、例えば、 Wikipediademo.design 3DプログラミングFAQで読むことができますが、これはすべて理論です(そして、エラーなしで書かれています)。



ここにあるもの:



ここにないもの:



前回のように、ソースコード全体を詳細にかみ砕きます(ここでは1,000行を超えることはありません)。主な点のみを説明し、ソースコード全体へのリンクを提供します(記事の最後)。 まだ興味がありますか? それでは続けましょう。



ファイルを読む



奇妙なことに、最初に直面しなければならない最も困難なタスクの1つであるファイルからの数字の読み取りが最も簡単でした。 ここには、速度と正確さの2つのレーキがあります。 BufferedInputStreamと排他的シーケンシャル読み取りを使用して速度を確保しますが、すべてが正確で少し複雑です:Javaはファイル内のすべてのデータがビッグエンディアンであると見なしますが、.3dsはリトルエンディアンを使用します。 さて...単純なラッパーを使用します。



private BufferedInputStream file; private byte[] bytes = new byte[8]; private long filePos = 0; ... private void Skip(long count) throws IOException { file.skip(count); filePos += count; } private void Seek(long end) throws IOException { if (filePos < end) { Skip(end - filePos); filePos = end; } } private byte ReadByte() throws IOException { file.read(bytes, 0, 1); filePos++; return bytes[0]; } private int ReadUnsignedByte() throws IOException { file.read(bytes, 0, 1); filePos++; return (bytes[0]&0xff); } private int ReadUnsignedShort() throws IOException { file.read(bytes, 0, 2); filePos += 2; return ((bytes[1]&0xff) << 8 | (bytes[0]&0xff)); } private int ReadInt() throws IOException { file.read(bytes, 0, 4); filePos += 4; return (bytes[3]) << 24 | (bytes[2]&0xff) << 16 | (bytes[1]&0xff) << 8 | (bytes[0]&0xff); } private float ReadFloat() throws IOException { return Float.intBitsToFloat(ReadInt()); }
      
      







良い意味では、BufferedInputStreamから継承された別のクラスである必要がありますが、この場合はそれを行う方が便利でした。



これで、チャンクの読み取りを開始できます。 まず第一に-主なもの:



  private Scene3D ProcessFile(long fileLen) throws IOException { Scene3D scene = null; while (filePos < fileLen) { int chunkID = ReadUnsignedShort(); int chunkLen = ReadInt() - 6; switch (chunkID) { case CHUNK_MAIN: if (scene == null) scene = ChunkMain(chunkLen); else Skip(chunkLen); break; default: Skip(chunkLen); } } return scene; } private Scene3D ChunkMain(int len) throws IOException { Scene3D scene = new Scene3D(); scene.materials = new ArrayList<Material3D>(); scene.objects = new ArrayList<Object3D>(); scene.lights = new ArrayList<Light3D>(); scene.animations = new ArrayList<Animation>(); long end = filePos + len; while (filePos < end) { int chunkID = ReadUnsignedShort(); int chunkLen = ReadInt() - 6; switch (chunkID) { case CHUNK_OBJMESH: Chunk3DEditor(scene, chunkLen); break; case CHUNK_KEYFRAMER: ChunkKeyframer(scene, chunkLen); break; case CHUNK_BACKCOL: scene.background = new float[4]; ChunkColor(chunkLen, scene.background); break; case CHUNK_AMB: scene.ambient = new float[4]; ChunkColor(chunkLen, scene.ambient); break; default: Skip(chunkLen); } } Seek(end); scene.Compute(0); return scene; }
      
      







ローダー全体の構造は非常に均質です。各チャンクについて、発生する可能性のあるサブチャンクに関する情報を含む独自の機能です。 必要なすべての情報をダウンロードします。不必要です-ジャンプして、すぐに次のチャンクに移動します。 無効なファイルに対する保護はここでは最小限です。



素材



材料のブロックは通常、最初に行くのは、三角形のブロックが後でそれを参照するためです。



マテリアルは、いくつかの色(アンビエント、拡散、鏡面反射)、マテリアルの名前、グレアのパラメーター、テクスチャの名前で構成されています。 上記のように、テクスチャはここにはロードされませんが、必要に応じて簡単に追加できます。



3Dモデル



各3Dモデル(ChunkTrimesh関数を参照)は、次のデータによって定義されます。



最初の3つの点ですべてが明確な場合、最後の点はやや不思議に見えます。 今後は、このデータを正しく適用する方法をまだ学習しましたが、私にとってはわかりにくい存在であると言えます。



頂点に関するすべての情報を1つの配列float []にダンプし、各頂点ごとに8つの実数(座標と法線で3つ、さらに2つのテクスチャ座標)を格納します。 最後の記事の数行を変更する必要があります。



  GLES20.glVertexAttribPointer(maPosition, 3, GLES20.GL_FLOAT, false, 32, 0); GLES20.glVertexAttribPointer(maNormal, 3, GLES20.GL_FLOAT, false, 32, 12);
      
      





ここでは、テクスチャ座標がなかったので、24が32に変更されましたが、現在はそうです。



すべての座標はChunkVector関数でロードされ、同時にY軸とZ軸を入れ替えます:



  private void ChunkVector(float[] vec, int offset) throws IOException { vec[offset + 0] = ReadFloat(); vec[offset + 2] = ReadFloat(); vec[offset + 1] = ReadFloat(); }
      
      







まあ、一般的に、色やパーセンテージなどのいくつかの標準タイプでは、それらの機能が使用されます。



三角形のリストは、特別な方法で処理する必要があります。まず、マテリアルが面に適用され(頂点には適用されません)、次に、頂点の法線が面から決定されます。 これを行うには、各面の法線を計算し、3つの頂点のそれぞれに追加し、その後(最後に、すべての三角形を読み込んだ後)正規化します。 たくさんの関数、ちょっとした数学-これで完了です。



面のリストのもう1つの特徴は、マテリアルの名前を含むチャンクの後に、マテリアルが適用されていない面が残る場合があることです。 それらの場合、レンダリングの際に次のようなデフォルトのマテリアルを使用する必要があります。



  mAmbient[0] = 0.587f; mAmbient[1] = 0.587f; mAmbient[2] = 0.587f; mAmbient[3] = 1.0f; mDiffuse[0] = 0.587f; mDiffuse[1] = 0.587f; mDiffuse[2] = 0.587f; mDiffuse[3] = 1.0f; mSpecular[0] = 0.896f; mSpecular[1] = 0.896f; mSpecular[2] = 0.896f; mSpecular[3] = 1.0f; ... int mats = obj.faceMats.size(); for (j = 0; j < mats; j++) { FaceMat mat = obj.faceMats.get(j); if (mat.material != null) { if (mat.material.ambient != null && scene.ambient != null) { for (k = 0; k < 3; k++) mAmbient[k] = mat.material.ambient[k] * scene.ambient[k]; GLES20.glUniform4fv(muAmbient, 1, mAmbient, 0); } else GLES20.glUniform4f(muAmbient, 0, 0, 0, 1); if (mat.material.diffuse != null) GLES20.glUniform4fv(muDiffuse, 1, mat.material.diffuse, 0); else GLES20.glUniform4fv(muDiffuse, 1, mDiffuse, 0); if (mat.material.specular != null) GLES20.glUniform4fv(muSpecular, 1, mat.material.specular, 0); else GLES20.glUniform4fv(muSpecular, 1, mSpecular, 0); GLES20.glUniform1f(muShininess, mat.material.shininess); } else { GLES20.glUniform4f(muAmbient, 0, 0, 0, 1); GLES20.glUniform4fv(muDiffuse, 1, mDiffuse, 0); GLES20.glUniform4fv(muSpecular, 1, mSpecular, 0); GLES20.glUniform1f(muShininess, 0); } GLES20.glDrawElements(GLES20.GL_TRIANGLES, mat.indexBuffer.length, GLES20.GL_UNSIGNED_SHORT, mat.bufOffset * 2); }
      
      







出来上がり。



光源



全方向性と方向性があります。 指向性光源については、話をするまで(指向性を考慮したシェーダーを書くのは難しくありませんが)、まぶしさについて少し説明します。 前の記事のモデルのシェーダーを検討し、それにいくつかの行を追加します。



  private final String vertexShaderCode = "precision mediump float;\n" + "uniform mat4 uMVPMatrix;\n" + "uniform mat4 uMVMatrix;\n" + "uniform mat3 uNMatrix;\n" + "uniform vec4 uAmbient;\n" + "uniform vec4 uDiffuse;\n" + "uniform vec4 uSpecular;\n" + "uniform float uShininess;\n" + ... "vec4 light_point_view_local(vec3 epos, vec3 normal, int idx) {\n" + " vec3 vert2light = uLight[idx].position - epos;\n" + " vec3 ldir = normalize(vert2light);\n" + " vec3 vdir = vec3(0.0, 0.0, 1.0);\n" + " vec3 halfv = normalize(ldir + vdir);\n" + " float NdotL = dot(normal, ldir);\n" + " float NdotH = dot(normal, halfv);\n" + " vec4 outCol = vec4(0.0, 0.0, 0.0, 1.0);\n" + " if (NdotL > 0.0) {\n" + " outCol = uLight[idx].color * uDiffuse * NdotL;\n" + " if (NdotH > 0.0 && uShininess > 0) {\n" + " outCol += uSpecular * pow(NdotH, uShininess);\n" + " }\n" + " }\n" + " return outCol;\n" + "}\n";
      
      







実際に、NdotHの計算とアプリケーションが追加されました。 ここのuShininessとMaterial3DのShininessは異なる次元を持っているので、それらの間の正確な対応を選択しませんでした(ここでも、もし誰かがそれを必要とするなら、これは簡単です)。



アニメーション



.3ds形式で最も困難で興味深いトピックの1つ。 実際には、アニメーショントラックを使用しないと、一部のオブジェクトがまったく正しく表示されない場合があります。 また、オブジェクトが相互のクローンである場合、さらに多くのオブジェクトが表示されません。



.3dsファイル内のすべてのオブジェクトは階層ツリーに結合され、各「祖先」の変換を「子孫」に適用する必要があります。 ツリーの頂点は上から下の順序で記述されるため、変換の適用は同じ順序で実行できます。 3Dモデルの.3dsファイルの観点から見ると、光源とカメラは同等のオブジェクトであり、階層で相互に接続し、アニメーションを均等に適用できるのは興味深いことです。 ただし、これまでのところ、3Dモデルにのみ関心があり、特に、それらの動き、回転、スケーリングの追跡にのみ関心があります。



オブジェクトごとに保存されます:





トラックをダウンロードするのは退屈なので、それらの使用方法についてもっと話しましょう。 だから私たちは持っています:



これらすべてを1つの既製の変換マトリックスに収集する必要があります。 シフトとスケーリングでは、すべてが比較的単純です。2つのフレーム間で線形補間が単純に適用され、値は絶対形式で設定されます。 しかし、ターンはすべて順番に適用する必要があります! そして、キーフレーム間で、次のフレームの回転を適切な度数だけ適用し、線形補間します。



もう1つの興味深い点は、子孫の変換(結果)とモデルの変換(世界)という2つのマトリックスを念頭に置く必要があることです。 最初は階層チェーンで使用され、2番目はモデルをレンダリングするときに使用されます。 これはどのような順序で行われますか?



 result = parent.result * move * rotate * scale; world = result * Move(-pivot) * trmatrix;
      
      





変換は、頂点に「右から左」の順序で適用されることが理解されています(OpenGLでの慣習です)。 ここで、trmatrixは3Dモデルのチャンクにあるものの逆です 。 合計、特定の時点の変換計算コード(ロード時に、すべてのフレーム番号が0から1の実数に変換されました):



  private void lerp3(float[] out, float[] from, float[] to, float t) { for (int i = 0; i < 3; i++) out[i] = from[i] + (to[i] - from[i]) * t; } private AnimKey findVec(AnimKey[] keys, float time) { AnimKey key = keys[keys.length - 1]; // We'll use either first, or last, or interpolated key for (int j = 0; j < keys.length; j++) { if (keys[j].time >= time) { if (j > 0) { float local = (time - keys[j - 1].time) / (keys[j].time - keys[j - 1].time); key = new AnimKey(); key.time = time; key.data = new float[3]; lerp3(key.data, keys[j - 1].data, keys[j].data, local); } else key = keys[j]; break; } } return key; } private void applyRot(float[] result, float[] data, float t) { if (Math.abs(data[3]) > 1.0e-7 && Math.hypot(Math.hypot(data[0], data[1]), data[2]) > 1.0e-7) Matrix.rotateM(result, 0, (float) (data[3] * t * 180 / Math.PI), data[0], data[1], data[2]); } public void Compute(float time) { int i, n = animations.size(); for (i = 0; i < n; i++) { Animation anim = animations.get(i); Object3D obj = anim.object; float[] result = new float[16]; Matrix.setIdentityM(result, 0); if (anim.position != null && anim.position.length > 0) { AnimKey key = findVec(anim.position, time); float[] pos = key.data; Matrix.translateM(result, 0, pos[0], pos[1], pos[2]); } if (anim.rotation != null && anim.rotation.length > 0) { // All rotations that are prior to the target time should be applied sequentially for (int j = anim.rotation.length - 1; j > 0; j--) { if (time >= anim.rotation[j].time) // rotation in the past, apply as is applyRot(result, anim.rotation[j].data, 1); else if (time > anim.rotation[j - 1].time) { // rotation between key frames, apply part of it float local = (time - anim.rotation[j - 1].time) / (anim.rotation[j].time - anim.rotation[j - 1].time); applyRot(result, anim.rotation[j].data, local); } // otherwise, it's a rotation in the future, skip it } // Always apply the first rotation applyRot(result, anim.rotation[0].data, 1); } if (anim.scaling != null && anim.scaling.length > 0) { AnimKey key = findVec(anim.scaling, time); float[] scale = key.data; Matrix.scaleM(result, 0, scale[0], scale[1], scale[2]); } if (anim.parent != null) Matrix.multiplyMM(anim.result, 0, anim.parent.result, 0, result, 0); else Matrix.translateM(anim.result, 0, result, 0, 0, 0, 0); if (obj != null && obj.trMatrix != null) { float[] pivot = new float[16]; Matrix.setIdentityM(pivot, 0); Matrix.translateM(pivot, 0, -anim.pivot[0], -anim.pivot[1], -anim.pivot[2]); Matrix.multiplyMM(result, 0, pivot, 0, obj.trMatrix, 0); } else { Matrix.setIdentityM(result, 0); Matrix.translateM(result, 0, -anim.pivot[0], -anim.pivot[1], -anim.pivot[2]); } Matrix.multiplyMM(anim.world, 0, anim.result, 0, result, 0); } }
      
      







これらはすべて、特に洗練された例で試行錯誤を繰り返して得られましたが、とにかく絶対的な正確さと正確さを恐れています。それはあまりにも強力です。 そして、これはスプラインを使用しないことです!



さらに、前の記事のモデルサイクルは少し異なります。



  num = scene.animations.size(); for (i = 0; i < num; i++) { Animation anim = scene.animations.get(i); Object3D obj = anim.object; if (obj == null) continue; Matrix.multiplyMM(mMVMatrix, 0, mVMatrix, 0, anim.world, 0); Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVMatrix, 0); // Apply a ModelView Projection transformation GLES20.glUniformMatrix4fv(muMVPMatrix, 1, false, mMVPMatrix, 0); GLES20.glUniformMatrix4fv(muMVMatrix, 1, false, mMVMatrix, 0); for (j = 0; j < 3; j++) for (k = 0; k < 3; k++) mNMatrix[k*3 + j] = mMVMatrix[k*4 + j]; GLES20.glUniformMatrix3fv(muNMatrix, 1, false, mNMatrix, 0); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, obj.glVertices); GLES20.glVertexAttribPointer(maPosition, 3, GLES20.GL_FLOAT, false, 32, 0); GLES20.glVertexAttribPointer(maNormal, 3, GLES20.GL_FLOAT, false, 32, 12); GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); ...
      
      







さらに、すべてが以前と同じです。



おわりに



それだけです。ほとんどの場合、この知識で十分です。まあ、私がここで言及し、できる限り歩き回った中で最も病気の熊手です。 テクスチャのダウンロードを追加する場合、すべては問題ありませんが、それを宿題として残します。



実際、約束された既製のソース: Scene3D (データ構造)とLoad3DS (ローダー)。 ファイルはメモリカードのルート(「/ sdcard /」)からロードされることに注意してください。これをより合理的なものに変更することを強くお勧めします。



更新:通常のコピーに関して非常に多くのコピーが壊れているため、ソースにスムージンググループを操作するためのコードを追加しました。 インデックスバッファは2バイトのままなので、オーバーフローに注意してください!



All Articles