はじめに
p5.jsには近年、baseMaterialShader()という関数ができました。これを使うと既存のshaderを改変することができます。今回はこれを使ってキューブマップにより背景を作ってみたいと思います。
baseMaterialShader()
いつもお世話になっているサイト:
キューブ環境マッピング
しかしキューブマップは実装されていないので、直接法線情報から取得するやり方でやります。テクスチャ画像を6枚用意するだけですから、法線情報からダイレクトに出すことは可能です。
modifyで処理を完結させられるようになった都合上、大幅に内容を更新しました。
ロードマップ
p5.jsはx軸が右、y軸が下、z軸が手前です。キューブマップがどのように実装されているかというと、これはxp, xn, yp, yn, zp, znという6枚のテクスチャの集合です。ざっくり言うと。それらの相互関係ですが、上のサイトにあるように、
(お借りしました)
このようになっています。これはどういうことかというと、立方体があるとして、そこにこれを外から見るように貼り付けるわけです。なお、通常webglの座標系は x 軸が右、 y 軸が上、 z 軸が手前です。そういうわけで、これを立方体の展開図とし、今見えている画像が「外側」として立方体を組み立てると、そうなります。 p はpositive(正方向)、 n はnegative(負方向)というわけですね。それが仕様です。
今回利用するキューブマップは、こちらのサイトの物をお借りしています:
https://www.humus.name/index.php?page=Textures&start=80
こちらの素材をダウンロードすると、posx,negx,posy,negy,posz,negzという6枚のテクスチャが得られます。これらがすなわち上の図のPX,NX,PY,NY,PZ,NZに対応します。
どっちが上でもいいわけですが、自分はx,y,zが先の方が好きなので、以下のコードでは逆になっています。
コード全文
コードは以下です。
p5 cubemap
// cubemapping: https://wgld.org/d/webgl/w044.html
// 素材:https://www.humus.name/index.php?page=Textures&start=80
// の、Bridge2ですね。
// 2024-11-29
// modifyで完結するように追記
/*
Author
======
This is the work of Emil Persson, aka Humus.
http://www.humus.name
License
=======
This work is licensed under a Creative Commons Attribution 3.0 Unported License.
http://creativecommons.org/licenses/by/3.0/
*/
const skyCubeMaps = {};
let myShader;
// --------------------- preload ------------------- //
function preload(){
skyCubeMaps.xp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXP.jpg");
skyCubeMaps.xn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeXN.jpg");
skyCubeMaps.yp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYP.jpg");
skyCubeMaps.yn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeYN.jpg");
skyCubeMaps.zp = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZP.jpg");
skyCubeMaps.zn = loadImage("https://inaridarkfox4231.github.io/assets/cubeMaps/bridgeZN.jpg");
}
function setup() {
createCanvas(window.innerWidth, window.innerHeight, WEBGL);
createCubemapShader();
}
function draw() {
orbitControl();
background(0);
shader(myShader);
noLights();
noStroke();
fill(255);
myShader.setUniform("uTextureXP", skyCubeMaps.xp);
myShader.setUniform("uTextureXN", skyCubeMaps.xn);
myShader.setUniform("uTextureYP", skyCubeMaps.yp);
myShader.setUniform("uTextureYN", skyCubeMaps.yn);
myShader.setUniform("uTextureZP", skyCubeMaps.zp);
myShader.setUniform("uTextureZN", skyCubeMaps.zn);
sphere(2000);
resetShader();
lights();
fill(255);
rotateX(frameCount*TAU/120);
rotateY(frameCount*TAU/180);
torus(100, 20, 24);
}
function createCubemapShader(){
const sh = baseMaterialShader();
// これでいいみたいですね。まあ仕方ないな。
myShader = sh.modify({
vertexDeclarations: `OUT vec3 vLocalNormal;`,
fragmentDeclarations: `
IN vec3 vLocalNormal;
uniform sampler2D uTextureXP;
uniform sampler2D uTextureXN;
uniform sampler2D uTextureYP;
uniform sampler2D uTextureYN;
uniform sampler2D uTextureZP;
uniform sampler2D uTextureZN;
vec4 getXP(sampler2D tex, in vec3 n){
vec2 uv = n.zy/n.xx;
uv.x = 0.5 - 0.5*uv.x;
uv.y = 0.5 + 0.5*uv.y;
return texture(tex, uv);
}
vec4 getXN(sampler2D tex, in vec3 n){
vec2 uv = -n.zy/n.xx;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getYP(sampler2D tex, in vec3 n){
vec2 uv = -n.xz/n.yy;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getYN(sampler2D tex, in vec3 n){
vec2 uv = n.xz/n.yy;
uv.x = 0.5 + 0.5*uv.x;
uv.y = 0.5 - 0.5*uv.y;
return texture(tex, uv);
}
vec4 getZP(sampler2D tex, in vec3 n){
vec2 uv = n.xy/n.zz;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getZN(sampler2D tex, in vec3 n){
vec2 uv = -n.xy/n.zz;
uv.x = 0.5 - 0.5*uv.x;
uv.y = 0.5 + 0.5*uv.y;
return texture(tex, uv);
}
vec4 getCubeTexture(sampler2D XP, sampler2D XN, sampler2D YP, sampler2D YN, sampler2D ZP, sampler2D ZN, in vec3 normal){
vec3 n = normalize(normal);
// これを実行するとこっちと同じ見た目になる:
// https://www.humus.name/index.php?page=Cubemap&item=Bridge2
// cubemapは基本的に外から見た場合の割り当てなので
// 中から見る分には鏡写しになる
// 文字を置くと分かる
// n.x=-n.x; // まあ背景に使う分には特に問題ないかと
if(n.x >= abs(n.y) && n.x >= abs(n.z)){
return getXP(XP, n);
}else if(n.x <= -abs(n.y) && n.x <= -abs(n.z)){
return getXN(XN, n);
}else if(n.y <= -abs(n.x) && n.y <= -abs(n.z)){
return getYP(YP, n);
}else if(n.y >= abs(n.x) && n.y >= abs(n.z)){
return getYN(YN, n);
}else if(n.z >= abs(n.x) && n.z >= abs(n.y)){
return getZP(ZP, n);
}else if(n.z <= -abs(n.x) && n.z <= -abs(n.y)){
return getZN(ZN, n);
}
return vec4(0.0);
}
`,
'void afterVertex':`(){vLocalNormal = aNormal;}`,
'vec4 getFinalColor':`(vec4 color){
// なお、ルーチン内部でuniform変数を使うのは基本的にあまりいいマナーでは
// 無いと私は考えています。汎用性が下がるんで。
return getCubeTexture(uTextureXP, uTextureXN, uTextureYP, uTextureYN, uTextureZP, uTextureZN, vLocalNormal);
}`
});
}
baseMaterialShaderの利用
vertexDeclarationsは今回、キューブマッピングに使うローカル法線の譲渡だけです。
fragmentDeclarationsで、varyingの処理と、sampler2Dが6枚分、それにルーチンの用意をしています。言い忘れていましたがvertex/fragmentDeclarationsは内容がそのままヘッダになる仕様のようで、つまりルーチンを普通に書けます。キューブマップの取得に使う関数でuniformを使いたくないので、そうしています。
法線情報をテクスチャに変換する
具体的にはこうします。
つまり組み立てて立方体になるところをイメージして、法線が立方体に突き刺さるんで、その位置をuvに変換してテクスチャ採取すればいいわけです。というかcubemapがそういうことをしているようです。詳しくは分かりませんが。
uniform sampler2D uTextureXP;
uniform sampler2D uTextureXN;
uniform sampler2D uTextureYP;
uniform sampler2D uTextureYN;
uniform sampler2D uTextureZP;
uniform sampler2D uTextureZN;
vec4 getXP(sampler2D tex, in vec3 n){
vec2 uv = n.zy/n.xx;
uv.x = 0.5 - 0.5*uv.x;
uv.y = 0.5 + 0.5*uv.y;
return texture(tex, uv);
}
vec4 getXN(sampler2D tex, in vec3 n){
vec2 uv = -n.zy/n.xx;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getYP(sampler2D tex, in vec3 n){
vec2 uv = -n.xz/n.yy;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getYN(sampler2D tex, in vec3 n){
vec2 uv = n.xz/n.yy;
uv.x = 0.5 + 0.5*uv.x;
uv.y = 0.5 - 0.5*uv.y;
return texture(tex, uv);
}
vec4 getZP(sampler2D tex, in vec3 n){
vec2 uv = n.xy/n.zz;
uv = 0.5 + 0.5*uv;
return texture(tex, uv);
}
vec4 getZN(sampler2D tex, in vec3 n){
vec2 uv = -n.xy/n.zz;
uv.x = 0.5 - 0.5*uv.x;
uv.y = 0.5 + 0.5*uv.y;
return texture(tex, uv);
}
vec4 getCubeTexture(sampler2D XP, sampler2D XN, sampler2D YP, sampler2D YN, sampler2D ZP, sampler2D ZN, in vec3 normal){
vec3 n = normalize(normal);
// これを実行するとこっちと同じ見た目になる:
// https://www.humus.name/index.php?page=Cubemap&item=Bridge2
// cubemapは基本的に外から見た場合の割り当てなので
// 中から見る分には鏡写しになる
// 文字を置くと分かる
// n.x=-n.x; // まあ背景に使う分には特に問題ないかと
if(n.x >= abs(n.y) && n.x >= abs(n.z)){
return getXP(XP, n);
}else if(n.x <= -abs(n.y) && n.x <= -abs(n.z)){
return getXN(XN, n);
}else if(n.y <= -abs(n.x) && n.y <= -abs(n.z)){
return getYP(YP, n);
}else if(n.y >= abs(n.x) && n.y >= abs(n.z)){
return getYN(YN, n);
}else if(n.z >= abs(n.x) && n.z >= abs(n.y)){
return getZP(ZP, n);
}else if(n.z <= -abs(n.x) && n.z <= -abs(n.y)){
return getZN(ZN, n);
}
return vec4(0.0);
}
非常に泥臭い、手間のかかる実装です。やってることは単純計算なので難しくないです。
中にも書きましたが、
n.x = -n.x;
これを実行すると鏡写しになり、例のサイトと同じ見た目になります。キューブマッピングは基本的に見えている面が外になるように立方体を組み立てた時の見た目をイメージしているので、内側から見る場合鏡写しになってしまいます(文字を置けばわかる)。ただまあ背景に使う分には全く問題ないとも言えます。
法線について
今回は球を描画して背景とすることが目的ですが、法線にはローカル法線を使っています。
vLocalNormal = aNormal;
/* ~~~~~ */
vec4 baseColor = inputs.color;
// なお、ルーチン内部でuniform変数を使うのは基本的にあまりいいマナーでは
// 無いと私は考えています。汎用性が下がるんで。
baseColor = getCubeTexture(uTextureXP, uTextureXN, uTextureYP, uTextureYN, uTextureZP, uTextureZN, vLocalNormal);
今回の目的のためにそうしているだけです。状況によっては違う場合もあるでしょう。
メイン描画
どでかい球を描画して背景とします。
function draw() {
orbitControl();
background(0);
shader(myShader);
noLights();
noStroke();
fill(255);
myShader.setUniform("uTextureXP", skyCubeMaps.xp);
myShader.setUniform("uTextureXN", skyCubeMaps.xn);
myShader.setUniform("uTextureYP", skyCubeMaps.yp);
myShader.setUniform("uTextureYN", skyCubeMaps.yn);
myShader.setUniform("uTextureZP", skyCubeMaps.zp);
myShader.setUniform("uTextureZN", skyCubeMaps.zn);
sphere(2000);
resetShader();
lights();
fill(255);
rotateX(frameCount*TAU/120);
rotateY(frameCount*TAU/180);
torus(100, 20, 24);
}
サイズ2000としていますがもっと大きくしたい場合もあるでしょう。ただカメラのfarによってはクリッピングが発生するので気を付けなければいけないです。通常のトーラスの描画のためにresetShader()をしています。そのために改変した後でshaderを戻しています。
おわりに
ここまでお読みいただいてありがとうございました。キューブマップ、どういう形で実装されるんでしょうね(そもそもそんな日は来るのか)。