0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

p5.jsでシンプルな3D(前回の続き)

Last updated at Posted at 2025-10-15

はじめに

 前回シンプルな3Dをやったのでそれを援用して3Dをやりましょう。楽しみですね。

wxazxaxaxxxwxs2x.png

 前回の記事でやったことをそのままGLSLに落とします。なのでほぼ省略になります。内容は前回の記事を参考にしてください:

コード全文

p5 3D.

// 3D.
// p5だとらくちんですね。

const vs =
`#version 300 es
in vec3 aPosition;
in vec3 aNormal;
uniform mat3 uModel;
uniform vec3 uEye;
uniform vec3 uViewX;
uniform vec3 uViewY;
uniform vec3 uViewZ;
uniform vec4 uProj; // fov,aspect,near,far
out vec3 vNormal;
void main(){
  vNormal = aNormal;
  vec3 p = aPosition * uModel;
  p -= uEye;
  vec3 q = vec3(dot(p, uViewX), dot(p, uViewY), dot(p, uViewZ));
  float fov = uProj.x;
  float aspect = uProj.y;
  float near = uProj.z;
  float far = uProj.w;
  float factor = 1.0/tan(fov/2.0);
  gl_Position = vec4(
    q.x * aspect * factor,
    q.y * factor,
    ((near+far)/(near-far)) * q.z + (2.0*near*far)/(near-far),
    -q.z
  );
}
`;

const fs =
`#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main(){
  fragColor = vec4(0.5+0.5*vNormal, 1.0);
}
`;

function setup(){
  createCanvas(400,400,WEBGL);

  const geom = new p5.Geometry();
  const cv = (x,y,z) => createVector(x,y,z);
  //geom.vertices.push(createVector(-50,-50,0), createVector(50,-50,0), createVector(-50,50,0), createVector(50,50,0));
  //geom.vertexNormals.push(createVector(0,0,1),createVector(0,0,1),createVector(0,0,1),createVector(0,0,1));
  //geom.faces.push([0,1,2],[2,1,3]);
  geom.vertices.push(
    cv(-50,-50,50), cv(50,-50,50), cv(-50,50,50), cv(50,50,50),
    cv(-50,-50,-50), cv(50,-50,-50), cv(-50,50,-50), cv(50,50,-50)
  );
  geom.vertexNormals = geom.vertices.map((v) => v.copy().normalize());
  geom.faces.push(
    [0,1,2], [2,1,3], [0,4,5], [0,5,1], [1,5,7], [1,7,3],
    [2,6,4], [2,4,0], [6,2,3], [6,3,7], [4,6,7], [4,7,5]
  );

  const eye = createVector(0,0,200*sqrt(3));
  const center = createVector(0,0,0);
  const near = 20*sqrt(3);
  const far = 2000*sqrt(3);
  const aspect = width/height;
  const fov = PI/3;

  const v0 = center.copy().sub(eye).normalize(); // 視線方向単位ベクトル
  const up = createVector(0,1,0); // 暫定上方向ベクトル
  const viewX = v0.cross(up).normalize();
  const viewZ = v0.copy().mult(-1).normalize();
  const viewY = viewZ.cross(viewX).normalize();

  const sh = createShader(vs, fs);
  shader(sh);

  const gl = drawingContext;
  gl.enable(gl.CULL_FACE);

  const loopFunction = () => {
    const t = frameCount*TAU/240;
    background(0);
    const axis = createVector(1,1,1).normalize();
    sh.setUniform("uModel", [
      cos(t) + (1-cos(t))*axis.x*axis.x, (1-cos(t))*axis.x*axis.y - sin(t)*axis.z, (1-cos(t))*axis.z*axis.x +sin(t)*axis.y,
      (1-cos(t))*axis.x*axis.y + sin(t)*axis.z, cos(t) + (1-cos(t))*axis.y*axis.y, (1-cos(t))*axis.y*axis.z - sin(t)*axis.x,
      (1-cos(t))*axis.z*axis.x - sin(t)*axis.y, (1-cos(t))*axis.y*axis.z + sin(t)*axis.x, cos(t) + (1-cos(t))*axis.z*axis.z
    ]);
    sh.setUniform("uEye", [eye.x, eye.y, eye.z]);
    sh.setUniform("uViewX", [viewX.x, viewX.y, viewX.z]);
    sh.setUniform("uViewY", [viewY.x, viewY.y, viewY.z]);
    sh.setUniform("uViewZ", [viewZ.x, viewZ.y, viewZ.z]);
    sh.setUniform("uProj", [fov, aspect, near, far]);

    model(geom);
  }

  draw = loopFunction;
}

カメラの設定

 こんな感じ:

  const eye = createVector(0,0,200*sqrt(3));
  const center = createVector(0,0,0);
  const near = 20*sqrt(3);
  const far = 2000*sqrt(3);
  const aspect = width/height;
  const fov = PI/3;

 視点は200*sqrt(3)だけ z 軸の上方、注視点は原点、ニアクリップは距離の1/10, ファークリップは10倍、視野角は60°.

ビュー座標系を作る

 前回やったのと全く同じ計算をしましょう。

  const v0 = center.copy().sub(eye).normalize(); // 視線方向単位ベクトル
  const up = createVector(0,1,0); // 暫定上方向ベクトル
  const viewX = v0.cross(up).normalize();
  const viewZ = v0.copy().mult(-1).normalize();
  const viewY = viewZ.cross(viewX).normalize();

まあ今回はシェーダー内部で諸々の計算をするので、こっちでは何にもしません。uniformで送るだけです。

ジオメトリ

 p5.Geometryは便利ですね。平面も用意したんですが、まあ立方体の方がいいですかね。

  const geom = new p5.Geometry();
  const cv = (x,y,z) => createVector(x,y,z);
  //geom.vertices.push(createVector(-50,-50,0), createVector(50,-50,0), createVector(-50,50,0), createVector(50,50,0));
  //geom.vertexNormals.push(createVector(0,0,1),createVector(0,0,1),createVector(0,0,1),createVector(0,0,1));
  //geom.faces.push([0,1,2],[2,1,3]);
  geom.vertices.push(
    cv(-50,-50,50), cv(50,-50,50), cv(-50,50,50), cv(50,50,50),
    cv(-50,-50,-50), cv(50,-50,-50), cv(-50,50,-50), cv(50,50,-50)
  );
  geom.vertexNormals = geom.vertices.map((v) => v.copy().normalize());
  geom.faces.push(
    [0,1,2], [2,1,3], [0,4,5], [0,5,1], [1,5,7], [1,7,3],
    [2,6,4], [2,4,0], [6,2,3], [6,3,7], [4,6,7], [4,7,5]
  );

立方体.png

回転行列

 ロドリゲスの行列を露骨に用意しました。ベクトル(1,1,1)の周りに回しましょう。

\begin{pmatrix}
\cos\theta + (1-\cos\theta)x^2 & (1-\cos\theta)xy - (\sin\theta)z & (1-\cos\theta)xz + (\sin\theta)y \\
(1-\cos\theta)xy + (\sin\theta)z & \cos\theta + (1-\cos\theta)y^2 & (1-\cos\theta)yz - (\sin\theta)x \\
(1-\cos\theta)xz - (\sin\theta)y & (1-\cos\theta)yz + (\sin\theta)x & \cos\theta + (1-\cos\theta)z^2
\end{pmatrix}
    const axis = createVector(1,1,1).normalize();
    sh.setUniform("uModel", [
      cos(t) + (1-cos(t))*axis.x*axis.x, (1-cos(t))*axis.x*axis.y - sin(t)*axis.z, (1-cos(t))*axis.z*axis.x +sin(t)*axis.y,
      (1-cos(t))*axis.x*axis.y + sin(t)*axis.z, cos(t) + (1-cos(t))*axis.y*axis.y, (1-cos(t))*axis.y*axis.z - sin(t)*axis.x,
      (1-cos(t))*axis.z*axis.x - sin(t)*axis.y, (1-cos(t))*axis.y*axis.z + sin(t)*axis.x, cos(t) + (1-cos(t))*axis.z*axis.z
    ]);

行列uniformは配列で渡せば問題ないですね。ここでちょっとシェーダーの方を見てみましょうか。

シェーダー

vs
#version 300 es
in vec3 aPosition;
in vec3 aNormal;
uniform mat3 uModel;
uniform vec3 uEye;
uniform vec3 uViewX;
uniform vec3 uViewY;
uniform vec3 uViewZ;
uniform vec4 uProj; // fov,aspect,near,far
out vec3 vNormal;
void main(){
  vNormal = aNormal;
  vec3 p = aPosition * uModel;
  p -= uEye;
  vec3 q = vec3(dot(p, uViewX), dot(p, uViewY), dot(p, uViewZ));
  float fov = uProj.x;
  float aspect = uProj.y;
  float near = uProj.z;
  float far = uProj.w;
  float factor = 1.0/tan(fov/2.0);
  gl_Position = vec4(
    q.x * aspect * factor,
    q.y * factor,
    ((near+far)/(near-far)) * q.z + (2.0*near*far)/(near-far),
    -q.z
  );
}

 完全に前回と同じ計算をしています。前回最後に出したのはgl_Positionに相当するんですよ...wで割らなくていいのかって?それはね、シェーダーがやってくれるから必要ないんですよ。それ想定して組んであるので。だから4つの成分を出すところで終わりです。なお、ローカル法線をフラグメントシェーダーに渡しているのは彩色のためです。uModelを掛ける方向は右です。内部では右乗算になるからです。じゃあ、次。

fs
#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main(){
  fragColor = vec4(0.5+0.5*vNormal, 1.0);
}

いわゆる法線彩色ってやつです。0.5倍して0.5を足して終わり。ライティングは本質的でないので。気になる人は挑戦してみましょう。

カリング

 まあ必要ないかもしれませんが、一応、表描画であることを確認するために入れました。ポリゴンが多くてなおかつ表面しか描画しなくていい場合、これを実行して負荷を減らしたりします。カリング関連のp5の処理は存在しないのでdrawingContextを使います。ここだけです。

  const gl = drawingContext;
  gl.enable(gl.CULL_FACE);

 なお、p5の通常の3D描画の場合はフロントをカリングしないといけないんですが、この記事では素直に射影の処理をやっているので(前回参照)、デフォルト(バックカリング)で適用しています。

uniformをぶちこむ

 配列で渡せば問題ありません。

    sh.setUniform("uEye", [eye.x, eye.y, eye.z]);
    sh.setUniform("uViewX", [viewX.x, viewX.y, viewX.z]);
    sh.setUniform("uViewY", [viewY.x, viewY.y, viewY.z]);
    sh.setUniform("uViewZ", [viewZ.x, viewZ.y, viewZ.z]);
    sh.setUniform("uProj", [fov, aspect, near, far]);

あとはmodelすれば全部アトリビュートに関してよしなにしてくれます。お疲れ様でした。

    model(geom);

おわりに

 p5は便利ですね~。

 ここまでお読みいただいてありがとうございました。

行列は?

 モデルで使いましたが...ええ、知っています。ビューと射影の処理も行列で書けるんです。ただ、仕組みが分かってれば翻訳は容易なので割愛しました。

そういえばy反転は?

 やってません...というかあれは邪道なんですよね。WebGLは x 右、 y 上、 z 手前がスタンダードです。Threeもそうしています。郷に入りては郷に従えというやつです。素直にやりました。というかp5はあそこをいじりすぎなんですよね...混乱したくないのでやりませんでした。前回の記事で射影のところを見ればわかりますがy軸の向きがそうなっているのを確認できると思いますし、この記事の画像でもそうなっています。その方が分かりやすいからです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?