はじめに
p5.jsにはbaseMaterialShader()という関数があって、これを使うとライティングシェーダーをすごく簡単に改変できます。すばらしい機能ですが、現在はexperimentalという位置づけなので、今後変更があるかもしれません。それを踏まえたうえで読んでいただければ幸いです。
baseMaterialShader()
今回は簡単なフォグを実装します。遠くにあるものが背景と同化する感じの機能です。厳密にはフレームバッファを使ってやったりするのですが(ディファードレンダリングとかいうらしい...詳しくないので分かんないです...)、ここで扱うのはどっちかというとインチキに近いものです。それでもまあ、ある程度それっぽくはなります。
コード全文
let sh;
let bg;
function preload(){
bg = loadImage("https://inaridarkfox4231.github.io/assets/backgrounds/ocean2.JPG");
}
function setup() {
createCanvas(800, 640, WEBGL);
pixelDensity(1);
camera(500, 500, 500, 0,0,0,0,0,-1);
perspective(PI/3,width/height,50*sqrt(3),5000*sqrt(3));
sh = baseMaterialShader().modify({
'vertexDeclarations':`
OUT vec4 vNDC;
`,
'void afterVertex':`(){
vec4 p = uModelViewMatrix * vec4(aPosition, 1.0);
vNDC = uProjectionMatrix * p;
}`,
'fragmentDeclarations':`
IN vec4 vNDC;
uniform vec2 uFogParams;
`,
'vec4 getFinalColor':`(vec4 color){
float depth = 0.5 + 0.5 * vNDC.z/vNDC.w;
float depthAlpha = smoothstep(uFogParams.y, uFogParams.x, depth);
color *= vec4(depthAlpha);
return color;
}`
});
const gl = this._renderer.GL;
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT);
}
function draw() {
orbitControl(1,1,1,{freeRotation:true});
setBackground();
shader(sh);
sh.setUniform("uFogParams", [0.88, 0.92]);
lights();
fill(255, 128, 64);
specularMaterial(128);
noStroke();
for(let x=-2; x<=2; x++){
for(let y=-2; y<=2; y++){
for(let z=-2; z<=2; z++){
push();
translate(x*160, y*160, z*160);
rotateX(frameCount*TAU/120);
rotateY(frameCount*TAU/240);
torus(40, 20, 48);
pop();
}
}
}
}
function setBackground(){
resetShader();
push();
camera(0,0,1,0,0,0,0,1,0);
ortho(-1,1,-1,1,0,1);
texture(bg);
plane(2);
pop();
}
実行結果:
こんな感じで遠くにあるものがぼかされて背景と同化します。近くにあるものは普通に見えるわけです。背景は、夏に浮島海岸ってところで潜って撮った写真です(どうでもいい)。
おさかな!🐟
シェーダーの改変
フォグの実装に使うのは正規化デバイス座標の z 成分と w 成分です。ここから補間された深度値が計算されるのでそれを使います。
sh = baseMaterialShader().modify({
'vertexDeclarations':`
OUT vec4 vNDC;
`,
'void afterVertex':`(){
vec4 p = uModelViewMatrix * vec4(aPosition, 1.0);
vNDC = uProjectionMatrix * p;
}`,
'fragmentDeclarations':`
IN vec4 vNDC;
uniform vec2 uFogParams;
`,
'vec4 getFinalColor':`(vec4 color){
float depth = 0.5 + 0.5 * vNDC.z/vNDC.w;
float depthAlpha = smoothstep(uFogParams.y, uFogParams.x, depth);
color *= vec4(depthAlpha);
return color;
}`
});
正規化デバイス座標をラスタライズして、そこから深度値を計算します。それをユニフォームの閾値でsmoothstepにより補間してalphaの値を計算、それをfinalColorにまるごと掛け算します。
以上です!(めちゃ簡単)
メイン描画
背景についてはちょっとした裏技を使っています。
function setBackground(){
resetShader();
push();
camera(0,0,1,0,0,0,0,1,0);
ortho(-1,1,-1,1,0,1);
texture(bg);
plane(2);
pop();
}
まずシェーダーを戻しておきます。背景用にカメラを用意するのがめんどくさいので、直接カメラをいじって背景としています。
メインループはこんな感じですね:
function draw() {
orbitControl(1,1,1,{freeRotation:true});
setBackground();
shader(sh);
sh.setUniform("uFogParams", [0.88, 0.92]);
lights();
fill(255, 128, 64);
specularMaterial(128);
noStroke();
for(let x=-2; x<=2; x++){
for(let y=-2; y<=2; y++){
for(let z=-2; z<=2; z++){
push();
translate(x*160, y*160, z*160);
rotateX(frameCount*TAU/120);
rotateY(frameCount*TAU/240);
torus(40, 20, 48);
pop();
}
}
}
}
今回は上下のないスケッチなので堂々とfreeRotationしています。フォグのパラメータは、これをきちんと指定するのは実は非常にめんどくさいんですが、通常のセッティングなら0.88と0.92でだいたいそれっぽくなります。境界をもっときつくしたい場合は値の差を小さくすればいいです(逆に大きくするとボケ部分が広くなります)。あとはトーラスを125個描いてるだけです。描画順とか色々考慮しないといけない?
簡易版なのでそこまで考えられないです。ごめんなさい...
さすがにそのままやると若干ボロが出てしまうので、カリングだけいじって、少しでもマシに見える工夫をしています。
const gl = this._renderer.GL;
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT);
なぜFRONTをcull(=摘み取る)しているかというとp5は射影行列の向きが逆だからです。へそ曲がりめ。
おわりに
フレームバッファを使ったフォグでもいいんですが、簡易版でもある程度それっぽくはなるというお話でした。ここでは深度値を使いましたが、距離を使うことももちろん可能です。やり方は大体一緒です。閾値のところを工夫するだけなので興味があったら取り組んでみるといいと思います。wgldさんのこれも参考になります。
距離フォグ
ここまでお読みいただいてありがとうございました。