3Dプログラミング講座・その34:無限透視射影と対数距離射影








無限遠透視射影と動的スケール調整による広大な空間の可視化

このサンプルでは、無限遠透視射影(FrustumInf)と、 射影前の3次元座標を動的にスケールする処理を組み合わせ、 実座標上では大きく離れた位置に存在する、サイズの異なる物体を 連続的に観察します。

特徴的なのは、単純にカメラをズームするのではなく、 カメラ自身が実座標上を移動し、その移動に合わせて射影前の空間スケールを変更する 点です。

1. サンプルで配置しているキューブ

シーンには、実座標上で位置と大きさが異なる複数のキューブを配置しています。 例えば、概ね次のようなスケールと距離を持っています。

これらは同じ大きさの物体を単純に遠くへ配置したものではありません。 実座標上で物体そのもののスケールが大きく変化する空間を構成しています。

2. カメラは実座標上を移動する

カメラ位置に相当する zpos1 は、時間経過に応じて増加します。

zpos1 += speed * (1 - t);

したがって、このデモでは視点を単純に画面上でズームしているのではなく、 実際の3次元座標系の中をカメラが前進していると考えることができます。

カメラが進むことで、最初は遠方にあったキューブが次第に近づき、 さらにカメラがその位置を通り越して、次のスケールのキューブへ進んでいきます。

3. カメラの移動だけではスケール差が大きすぎる

通常の透視射影だけでこの空間を観察すると、 実座標上の距離と物体サイズの差が非常に大きいため、 あるキューブから次のキューブへ移動する際に、 画面上の見かけの大きさが大きく変化します。

そこで、このサンプルでは現在のカメラ位置に応じて 射影前の座標そのものを再スケールしています。

4. scaleNow による連続的な空間スケール変更

現在のカメラ位置がどのキューブの間にあるかを求め、 その区間のスケールを補間します。

scaleNow =
    instances[i].scale * (1 - t) +
    instances[i+1].scale * t;

つまり、あるスケールから次のスケールへ移動するとき、 突然倍率を切り替えるのではなく、 連続的に空間のスケールを変化させる構造になっています。

5. 無限透視射影の前に座標をスケールする

重要なのは、この倍率変更が FrustumInf() の内部で行われているわけではないことです。

まず実座標を現在の scaleNow で割ります。

p[0] /= scaleNow;
p[1] /= scaleNow;
p[2] /= scaleNow;

その後、この座標を無限遠透視射影へ渡します。

したがって処理の順序は、

  1. 実座標上でカメラを移動する
  2. 現在位置に応じて scaleNow を求める
  3. 射影前の3次元座標を scaleNow でスケールする
  4. FrustumInf() によって無限遠方向を圧縮する
  5. 画面へ描画する

となります。

つまり、このデモで視野の広がりを調整している本質は、 FrustumInf のパラメータを直接変更することではありません。

「無限透視射影の前の実座標をスケールする」ことで、 カメラの現在位置に合わせて観察する空間のスケールを連続的に変更しています。

6. FrustumInf が遠方を圧縮する

座標の再スケール後、FrustumInf() によって奥行きを ToLogAxis() を利用した無限対数軸へ変換します。

var VecWK = FrustumInf0(v);

内部では、奥行きに応じて


Range - ToLogAxis(z)

を利用してXY方向を圧縮します。

通常の透視射影のように遠方を急速に一点へ収束させるのではなく、 無限遠方向を有限領域へ連続的に圧縮することが、 このサンプルの基礎になっています。

7. 「小さいキューブ順」をカメラが通り越していく

このデモで特に重要なのが、キューブが単に 「小さい順に画面へ並んでいる」わけではないことです。

実座標上では、カメラが前進していくことで、


小さいキューブ
    ↓
その位置をカメラが通過
    ↓
次の大きさのキューブ
    ↓
さらにその位置をカメラが通過
    ↓
より大きなキューブ

という順序で実空間を移動します。

ところが、射影前の空間スケールをカメラ位置に合わせて変化させているため、 カメラが実座標上で物体を通り越しているにもかかわらず、 画面上では最も近いキューブの見かけの大きさが大きく変化しにくい という特徴が現れます。

8. 視野角を進むように見える理由

ここで重要なのは、「視野角そのものを直接変更している」と考えるよりも、 カメラが観察している座標系のスケールを連続的に変更している と考えることです。

実座標ではカメラが大きく移動しています。 しかし射影前の座標を現在のスケールに合わせて縮小するため、 カメラから見た局所的な空間は比較的似た大きさに保たれます。

その結果、

という表示になります。

9. X3DOM の logarithmic projection に近い振る舞い

この結果は、以前 X3DOM に存在した logarithmic projection のサンプルに近い振る舞いを示します。

ただし、この実装では X3DOM の既存実装をそのまま利用しているわけではありません。

このサンプルでは、

を組み合わせることで、同様に 非常に広いスケール範囲を連続的に観察する ことを実現しています。

10. 通常透視射影との違い

通常の透視射影では、基本的に


x' = x / z
y' = y / z

のように距離によって画面上の大きさが変化します。

そのため、遠方の物体ほど急速に小さくなり、 巨大な実座標空間を一続きの視野として扱うことは困難になります。

一方、このサンプルでは、


実座標
  ↓
動的スケール
  ↓
無限対数軸
  ↓
無限遠透視射影

という構造を取っています。

これによって、通常の透視射影とは異なる 「遠方を圧縮しながら、カメラ自身が巨大な実空間を移動する」 表現が可能になります。

11. 方角情報との関係

無限透視射影では、同じ奥行きにあるX方向とY方向の座標が 同じ距離依存係数によって縮小されます。

例えば、


[15, 10, -50]

は無限透視射影によって、


[0.6110382101, 0.4073588067, -50]

となります。

このとき、


15 / 10
=
0.6110382101 / 0.4073588067
=
1.5

となり、X:Yの方向比は変化しません。

したがって、距離方向を圧縮しても、 同一奥行き面内の方向比を維持したまま表示できる という性質があります。

12. 逆変換による確認

さらに InvFrustumInf() を使用すると、 射影した座標を元の3次元座標へ戻すことができます。


[15, 10, -50]
    ↓ FrustumInf
[0.6110382101, 0.4073588067, -50]
    ↓ InvFrustumInf
[15, 10, -50]

また、


[-200, 500, -250]
    ↓ FrustumInf
[-1.6566538523, 4.1416346309, -250]
    ↓ InvFrustumInf
[-200, 500, -250]

という復元も確認されています。

これは、今回の表示方法が単なる視覚的な変形ではなく、 対応する逆変換を持つ座標変換として構成されていることを示します。

13. このサンプルの本質

このサンプルは、単に「遠くの物体を小さく表示しない」ためのズーム機能ではありません。

本質的には、

実座標上ではカメラが巨大な空間を移動し続けながら、 現在位置に応じて射影前の座標スケールを連続的に変更し、 その上で無限遠透視射影によって奥行きを圧縮する

という二段階の空間変換です。

そのため、実座標上では桁違いに異なる距離・サイズを持つ物体群を、 一つの連続した視野の中で観察することができます。

14. まとめ

したがって、このデモは 「無限遠透視射影」単体のデモから一歩進み、 「実座標上のカメラ移動+射影前の動的スケール+無限遠透視射影」によって、 巨大なスケール範囲を連続的に観察するデモ と位置付けることができます。





コード全文



var TMan = new N6LTimerMan();  //タイマーマネージャー
// Range と Base は外側で設定しておく
var Range = 1.0;
var Base  = 8.0;

var p1=[];
var pp1=[];
var time1;
var adapter1;
var device1;
var canvas1;
var context1;
var format1;
var linePipeline1;
var trianglePipeline1;
var vertexBufferTri1;
var vertexBufferWire1;
var vertexCount1 = 0;
var depthTexture1;
var depthTextureView1;
var zpos1 = 1;


// 1個のキューブのテンプレート
const baseCube = [
  [10,10,10],[-10,10,10],[-10,-10,10],[10,-10,10],
  [10,10,-10],[-10,10,-10],[-10,-10,-10],[10,-10,-10]
];

// ミクロ~マクロのキューブを並べる
const instances = [
  { scale: 1.0,   pos:  [0, 0, -5000],   phase: 0   },   // 極小(ミクロ)
  { scale: 10.0,  pos:  [0, 0, -7500],   phase: 72  },   // 小さい
  { scale: 25.0,  pos:  [0, 0, -10000],  phase: 144 },   // 中くらい
  { scale: 75.0,  pos:  [0, 0, -15000],  phase: 216 },   // 大きい
  { scale: 200.0, pos:  [0, 0, -23000],  phase: 288 },   // 巨大(マクロ)
];

const phongshaderCode = `
const Range : f32 = ${Range};

struct VertexInput {
    @location(0) position : vec3, // 面側と同じく position を受け取る
};

struct VertexOutput {
    @builtin(position) pos : vec4,
};

@vertex
fn vs_main(input : VertexInput) -> VertexOutput {
    var out : VertexOutput;
    
    // 面側と完全に同じロジックで深度を計算・クランプする
    // (もし面と線が完全に同一個所で重なってZファイティングが起きる場合は、
    //   ここで depth にわずかなオフセット(例: -0.001)を足すと線を少し手前に浮かせられます)
    // 奥(z / Range が 1.0 に近づく)に向かって、2乗で急速にオフセットが 0 に消えていく
    var factor = 1.0 - input.position.z / Range;
    var depth = (-input.position.z) / Range - 0.000002 * (factor * factor * factor * factor);
    out.pos = vec4(input.position.xy, clamp(depth, 0.0, 1.0), 1.0);
    return out;
}

@fragment
fn fs_main() -> @location(0) vec4 {
    // ワイヤーフレームの色(例:黒や好みの色)
    return vec4(0.0, 1.0, 0.0, 1.0);
}
`;

const trianglephongshaderCode = `
const Range : f32 = ${Range};

struct VertexInput {
    @location(0) position : vec3,
    @location(1) normal   : vec3,
    @location(2) cubeIndex : f32,   // ★追加
};

struct VertexOutput {
    @builtin(position) pos : vec4,
    @location(0) normal : vec3,
    @location(1) cubeIndex : f32,   // ★追加
};

@vertex
fn vs_main(input : VertexInput) -> VertexOutput {
    var out : VertexOutput;
    var depth = (-input.position.z) / Range;
    out.pos = vec4(input.position.xy, clamp(depth, 0.0, 1.0), 1.0);
    out.normal = input.normal;
    out.cubeIndex = input.cubeIndex;   // ★追加
    return out;
}

@fragment
fn fs_main(
    @location(0) normal : vec3,
    @location(1) cubeIndex : f32
) -> @location(0) vec4 {


    // ★ 通常のフォンシェーディング
    var N = normalize(normal);
    var L = normalize(vec3(0.3, 0.5, -1.0));
    var diff = max(dot(N, L), 0.0);

    var ambient = 0.2;
    var brightness = ambient + (1.0 - ambient) * diff;

    var color = vec3(0.7, 1.0, 0.7) * brightness;
    if (cubeIndex == 1.0) {
      color = vec3(0.7, 1.0, 1.0) * brightness;
    }
    else if (cubeIndex == 2.0) {
      color = vec3(0.7, 0.7, 1.0) * brightness;
    }
    else if (cubeIndex == 3.0) {
      color = vec3(1.0, 0.7, 1.0) * brightness;
    }
    else if (cubeIndex == 4.0) {
      color = vec3(1.0, 0.7, 0.7) * brightness;
    }
    return vec4(color, 1.0);
}
`;

function makeFace(indices, zoom, sx, sy) {
  const v = [];
  for (var i of indices) v.push(norm(pp[i], zoom, sx, sy));
  v.push(norm(pp[indices[0]], zoom, sx, sy)); // 閉じる
  return v;
}

function GLoop(id, sw = 1) {
  var Vp;
  var Vrp;
  var Vpp;
  var Vtime;
  var Vadapter;
  var Vdevice;
  var Vcanvas;
  var Vcontext;
  var Vformatt;
  var VlinePipeline;
  var VtrianglePipeline;
  var VvertexBufferTri;
  var VvertexBufferWire;
  var VvertexCount;
  var Vzpos = 0;

//  if(sw === 1) {
    Vrp = [];
    Vpp=pp1;
    Vadapter=adapter1;
    Vdevice=device1;
    Vcanvas=canvas1;
    Vcontext=context1;
    Vformat=format1;
    VlinePipeline=linePipeline1;
    VtrianglePipeline=trianglePipeline1;
    VvertexBufferTri=vertexBufferTri1;
    VvertexBufferWire=vertexBufferWire1;
    VvertexCount = 0;
    time1 += Math.PI / 180;
    if (time1 >= Math.PI * 2) {
      time1 -= Math.PI * 2;
    }
    Vtime=time1;

    // 区間の z を取得
    const z0 = -instances[0].pos[2];
    const z1 = -instances[instances.length - 1].pos[2] * 1.25;
    const z2 = z1 * 0.75;

    // 0 → 1 の進行度
    var t = zpos1 / z1;

    // ★ イージング(滑らかにする)
    t = t * t * (3 - 2 * t);   // smoothstep

    // 進行速度
    const speed = 50;       // 好きな値で調整

    // ★ zpos1 を滑らかに進める
    zpos1 += speed * (1 - t);

    // ★ 終端でループ
    if (zpos1 >= z2) zpos1 = 0;

    Vzpos = zpos1;

    const scene = buildScene(Vtime, Vzpos);
    Vrp = scene.points;
    const Vskips = scene.skips;
//  }

  Vpp = [];

  for (var i = 0; i < Vrp.length; i++) {
    var VecWK = new N6LVector([1.0,Vrp[i][0],Vrp[i][1],Vrp[i][2]],true);
    VecWK = FrustumInf(VecWK);

    Vpp[i] = [];
    for (var j = 0; j < 3; j++) {
      Vpp[i][j] = VecWK.x[j + 1];
    }
  }


  const Vsx = 600, Vsy = 600;
  const Vzoom = 250;

  // 1キューブのエッジ定義(固定)
  const Vedges = [
    [4,5], [5,6], [6,7], [7,4],
    [0,1], [1,2], [2,3], [3,0],
    [0,4], [1,5], [2,6], [3,7],
  ];

  // ★ 複数キューブ対応:全キューブ分の頂点を作る
  const Vverts = [];

  // Vpp は「全キューブの頂点」が並んでいる
  // 1キューブ=8頂点なので、キューブ数は Vpp.length / 8
  const cubeCount = Vpp.length / 8;

  for (var ci = 0; ci < cubeCount; ci++) {

    // キューブ ci の先頭インデックス
    const base = ci * 8;

    // 12エッジを処理
    for (const [a, b] of Vedges) {

        // キューブ内の頂点番号に変換
        const ia = base + a;
        const ib = base + b;

        var v1 = norm(Vpp[ia], Vzoom, Vsx, Vsy);
        var v2 = norm(Vpp[ib], Vzoom, Vsx, Vsy);

        // 0ドットガード
        var dx = v2[0] - v1[0];
        var dy = v2[1] - v1[1];
        if (dx * dx + dy * dy < 1e-12) {
            v2[0] += 1.0;
        }

        // Z軸は ToLogAxis で圧縮
        Vverts.push(v1[0], v1[1], ToLogAxis(Vpp[ia][2]));
        Vverts.push(v2[0], v2[1], ToLogAxis(Vpp[ib][2]));

    }
  }

  const Vdata = new Float32Array(Vverts);
  // ★ ワイヤーフレームの頂点を書き込む
  Vdevice.queue.writeBuffer(VvertexBufferWire, 0, Vdata.buffer);


  // ★ 描画パス開始
  const Vencoder = Vdevice.createCommandEncoder();
  const VtextureView = Vcontext.getCurrentTexture().createView();

  var Vpass;
  if(sw === 1){
    var dtv;
    switch(sw){
    case 1: dtv = depthTextureView1; break;
    }
    Vpass = Vencoder.beginRenderPass({
      colorAttachments: [{
        view: VtextureView,
        loadOp: 'clear',
        storeOp: 'store',
        clearValue: { r: 0, g: 0, b: 0, a: 1 },
      }],
      depthStencilAttachment: {
        view: dtv,
        depthClearValue: 1.0,
        depthLoadOp: 'clear',
        depthStoreOp: 'store',
      },
    });
  }


  // ★ 複数キューブ対応:三角形描画
  {
    const baseNormals = {
        top:    [ 0,  0,  1 ],
        bottom: [ 0,  0, -1 ],
        front:  [ 0, -1,  0 ],
        back:   [ 0,  1,  0 ],
        right:  [ 1,  0,  0 ],
        left:   [-1,  0,  0 ],
    };

    const N1 = rotateNormal(baseNormals.top,    Vtime);
    const N2 = rotateNormal(baseNormals.bottom, Vtime);
    const N3 = rotateNormal(baseNormals.front,  Vtime);
    const N4 = rotateNormal(baseNormals.right,  Vtime);
    const N5 = rotateNormal(baseNormals.back,   Vtime);
    const N6 = rotateNormal(baseNormals.left,   Vtime);

    const Vtverts = [];

    for (var ci = 0; ci < cubeCount; ci++) {

        const base = ci * 8;

        // 頂点取得
        const v = [];
        const ax = [];
        for (var i = 0; i < 8; i++) {
            v[i]  = norm(Vpp[base + i], Vzoom, Vsx, Vsy);
            ax[i] = ToLogAxis(Vpp[base + i][2]);
        }

        // 上面 (4,5,6,7)
        Vtverts.push(
            v[4][0], v[4][1], ax[4], N1[0], N1[1], N1[2], ci + Vskips,
            v[5][0], v[5][1], ax[5], N1[0], N1[1], N1[2], ci + Vskips,
            v[6][0], v[6][1], ax[6], N1[0], N1[1], N1[2], ci + Vskips,
        );
        Vtverts.push(
            v[4][0], v[4][1], ax[4], N1[0], N1[1], N1[2], ci + Vskips,
            v[6][0], v[6][1], ax[6], N1[0], N1[1], N1[2], ci + Vskips,
            v[7][0], v[7][1], ax[7], N1[0], N1[1], N1[2], ci + Vskips,
        );

        // 下面 (0,1,2,3)
        Vtverts.push(
            v[0][0], v[0][1], ax[0], N2[0], N2[1], N2[2], ci + Vskips,
            v[2][0], v[2][1], ax[2], N2[0], N2[1], N2[2], ci + Vskips,
            v[1][0], v[1][1], ax[1], N2[0], N2[1], N2[2], ci + Vskips,
        );
        Vtverts.push(
            v[0][0], v[0][1], ax[0], N2[0], N2[1], N2[2], ci + Vskips,
            v[3][0], v[3][1], ax[3], N2[0], N2[1], N2[2], ci + Vskips,
            v[2][0], v[2][1], ax[2], N2[0], N2[1], N2[2], ci + Vskips,
        );

        // 前面 (0,1,5,4)
        Vtverts.push(
            v[0][0], v[0][1], ax[0], N3[0], N3[1], N3[2], ci + Vskips,
            v[1][0], v[1][1], ax[1], N3[0], N3[1], N3[2], ci + Vskips,
            v[5][0], v[5][1], ax[5], N3[0], N3[1], N3[2], ci + Vskips,
        );
        Vtverts.push(
            v[0][0], v[0][1], ax[0], N3[0], N3[1], N3[2], ci + Vskips,
            v[5][0], v[5][1], ax[5], N3[0], N3[1], N3[2], ci + Vskips,
            v[4][0], v[4][1], ax[4], N3[0], N3[1], N3[2], ci + Vskips,
        );

        // 右面 (1,2,6,5)
        Vtverts.push(
            v[1][0], v[1][1], ax[1], N4[0], N4[1], N4[2], ci + Vskips,
            v[2][0], v[2][1], ax[2], N4[0], N4[1], N4[2], ci + Vskips,
            v[6][0], v[6][1], ax[6], N4[0], N4[1], N4[2], ci + Vskips,
        );
        Vtverts.push(
            v[1][0], v[1][1], ax[1], N4[0], N4[1], N4[2], ci + Vskips,
            v[6][0], v[6][1], ax[6], N4[0], N4[1], N4[2], ci + Vskips,
            v[5][0], v[5][1], ax[5], N4[0], N4[1], N4[2], ci + Vskips,
        );

        // 後面 (2,3,7,6)
        Vtverts.push(
            v[2][0], v[2][1], ax[2], N5[0], N5[1], N5[2], ci + Vskips,
            v[3][0], v[3][1], ax[3], N5[0], N5[1], N5[2], ci + Vskips,
            v[7][0], v[7][1], ax[7], N5[0], N5[1], N5[2], ci + Vskips,
        );
        Vtverts.push(
            v[2][0], v[2][1], ax[2], N5[0], N5[1], N5[2], ci + Vskips,
            v[7][0], v[7][1], ax[7], N5[0], N5[1], N5[2], ci + Vskips,
            v[6][0], v[6][1], ax[6], N5[0], N5[1], N5[2], ci + Vskips,
        );

        // 左面 (3,0,4,7)
        Vtverts.push(
            v[3][0], v[3][1], ax[3], N6[0], N6[1], N6[2], ci + Vskips,
            v[0][0], v[0][1], ax[0], N6[0], N6[1], N6[2], ci + Vskips,
            v[4][0], v[4][1], ax[4], N6[0], N6[1], N6[2], ci + Vskips,
        );
        Vtverts.push(
            v[3][0], v[3][1], ax[3], N6[0], N6[1], N6[2], ci + Vskips,
            v[4][0], v[4][1], ax[4], N6[0], N6[1], N6[2], ci + Vskips,
            v[7][0], v[7][1], ax[7], N6[0], N6[1], N6[2], ci + Vskips,
        );
    }


var SKIP = true;
if(!SKIP) {
console.log("Vrp.length", Vrp.length);
console.log("Vpp.length", Vpp.length);
console.log("cubeCount", cubeCount);
console.log("wire verts", Vverts.length);      // 3 * 12 * cubeCount のはず
console.log("tri verts", Vtverts.length);      // 7 * 36 * cubeCount のはず
//checkValue(SKIP,"SKIP");
}

    const Vtdata = new Float32Array(Vtverts);
    Vdevice.queue.writeBuffer(VvertexBufferTri, 0, Vtdata.buffer);

    Vpass.setPipeline(VtrianglePipeline);
    Vpass.setVertexBuffer(0, VvertexBufferTri);

    // ★ 全キューブ分の三角形を描く
    Vpass.draw(cubeCount * 12 * 3, 1, 0, 0);
  }

  // ★ 線パイプラインに切り替え
  Vpass.setPipeline(VlinePipeline);

  // ★ 全キューブ分の線データを書き込む
  Vdevice.queue.writeBuffer(VvertexBufferWire, 0, Vdata.buffer);
  Vpass.setVertexBuffer(0, VvertexBufferWire);

  // ★ 複数キューブ対応:キューブ数 × 12本の線を描く
  const edgesPerCube = Vedges.length; // 12

  for (var ci = 0; ci < cubeCount; ci++) {

    // このキューブの線の開始オフセット
    const baseOffset = ci * edgesPerCube * 2;  
    // 1エッジ=2頂点なので ×2

    for (var ei = 0; ei < edgesPerCube; ei++) {
        const offset = baseOffset + ei * 2;
        Vpass.draw(2, 1, offset, 0);
    }
  }

  Vpass.end();
  Vdevice.queue.submit([Vencoder.finish()]);

  TMan.timer[id].setalerm(() => GLoop(id,sw), 50);
}

function buildScene(time, Vzpos) {

    const baseCube = [
      [10,10,10],[-10,10,10],[-10,-10,10],[10,-10,10],
      [10,10,-10],[-10,10,-10],[-10,-10,-10],[10,-10,-10]
    ];

    const allPoints = [];
    var skips = 0;
    // --------------------------------------------------
    // ★ ① 区間判定(scaleNow を決める)だけを行う
    // --------------------------------------------------
    var scaleNow = 1.0;

    for (var i = 0; i < instances.length - 1; i++) {

        const z0 = -instances[i].pos[2];
        const z1 = -instances[i+1].pos[2];

        if (Vzpos >= z0 && Vzpos < z1) {

            const t = (Vzpos - z0) / (z1 - z0);

            scaleNow =
                instances[i].scale * (1 - t) +
                instances[i+1].scale * t;

            break;
        }
    }


    // --------------------------------------------------
    // ★ ② キューブ生成(描画判定+頂点生成)
    // --------------------------------------------------
    for (const inst of instances) {

        // キューブ中心の z
        const centerZ = inst.pos[2] - (40 - Vzpos);

        // 正なら描画しない
        if (0 < centerZ) { skips++; continue; }
         // ★ キューブ固有の位相(ラジアン)
        const rad = inst.phase * Math.PI / 180

;       // 頂点生成
        for (const pt of baseCube) {

            var p = [
                pt[0] * inst.scale,
                pt[1] * inst.scale,
                pt[2] * inst.scale
            ];

            // ★ キューブ固有の位相を加えた回転
            p = rot([0,0,-1], 1 * time + rad, p);
            p = rot([0,1,0], 2 * time + rad, p);
            p = rot([1,0,0], 3 * time + rad, p);

            p[0] += inst.pos[0];
            p[1] += inst.pos[1];
            p[2] += inst.pos[2];

            p[2] -= (40 - Vzpos);

            // 区間スケールで逆縮小
            p[0] /= scaleNow;
            p[1] /= scaleNow;
            p[2] /= scaleNow;

            allPoints.push(p);
        }
    }

    return { points: allPoints, skips: skips };
}

// 回転後の法線を作るヘルパ
function rotateNormal(n, time) {
  var v = n;
  v = rot([0,0,-1], 1 * time, v);
  v = rot([0,1,0], 2 * time, v);
  v = rot([1,0,0], 3 * time, v);
  return v;
}
function norm(pt, zoom, sx, sy) {
  const x = pt[0] * zoom + sx / 2;
  const y = pt[1] * zoom + sy / 2;
  // WebGPU は左上原点ではなく NDC(-1~+1)
  const nx = (x / sx) * 2 - 1;
  const ny = -((y / sy) * 2 - 1); // Y軸反転
  return [nx, ny];
}
function rot(a,th,p) {
  var c = Math.cos(th),s = Math.sin(th);
  return [(c+a[0]*a[0]*(1-c))*p[0]+(a[0]*a[1]*(1-c)-a[2]*s)*p[1]+(a[0]*a[2]*(1-c)+a[1]*s)*p[2],
          (a[1]*a[0]*(1-c)+a[2]*s)*p[0]+(c+a[1]*a[1]*(1-c))*p[1]+(a[1]*a[2]*(1-c)-a[0]*s)*p[2],
          (a[2]*a[0]*(1-c)-a[1]*s)*p[0]+(a[2]*a[1]*(1-c)+a[0]*s)*p[1]+(c+a[2]*a[2]*(1-c))*p[2]];
}
function frustum(n,p) {
  var z = p[2];
  return [(n/z)*p[0], (n/z)*p[1]];
}

// 無限対数軸 → 通常軸
function FromLogAxis(x) {
    if (isNaN(x) || Range <= Math.abs(x)) {
        return Infinity;
    }

    var v = Range / (Math.log(Math.abs(x) / Range) / Math.log(1.0 / Base));

    if (isNaN(v) || !isFinite(v)) {
        v = Infinity;
    }

    if (x < 0.0) v *= -1.0;
    return v;
}

// 通常軸 → 無限対数軸
function ToLogAxis(x) {
    var c = Range / Math.abs(x);

    if (isNaN(c) || !isFinite(c)) {
        return 0.0;
    }

    var v = Range * Math.pow(1.0 / Base, c);

    if (isNaN(v) || !isFinite(v)) {
        v = 0.0;
    }

    if (x < 0.0) v *= -1.0;
    return v;
}

// 無限遠透視射影(Range スケール)
function FrustumInf0(v) {
    var VecWK2 = v.clone().Homogeneous();
    var VecWK  = new N6LVector(4);

    VecWK.x[0] = 1.0;
    VecWK.x[1] = (Range - ToLogAxis(-VecWK2.x[3])) * VecWK2.x[1] / Range;
    VecWK.x[2] = (Range - ToLogAxis(-VecWK2.x[3])) * VecWK2.x[2] / Range;
    VecWK.x[3] = VecWK2.x[3];
    return VecWK.SetHomo(true);
}

// 逆無限遠透視射影(Range スケール)
function InvFrustumInf0(v, z) {
    var VecWK2 = v.clone().Homogeneous();
    var VecWK  = new N6LVector(4);

    VecWK.x[0] = 1.0;
    VecWK.x[1] = Range * VecWK2.x[1] / (Range - ToLogAxis(-z));
    VecWK.x[2] = Range * VecWK2.x[2] / (Range - ToLogAxis(-z));
    VecWK.x[3] = z / Range;
    return VecWK.SetHomo(true);
}

// 無限遠透視射影(正規化)
function FrustumInf(v) {
    var VecWK = FrustumInf0(v);
    return VecWK.Div(Range).SetHomo(true);
}

// 逆無限遠透視射影(正規化)
function InvFrustumInf(v, z) {
    var VecWK = InvFrustumInf0(v, z);
    return VecWK.Mul(Range).SetHomo(true);
}

async function init1() {
  p1 = [[10,10,10],[-10,10,10],[-10,-10,10],[10,-10,10],
       [10,10,-10],[-10,10,-10],[-10,-10,-10],[10,-10,-10]];
  time1 = 0;

  if (!navigator.gpu) throw new Error('WebGPU not available');

  adapter1 = await navigator.gpu.requestAdapter();
  if (!adapter1) throw new Error('No suitable GPU adapter found');

  device1 = await adapter1.requestDevice();

  canvas1 = document.getElementById('cnv1');
  context1 = canvas1.getContext('webgpu');

  format1 = navigator.gpu.getPreferredCanvasFormat();

  context1.configure({
    device: device1,
    format: format1,
    alphaMode: 'opaque',
  });

  depthTexture1 = device1.createTexture({
    size: { width: 600, height: 600, depthOrArrayLayers: 1 },
    format: 'depth32float',
    usage: GPUTextureUsage.RENDER_ATTACHMENT,
  });
  depthTextureView1 = depthTexture1.createView();


  const shaderModule1 = device1.createShaderModule({ code: phongshaderCode });
  const triangleshaderModule1 = device1.createShaderModule({ code: trianglephongshaderCode });

  trianglePipeline1 = device1.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: triangleshaderModule1,
      entryPoint: 'vs_main',
      buffers: [{
        arrayStride: 7 * 4, // 例: vec3 position + vec3 normal
        attributes: [
          { shaderLocation: 0, offset: 0,        format: 'float32x3' }, // position
          { shaderLocation: 1, offset: 3 * 4,    format: 'float32x3' }, // normal
          { shaderLocation: 2, offset: 24, format: 'float32'   }, // cubeIndex
        ]
      }],
    },
    fragment: {
      module: triangleshaderModule1,
      entryPoint: 'fs_main',
      targets: [{
          format: format1,
          blend: {
              color: {
                  srcFactor: 'one',
                  dstFactor: 'zero',
                  operation: 'add',
              },
              alpha: {
                  srcFactor: 'one',
                  dstFactor: 'zero',
                  operation: 'add',
              },
          },
      }],
    },
    primitive: {
      topology: 'triangle-list',
      cullMode: 'back',
      frontFace: 'ccw',
    },
    depthStencil: {
      format: 'depth32float',
      depthWriteEnabled: true,
      depthCompare: 'less-equal',
    },
  });
  linePipeline1 = device1.createRenderPipeline({
    layout: 'auto',
    vertex: {
      module: shaderModule1,
      entryPoint: 'vs_main',
      buffers: [{
        arrayStride: 3 * 4,
        attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
      }],
    },
    fragment: {
      module: shaderModule1,
      entryPoint: 'fs_main',
      targets: [{
          format: format1,
          blend: {
              color: {
                  srcFactor: 'one',
                  dstFactor: 'zero',
                  operation: 'add',
              },
              alpha: {
                  srcFactor: 'one',
                  dstFactor: 'zero',
                  operation: 'add',
              },
          },
      }],
    },
    primitive: {
      topology: 'line-list',
    },
    depthStencil: {
      format: 'depth32float',
      depthWriteEnabled: false,
      depthCompare: 'less-equal',
    },
  });



  const cubeCount = instances.length;

  // ★ ワイヤー用バッファ
  const wireStride1 = 3 * 4;      // float32x3
  const wireVertsPerCube1 = 12 * 2; // 12 edges × 2 vertices
  const wireSize1 = cubeCount * wireVertsPerCube1 * wireStride1;

  vertexBufferWire1 = device1.createBuffer({
    size: wireSize1,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });

  // ★ 三角形用バッファ
  const triStride1 = 7 * 4;        // float32x7 = 28 bytes
  const triVertsPerCube1 = 36 * 3; // 36 triangles × 3 vertices
  const triSize1 = cubeCount * triVertsPerCube1 * triStride1;

  vertexBufferTri1 = device1.createBuffer({
    size: triSize1,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });

  zpos1 = 1;
}
function checkValue(val, name) {
    var SKIP = true;
    if(SKIP) return val;
    if (isNaN(val) || !isFinite(val) || Math.abs(val) < 1e-12) {
        console.log(`[発散検出] ${name} が不正な値になりました:`, val);
//        debugger; // ここで一時停止させられます
    }
    return val;
}
function checkNorm(val, name) {
    var SKIP = true;
    if(SKIP) return val;
    if (val < -1.0 || 1.0 < val) {
        console.log(`[発散検出] ${name} が不正な値になりました:`, val);
//        debugger; // ここで一時停止させられます
    }
    return val;
}
async function enter() {
  await init1();
  TMan.add();
  TMan.timer[0].setalerm(function() { GLoop(0,1); }, 50);
  return true;
}









<< WEBGPUテストと無限透視射影 : 工事中 >>





戻る