【夏休みの自由研究】「動き」と「色」の魔法(第3回)

第2回では画面にポツンと浮かぶ「完璧(かんぺき)な丸」を描くことに成功したね。 でも、今の丸はじっと止まったまま、眠っているみたい……。

今回使う魔法は、とってもシンプル。
算数(さんすう)で習った「たし算」「かけ算」
そして秘密のマシン「虹色パレット」を使いこなすだけ!

数式が「命」に変わる、シェーダーの本当の楽しさがついに始まります。
ワクワクする準備はできてるかな?

スポンサーリンク

🚀 たし算で「動き」を作る

算数の「たし算(+)」は、シェーダーの世界では
景色をグイッと動かす「スライド魔法」に変わるんだ!
🚀
「たし算」をするだけで、どうして丸が走りだすんだろう?
そのヒミツは、「今の場所に、時間をプラスする」という考え方にあるよ。
さっそく、魔法の仕組みをのぞいてみよう!

住所をズラせば、景色が動く!

第2回で、uv は「画面の住所(じゅうしょ)」だと習ったよね。
この住所に、監督(JS)からもらった「時間(u_time)」をたし算すると、不思議なことが起きるんだ!

イメージは「動く歩道」!

住所(uv)に時間をプラスするのは、止まっている地面を「ベルトコンベア」に変えるようなもの。
地面(住所)そのものが動いていくから、その上に描いた「丸」も一緒に流れていくんだよ!

✨ 魔法の1行 ✨
uv.x += u_time;
(今のヨコの住所に、時間をどんどん足していくよ!)
💡 なんで動いて見えるの?

フラグメントくんは、1秒間に60回も色をぬりなおしています。
「0秒のときの住所」「1秒たったときの住所」「2秒たったときの住所」……と、 たし算によって住所が少しずつズレていくから、私たちの目にはスルスル動いているように見えるんだね!

ヨコ(x)に足せばヨコに、タテ(y)に足せばタテに動くよ。
両方に足せば、ななめにスーッと動く魔法になるんだ。面白そうでしょ?

魔法をかける「順番(じゅんばん)」が大事!

シェーダーの魔法は、上から順番にかかっていくよ。
「走る魔法」をかける場所をまちがえると、丸が動かなかったり、変な方向にいっちゃうことも……。
一番きれいに動く「黄金(おうごん)のタイミング」はここだ!

1
舞台作り:3つの呪文で住所を整える
2
魔法発動:ここで住所をズラす!
3
お絵かき:length や step で形を描く
★ここに書く!
void main() {
  // (1) 3つの呪文で地図を準備…
  vec2 uv = vTexCoord * 2.0 – 1.0;
  vec2 uv = vTexCoord * 2.0 – 1.0;
  vec2 uv0 = uv;

  uv.x += u_time; // ← 住所を走らせる!

  // (3) その後に形を描く!
  float d = length(uv);
  float circle = step(0.3, d);
  …
}
💡 なぜここなの?
せっかくきれいに整えた「住所(地図)」を、色をぬる直前にグイッ!と動かすイメージだよ。描く直前に地図をズラすから、上に描かれる丸も一緒に動いて見えるんだね!

✨ かけ算で「光」をあやつる

かけ算(*)は、光の「ボリュームノブ」!
💡 ✖️ 魔法の数字
「たし算」が景色をスライドさせる魔法なら、
「かけ算」は光の強さを変える魔法だよ。

数字を大きくすればピカーッとまぶしくなり、小さくすればフワッと暗くなる。
この魔法を使えば、丸をチカチカ点滅させたり、宝石みたいに輝かせたりできるんだ。

さあ、どうやって光のボリュームをひねるのか、その仕組みを見てみよう!

光のボリュームをひねってみよう

ステップ1:名前をアップデート

circle strength

第2回の最後で計算した「丸のカタチ(0.0か1.0か)」の数字を、これからは「光の強さ(strength)」という箱に入れて進めていくよ。
シェーダーの世界では、「光の強さ」のことを一般的に strength(ストレングス) と呼ぶことが多いから2ヶ所、変更しておこうね。
中身は同じ「丸」のデータだよ!

ステップ2:割り算をつかう

今までは step というハサミを使って、パキッとした「形」を作ってきたね。
0.3より小さければ 0.0(黒)、大きければ 1.0(白) になるしくみだったよね

float strength = step(0.3, d);

でも、本物の光はもっとフワフワして、中心が一番まぶしいはず。ここで、ハサミ(step)を捨てて新しい魔法「わり算(/)」を使ってみよう!

float strength = 0.1 / d;
🔍 この式のすごいところ
  • step(ハサミ): 「0.0 か 1.0 か」の2種類。
  • わり算(光): 真ん中から外にむかって、なめらかな数字の階段ができる!

この「なめらかな階段」があるからこそ、あとで時間をかけ算したときに、ついたり消えたりするような動きができるようになるんだよ。

「0.1個」のケーキを、分ける計算なんだけど!
どうして1個ではなくて、 0.1個なの?

🧁 ケーキの数で「光の広がり」がこんなに変わる!
場所(キョリ) 1個を分けると?
1.0 / d
0.1個を分けると?
0.1 / d
0.1
(中心の近く)
10.0
超まぶしい!
1.0
ちょうどいい白
0.5
(まんなか辺)
2.0
まだ真っ白!
0.2
いい感じの暗さ
1.0
(画面のはし)
1.0
はしっこまで白!
0.1
ほぼ真っ暗
😱 「1.0 / d」だとどうなる?

シェーダーの画面は、数字が「1.0」を超えると全部真っ白に見えちゃうんだ。
ケーキが1個(1.0)だと、画面のはじっこまで全部の計算が「1.0」以上になっちゃうから、ただの白い四角になっちゃうんだね。

⚠️ 部屋のサイズにも注意だよ

例えば:mod(uv, 0.2) を使って、画面を 0.2 というすごく小さなサイズで区切ってる場合。

① 光のパワーをもっと小さくする strength = 0.02 / d;

光のパワーを弱めれば、
ハジっこに届く前に暗くなるから、きれいな丸になるよ!

② 部屋を広くする uv = mod(uv, 1.0) - 0.5;

タイルのサイズを大きくすれば、
光が消えるまでの「余裕」ができるんだ。

ステップ3:「かけ算」の魔法をかける

シェーダーでの光の強さは、0.0(真っ暗)〜 1.0(全開)という数字で決まるね。
ここに「かけ算」の魔法をかけると、光のパワーを自由自在にコントロールできるよ!

かけ算の魔法 ロボットへの命令 光のすがた
パワー2倍! strength * 2.0
限界突破してまぶしくなる!
パワー半分… strength * 0.5 🌙
フワッと優しい光になるよ
パワー・ゼロ! strength * 0.0 🌑
どんな光も一瞬で消えちゃう
💡 どこにかけ算すればいいの?

一番最後、色をぬる gl_FragColor の手前で計算してあげよう!
strength *= 2.0; と書くだけで、画面の中のすべての丸が一気にまぶしくなるよ。

でも、ずっと同じ明るさじゃつまらないよね?
次は、このかけ算に「時間(u_time)」を組み合わせて、ついたり消えたりさせるテクニックに挑戦だ!

ついたり消えたりする方法

魔法の波:sin(サイン)

sin は、「-1.0 から 1.0」の間をいったりきたりする波の形を作ってくれるんだ。

strength *= abs(sin(u_time));
① sin(波)
時間をいれると、時間がどれだけ進んでも、自動で「1.0 ➔ 0 ➔ -1.0 ➔ 0…」と波打つよ。
② abs(絶対値)
マイナスをプラスに変える魔法!これで、マイナスにいかない。「0.0 ➔ 1.0 ➔ 0.0」
地面をバウンドするようなイメージだね。
💡 なぜ「abs」が必要なの?

光の強さに「マイナスの数字」をかけると、画面が真っ暗なまま止まって見えちゃうんだ。
abs を使うことで、暗くなってもすぐに明るく戻ってくる、動きになるんだよ!

できたかな?
うまくいかない場合は、このコードをフラグメントくんのところにはりつけてね!

precision mediump float;
uniform float u_time;   
uniform vec2 u_resolution; 
varying vec2 vTexCoord; 


void main() {
 
  vec2 uv = vTexCoord * 2.0 - 1.0;
  uv.x *= u_resolution.x / u_resolution.y;
  vec2 uv0 = uv;

  //uv.y += u_time;

  uv = fract(uv * 3.0) - 0.5;

  float d = length(uv);

  float strength =0.1/ d;

  strength *= abs(sin(u_time));

  gl_FragColor = vec4(vec3(strength), 1.0);

}

白黒の世界におさらば!

今までずっと画面が白黒だったのには、ちゃんとした理由があったんだ。
最後に色をぬっている、この1行を見てみて。

gl_FragColor = vec4(vec3(strength), 1.0);

実は、シェーダーくんは色を 「赤・緑・青・透明度」 の4つのセットで考えています。

💡色は「3つセット」

色は R(赤)・G(緑)・B(青) の3つの光を混ぜて作ります。
vec3(strength) と書くと、ロボットは気を利かせて「赤も緑も青も、ぜーんぶ同じ強さ(strength)」にしてしまうんだ。
gl_FragColor = vec4( strength, strength, strength, 1.0 );
と同じなんだよ!

3つの光が同じ強さで混ざると、色は「白」か「灰色」にしかならない……。これが白黒の世界の正体だったんだね!

魔法の配合(はいごう)を変えてみよう

gl_FragColor = vec4(
  strength, // 赤
  0.0,      // 緑
  0.0,      // 青
  1.0       // 透明度
);
💡 ここがポイント!
「緑」と「青」を 0.0 にして、「赤」のところだけに strength(光のつぶ)を入れてあげると……画面の丸が一瞬で真っ赤な光に変わるんだ!

gl_FragColor = vec4(strength, 0.0, 0.0 ,1.0);

🌈 彩りの魔法:虹色パレットの召喚

これから使うのは、数字をいれるだけでキラキラした色を返してくれる
「パレット関数」という魔法の道具!

キミが作った「光のつぶ」を、虹色に変身させるよ。

【重要】2つの住所(uv と uv0)

虹色パレットでは「uv0」を使うよ!

① uv(タイルの中)

length(uv)

「丸のカタチ」を描く担当。
fractで区切られた狭いお部屋の中だけの住所だよ。だから、全部のタイルに同じ丸がコピーされるんだ。

② uv0(全体の景色)

palette(length(uv0))

「虹色のグラデーション」担当。
切り分ける前の広い世界の住所だよ。タイルを無視して、画面全体に1つだけの大きな虹をかけることができるんだ。

💡 なぜ「uv0」が必要だったの?

fract を使うと、地図が細かく切り刻まれて「全体で今どこにいるか」が分からなくなっちゃうんだ。

でも、切り刻まれる前の住所を uv0 にメモしておけば、たとえタイルの中にいても「自分は画面全体のどのへんに立っているか」を思い出せるんだよ!

「Palette(パレット)関数」で画面を虹色にしよう!

「赤・緑・青のバランスを考えるなんてムリ〜!」というキミに、とっておきの道具を教えるね。
それが、世界中のシェーダー職人が愛用している「Palette(パレット)関数」という自動着色マシンなんだ!

全自動マシン ✨ 数字をいれるだけで虹色が出てくる! ✨
🪙 (数字) ➡️ 🎰 ➡️

このマシンの中では、こんなカッコいい数式が動いているんだよ。

vec3 palette( float t ) {
  …(中略)…
  return a + b * cos( 6.28318 * (c * t + d) );
}
💡 使いかたは超かんたん!

palette( 好きな数字 ) と書いて「呼び出す」だけで、赤・緑・青(vec3)がバッチリ計算された「色」がポンッと飛び出してくるんだ!

ステップ1:「Palette(パレット)関数」(自動着色マシン)をセットしよう!

シェーダーくんは、とってもマジメな読書家。上の行から順番に一文字ずつ読んでいくよ。
だから、お絵かき(main)の中でマシンを使うなら、その前にマシンを組み立てておかないといけないんだ。

① 先に組み立てる ここが「Palette(パレット)関数」の設置場所!
vec3 palette(float t) { … }
⬇️ 順番に読み進める…
② ここで使う void main() {
← ここでpalette(d)を呼び出す
… }
💡 もし逆(ぎゃく)に先に呼び出しちゃうと?
お絵かきの最中に「パレットマシンを使って!」と命令されても、シェーダーくんは「そんなマシン、まだ見てないよ!」とパニック(エラー)になって、画面が真っ暗になっちゃうんだ。

🌟これが「Palette(パレット)関数」だ〜!!

vec3 palette(float t) {
vec3 a = vec3(0.5, 0.5, 0.5);
vec3 b = vec3(0.5, 0.5, 0.5);
vec3 c = vec3(1.0, 1.0, 1.0);
vec3 d = vec3(0.000, 0.333, 0.667);
return a + b * cos(6.28318 * (c * t + d));
}

さっそく、void mainの上に設置しよう!!

ステップ2:「Palette(パレット)関数」を使ってみよう!

さあ、「Palette(パレット)関数」に、第2回でメモしておいた「よびの地図(uv0)」を放り込んでみよう。

void mainの中で呼び出すよ!!

🎁 パレット関数からの「お返し」は vec3

数字をいれると、マシンは 「赤・緑・青」がセットになった「vec3」 を返してくれるんだ。

数字 (float) ➡️ 🎰 ➡️ vec3( R, G, B )
// 1. マシンから 3色セット(vec3) を受け取って color という箱に入れる
vec3 color = palette( length(uv0) );
// 2. まずは「色」だけを画面に映してみる!
gl_FragColor = vec4( color, 1.0 );
💡 画面はどうなった?

円形の虹色のグラデーションになってる?
これが、「色のベース」だよ。

🎨 虹色のカタチを決めるのは「キョリ」

length(uv0) は、「真ん中からのキョリ」をはかる呪文だったよね。

キョリが同じ場所(=円の形)には、同じ色がぬられる。だから、真ん中から外にむかって色がかわっていく「円形のグラデーション」になったんだよ!

💡 もし「たて」や「よこ」の虹にしたいなら?
  • palette( uv0.x )
  • palette( uv0.y )
に書きかえると、虹の向きが変わるよ。実験してみてね!

palette(length(uv0) + u_time) にして「時間(u_time)」をプラス(+)すると

🌈 なぜ、ずっと動かしても「色」がこわれないの?

マシンに「時間(u_time)」を伝えると、色がどんどん変わっていくよね。
「でも、時間が1万秒、1億秒って増え続けたら、マシンが爆発しちゃうんじゃない?」って心配になるかもしれないけど、大丈夫!

パレットマシンの中は、こんな「虹色の時計」になっているよ!

🕒 時計と同じだよ
夜中の12時をすぎたら、また「1時」にもどるよね?
このマシンも同じで、虹を1周したら、また最初にもどってぐるぐる回るだけなんだ!
♾️ ずっと続く魔法
中のプログラムには cos という「円をえがく魔法」が入っているよ。だから、どれだけ数字が増えても、虹の中から色を選びつづけてくれるんだ。

👨‍🔬マイナス(-)でも実験してみてね

ステップ3:これぞ究極の魔法:color * strength

さあ、準備はすべて整ったよ。手元には2つの魔法のデータがあるね。

  • color:画面全体に広がる、うつくしい「虹色の波紋(はもん)」。
  • strength:前につくった、ついたり消えたりする「白黒の光」。

この2つを「かけ算」したら、一体どうなると思う?

color
虹色の世界
×
strength
光のつぶ
完成!
輝く宝石!
🤔 なぜかけ算でうまくいくの?

シェーダーの算数では、「黒=0.0」 だったよね。
どんなに綺麗な虹色(color)でも、0.0 をかけたら真っ暗になって消えちゃう。

つまり、「光がある場所(strengthが1.0に近い場所)だけ、虹色が浮き上がって見える」 というわけ!
これで、真っ暗な中に、虹色に輝く宝石がフワフワ浮いているような、世界が完成するんだ!

precision mediump float;
uniform float u_time;
uniform vec2 u_resolution;
varying vec2 vTexCoord;

// 🏗️ 1. Palette(パレット)関数:mainの上に書くのがルール!
vec3 palette(float t) {
    vec3 a = vec3(0.5, 0.5, 0.5);
    vec3 b = vec3(0.5, 0.5, 0.5);
    vec3 c = vec3(1.0, 1.0, 1.0);
    vec3 d = vec3(0.000, 0.333, 0.667);
    return a + b * cos(6.28318 * (c * t + d));
}

void main() {
    vec2 uv = vTexCoord * 2.0 - 1.0;
    uv.x *= u_resolution.x / u_resolution.y;
    vec2 uv0 = uv;

    // タイルを作る
    uv = fract(uv * 3.0) - 0.5;

    // それそれのタイルのきょり
    float d = length(uv);
    // わり算で光らせる(👨‍🔬実験:もっと小さく0.02くらいにしてみよう!)
    float strength = 0.1 / d;  
    // ついたり消えたりする (👨‍🔬実験:消してみよう!)  
    strength *= abs(sin(u_time));  


    // 全体を虹色に  
    // (👨‍🔬実験:palette(uv0.x + u_time);にしてみよう!)
    vec3 color = palette(length(uv0) + u_time);

    // 運命のガッチャンコ(虹色のポスター × 懐中電灯)
    gl_FragColor = vec4(color * strength, 1.0);
}

🤔 「あれ? どっちが動いてるの?」
今のコードには、実は「暗くする魔法」が2つ入っています。だから、どっちが効いているのかわからなくなっちゃうんだ。

💡 光の点滅 strength *= abs(sin(u_time))

お部屋の「電気のスイッチ」をカチカチして、全体を明るくしたり暗くしたりする魔法だよ。

🌈 色の変化 palette( … + u_time)

着ている「服の色」をカラフルに変える魔法。でも、この虹には「黒」も混ざっているんだ。

💡 ここが重なりポイント!
「電気を消したとき(スイッチOFF)」と「服がたまたま黒くなったとき」は、どちらも画面が真っ暗に見えるよね。
今のコードは、この2つのリズムが重なっているから、違いがわかりにくくなっているんだよ!

ためしに strength の方を消してみると、「光はずっと出ているけど、色が順番に黒くなっているだけ」なのがわかるはずだよ!

🚀 第4回の予告:増殖の魔法:forループ

今のキミは、1つの美しい宝石を持っている。
じゃあ、それをどんどん増やして、
夜空いっぱいの星屑(ほしくず)みたいにしたくない?

💎
▶︎▶︎▶︎

スポンサーリンク