はじめに
前回シンプルな3Dをやったのでそれを援用して3Dをやりましょう。楽しみですね。
前回の記事でやったことをそのままGLSLに落とします。なのでほぼ省略になります。内容は前回の記事を参考にしてください:
コード全文
// 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]
);
回転行列
ロドリゲスの行列を露骨に用意しました。ベクトル(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は配列で渡せば問題ないですね。ここでちょっとシェーダーの方を見てみましょうか。
シェーダー
#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を掛ける方向は右です。内部では右乗算になるからです。じゃあ、次。
#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軸の向きがそうなっているのを確認できると思いますし、この記事の画像でもそうなっています。その方が分かりやすいからです。