はじめに
最近シェーダ書くことにハマっているので、今回はレイマーチングでいい感じにVJできそうな絵を作るために色々模索してみた試行錯誤を記事にしました。
前提
- 開発環境はkodelife
- フラグメントシェーダのみ利用
①:距離関数を準備
今回球と立方体と三角錐をそれぞれ準備したいのでそれぞれのSDFを作っておきます。
球
p: 三次元上の位置(原点は画面の中心)
r: 半径
を元に、描画する点の球の半径からの距離をfloatで返しています。
float sphereSDF(vec3 p, float r) {
return length(p) - r;
}
立方体
p: 三次元上の位置
c: 中心点
d: 各軸方向の半径
t: 立方体の厚み
で、球よりも複雑ですが、立方体を構成する部分と描画する点の距離を返すことで立方体が描かれています。
float boxSDF(vec3 p, vec3 c, vec3 d, float t) {
p = abs(p - c); // 点pをボックスの中心cからの相対位置に変換し、絶対値を取る
return length(max(p - d, vec3(0.0))) + min(max(max(p.x - d.x, p.y - d.y), p.z - d.z), 0.0) - t;
}
三角錐
詳しい説明は置いておきますが、下記サイトを参考に作成しました。
https://iquilezles.org/articles/distfunctions/
p: 三次元上の位置(三角錐の底面の中心は原点になり、y軸が高さ方向になる)
h: 三角錐の高さ
float sdPyramid(vec3 p, float h) {
float m2 = h*h + 0.25;
p.xz = abs(p.xz);
p.xz = (p.z>p.x) ? p.zx : p.xz;
p.xz -= 0.5;
vec3 q = vec3(p.z, h*p.y - 0.5*p.x, h*p.x + 0.5*p.y);
float s = max(-q.x,0.0);
float t = clamp((q.y-0.5*p.z)/(m2+0.25), 0.0, 1.0);
float a = m2*(q.x+s)*(q.x+s) + q.y*q.y;
float b = m2*(q.x+0.5*t)*(q.x+0.5*t) + (q.y-m2*t)*(q.y-m2*t);
float d2 = min(q.y,-q.x*m2-q.y*0.5) > 0.0 ? 0.0 : min(a,b);
return sqrt((d2+q.z*q.z)/m2) * sign(max(q.z,-p.y));
}
②:各図形を描画する(図形どうしの間の補間までやる)
下記のコードでできます。
詳しく説明するとめちゃくちゃ時間かかりそうなのでいったん説明しないです。
下記の本を読んでください。
https://gihyo.jp/book/2022/978-4-297-13034-3
#version 330
precision highp float;
precision highp int;
uniform float time;
uniform vec2 resolution;
uniform vec2 mouse;
uniform vec3 spectrum;
#define tempo 150
in VertexData
{
vec4 v_position;
vec3 v_normal;
vec2 v_texcoord;
} inData;
out vec4 fragColor;
//begin rot
vec2 rot2(vec2 p, float t) {
return vec2(cos(t) * p.x -sin(t) * p.y, sin(t) * p.x + cos(t) * p.y);
}
vec3 rotX(vec3 p, float t) {
return vec3(p.x, rot2(p.yz, t));
}
vec3 rotY(vec3 p, float t) {
return vec3(p.y, rot2(p.zx, t)).zxy;
}
vec3 rotZ(vec3 p, float t) {
return vec3(rot2(p.xy, t), p.z);
}
vec3 euler(vec3 p, vec3 t) {
return rotZ(rotY(rotX(p, t.x), t.y), t.z);
}
//end rot
float boxSDF(vec3 p, vec3 c, vec3 d, float t) {
p = abs(p - c);
return length(max(p - d, vec3(0.0))) + min(max(max(p.x - d.x, p.y - d.y), p.z - d.z), 0.0) - t;
}
float sphereSDF(vec3 p, float r) {
return length(p) - r;
}
float sdPyramid(vec3 p, float h) {
float m2 = h*h + 0.25;
p.xz = abs(p.xz);
p.xz = (p.z>p.x) ? p.zx : p.xz;
p.xz -= 0.5;
vec3 q = vec3(p.z, h*p.y - 0.5*p.x, h*p.x + 0.5*p.y);
float s = max(-q.x,0.0);
float t = clamp((q.y-0.5*p.z)/(m2+0.25), 0.0, 1.0);
float a = m2*(q.x+s)*(q.x+s) + q.y*q.y;
float b = m2*(q.x+0.5*t)*(q.x+0.5*t) + (q.y-m2*t)*(q.y-m2*t);
float d2 = min(q.y,-q.x*m2-q.y*0.5) > 0.0 ? 0.0 : min(a,b);
return sqrt((d2+q.z*q.z)/m2) * sign(max(q.z,-p.y));
}
float random(float n) {
return fract(sin(n) * 43758.5453123);
}
float sceneSDF(vec3 p) {
// BPMからビート時間を計算(60秒 / BPM)
float beatTime = 60.0 / float(tempo);
// 現在の時間をビート時間で割って正規化
float switchTime = mod(time, beatTime * 3.0) / beatTime;
float d1 = sphereSDF(p, .8);
float d2 = boxSDF(p, vec3(0.0), v, 0.0);
float d3 = sdPyramid(p, 1.0);
float t1 = smoothstep(0.0, 0.8, switchTime) - smoothstep(0.8, 1.6, switchTime);
float t2 = smoothstep(0.8, 1.6, switchTime) - smoothstep(1.6, 2.4, switchTime);
float t3 = smoothstep(1.6, 2.4, switchTime) - smoothstep(2.4, 3.0, switchTime);
return d1 * t1 + d2 * t2 + d3 * t3;
}
vec3 gradSDF(vec3 p) {
float eps = 0.001;
return normalize(vec3(
sceneSDF(p + vec3(eps, 0.0, 0.0)) - sceneSDF(p - vec3(eps, 0.0, 0.0)),
sceneSDF(p + vec3(0.0, eps, 0.0)) - sceneSDF(p - vec3(0.0, eps, 0.0)),
sceneSDF(p + vec3(0.0, 0.0, eps)) - sceneSDF(p - vec3(0.0, 0.0, eps))
));
}
vec3 renderScene(vec2 p) {
vec3 t = vec3(time * 1.0);
vec3 cPos = euler(vec3(0.0, 0.0, 2.0), t);
vec3 cDir = euler(vec3(0.0, 0.0, -1.0), t);
vec3 cUp = euler(vec3(0.0, 1.0, 0.0), t);
vec3 cSide = cross(cDir, cUp);
float targetDepth = 1.0;
vec3 lDir = euler(vec3(0.0, 0.0, 1.0), t);
vec3 ray = cSide * p.x + cUp * p.y + cDir * targetDepth;
vec3 rPos = ray + cPos;
ray = normalize(ray);
vec3 color = vec3(0.0);
for(int i = 0; i < 50; i++) {
if (sceneSDF(rPos) > 0.001) {
rPos += sceneSDF(rPos) * ray;
} else {
float amb = 0.1;
float diff = 0.9 * max(dot(normalize(lDir), gradSDF(rPos)), 0.0);
vec3 col = vec3(0.8, 0.5, 0.9);
color = col * (diff + amb);
break;
}
}
return color;
}
void main(void) {
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
fragColor = vec4(renderScene(p), 1.0);
}
現状の見た目はシンプルに球→立方体→三角錐が変化していくだけのものです。
③:RGBずらしを入れる
それっぽい見た目にしたいのでrgbをずらします。
renderScene
で作成した結果をr,g,bのそれぞれの要素に分割してそれらの位置をちょっとずつずらすことでrgbずらしを実装します。
void main(void) {
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
// RGB各チャンネルのオフセット
float offset = 0.01;
vec3 colR = renderScene(p + vec2(0.0, -offset)); // 赤チャンネル
vec3 colG = renderScene(p + vec2(0.866*offset, 0.5*offset)); // 緑チャンネル
vec3 colB = renderScene(p + vec2(-0.866*offset, 0.5*offset)); // 青チャンネル
// RGB各チャンネルを組み合わせる
fragColor = vec4(colR.r, colG.g, colB.b, 1.0);
}
④:グリッチエフェクトを入れる
spectrum(特定周波数)の大きさによってグリッチエフェクトが起きるように調整します。
うまく作ると楽曲に合わせてグリッチするようにできます。
float sceneSDF(vec3 p) {
// BPMからビート時間を計算(60秒 / BPM)
float beatTime = 60.0 / float(tempo);
// 現在の時間をビート時間で割って正規化
float switchTime = mod(time, beatTime * 3.0) / beatTime;
// グリッチエフェクトのパラメータ
float glitchIntensity = .05;
float glitchThreshold = .3;
// グリッチ効果の計算
vec3 glitchOffset = vec3(0.0);
float noise = random(floor(time * 30.0));
if (noise > 1.0 - glitchThreshold) {
glitchOffset = vec3(
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0,
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0,
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0
) * glitchIntensity;
}
// グリッチを適用した位置で距離関数を計算
p += glitchOffset;
float d1 = sphereSDF(p, .8);
float d2 = boxSDF(p, vec3(0.0), v, 0.0);
float d3 = sdPyramid(p, 1.0);
float t1 = smoothstep(0.0, 0.8, switchTime) - smoothstep(0.8, 1.6, switchTime);
float t2 = smoothstep(0.8, 1.6, switchTime) - smoothstep(1.6, 2.4, switchTime);
float t3 = smoothstep(1.6, 2.4, switchTime) - smoothstep(2.4, 3.0, switchTime);
return d1 * t1 + d2 * t2 + d3 * t3;
}
⑤:fractで増殖させる
図形の数を増やしたいと思います。
普通にfor文とかで増やそうとすると処理が重くなってしまうのでfractを利用して増やしましょう。
SDFの呼び出し部分を下記のように変更します。
float d1 = sphereSDF(vec3(fract(p+0.5)-0.5), .1);
float d2 = boxSDF(vec3(fract(p+0.5)-0.5), vec3(0.0), vec3(0.1), 0.0);
float d3 = sdPyramid(vec3(fract(p+0.5)-0.5), 1.0);
⑥:最終形のコード
こんな感じです
#version 330
precision highp float;
precision highp int;
uniform float time;
uniform vec2 resolution;
uniform vec2 mouse;
uniform vec3 spectrum;
#define tempo 150
in VertexData
{
vec4 v_position;
vec3 v_normal;
vec2 v_texcoord;
} inData;
out vec4 fragColor;
//begin rot
vec2 rot2(vec2 p, float t) {
return vec2(cos(t) * p.x -sin(t) * p.y, sin(t) * p.x + cos(t) * p.y);
}
vec3 rotX(vec3 p, float t) {
return vec3(p.x, rot2(p.yz, t));
}
vec3 rotY(vec3 p, float t) {
return vec3(p.y, rot2(p.zx, t)).zxy;
}
vec3 rotZ(vec3 p, float t) {
return vec3(rot2(p.xy, t), p.z);
}
vec3 euler(vec3 p, vec3 t) {
return rotZ(rotY(rotX(p, t.x), t.y), t.z);
}
//end rot
float boxSDF(vec3 p, vec3 c, vec3 d, float t) {
p = abs(p - c);
return length(max(p - d, vec3(0.0))) + min(max(max(p.x - d.x, p.y - d.y), p.z - d.z), 0.0) - t;
}
float sphereSDF(vec3 p, float r) {
return length(p) - r;
}
float sdPyramid(vec3 p, float h) {
float m2 = h*h + 0.25;
p.xz = abs(p.xz);
p.xz = (p.z>p.x) ? p.zx : p.xz;
p.xz -= 0.1;
vec3 q = vec3(p.z, h*p.y - 0.5*p.x, h*p.x + 0.5*p.y);
float s = max(-q.x,0.0);
float t = clamp((q.y-0.5*p.z)/(m2+0.25), 0.0, 1.0);
float a = m2*(q.x+s)*(q.x+s) + q.y*q.y;
float b = m2*(q.x+0.5*t)*(q.x+0.5*t) + (q.y-m2*t)*(q.y-m2*t);
float d2 = min(q.y,-q.x*m2-q.y*0.5) > 0.0 ? 0.0 : min(a,b);
return sqrt((d2+q.z*q.z)/m2) * sign(max(q.z,-p.y));
}
float random(float n) {
return fract(sin(n) * 43758.5453123);
}
float sceneSDF(vec3 p) {
// BPMからビート時間を計算(60秒 / BPM)
float beatTime = 60.0 / float(tempo);
// 現在の時間をビート時間で割って正規化
float switchTime = mod(time, beatTime * 3.0) / beatTime;
// グリッチエフェクトのパラメータ
float glitchIntensity = .05;
float glitchThreshold = .5;
// グリッチ効果の計算
vec3 glitchOffset = vec3(0.0);
float noise = random(floor(time * 30.0));
if (noise > 1.0 - glitchThreshold) {
glitchOffset = vec3(
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0,
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0,
random(p.x * (spectrum.x * 100.0)) * 2.0 - 1.0
) * glitchIntensity;
}
// グリッチを適用した位置で距離関数を計算
p += glitchOffset;
float d1 = sphereSDF(vec3(fract(p+0.5)-0.5), .1);
float d2 = boxSDF(vec3(fract(p+0.5)-0.5), vec3(0.0), vec3(0.1), 0.0);
float d3 = sdPyramid(vec3(fract(p+0.5)-0.5), 1.0);
float t1 = smoothstep(0.0, 0.8, switchTime) - smoothstep(0.8, 1.6, switchTime);
float t2 = smoothstep(0.8, 1.6, switchTime) - smoothstep(1.6, 2.4, switchTime);
float t3 = smoothstep(1.6, 2.4, switchTime) - smoothstep(2.4, 3.0, switchTime);
return d1 * t1 + d2 * t2 + d3 * t3;
}
vec3 gradSDF(vec3 p) {
float eps = 0.001;
return normalize(vec3(
sceneSDF(p + vec3(eps, 0.0, 0.0)) - sceneSDF(p - vec3(eps, 0.0, 0.0)),
sceneSDF(p + vec3(0.0, eps, 0.0)) - sceneSDF(p - vec3(0.0, eps, 0.0)),
sceneSDF(p + vec3(0.0, 0.0, eps)) - sceneSDF(p - vec3(0.0, 0.0, eps))
));
}
vec3 renderScene(vec2 p) {
vec3 t = vec3(time * 1.0);
vec3 cPos = euler(vec3(0.0, 0.0, 2.0), t);
vec3 cDir = euler(vec3(0.0, 0.0, -1.0), t);
vec3 cUp = euler(vec3(0.0, 1.0, 0.0), t);
vec3 cSide = cross(cDir, cUp);
float targetDepth = 1.0;
vec3 lDir = euler(vec3(0.0, 0.0, 1.0), t);
vec3 ray = cSide * p.x + cUp * p.y + cDir * targetDepth;
vec3 rPos = ray + cPos;
ray = normalize(ray);
vec3 color = vec3(0.0);
for(int i = 0; i < 50; i++) {
if (sceneSDF(rPos) > 0.001) {
rPos += sceneSDF(rPos) * ray;
} else {
float amb = 0.1;
float diff = 0.9 * max(dot(normalize(lDir), gradSDF(rPos)), 0.0);
vec3 col = vec3(0.8, 0.5, 0.9);
color = col * (diff + amb);
break;
}
}
return color;
}
void main(void) {
vec2 p = (gl_FragCoord.xy * 2.0 - resolution) / min(resolution.x, resolution.y);
// RGB各チャンネルのオフセット
float offset = 0.005;
vec3 colR = renderScene(p + vec2(0.0, -offset)); // 赤チャンネル
vec3 colG = renderScene(p + vec2(0.866*offset, 0.5*offset)); // 緑チャンネル
vec3 colB = renderScene(p + vec2(-0.866*offset, 0.5*offset)); // 青チャンネル
// RGB各チャンネルを組み合わせる
fragColor = vec4(colR.r, colG.g, colB.b, 1.0);
}