WordPressフルサイト編集機能でバレンタインを演出・シェーダー

前にp5.jsでパスを描画して2Dで実装、今回はシェーダー(GLSL)のみで実装してみます

WordPressブロックテーマでハートのエフェクトを演出

目次
  1. はじめに
  2. 実装の手順
  3. 基本的なWebGLのセットアップ
  4. フラグメントシャーダーの実装
    1. ハート型の描画と背景を透明にする
    2. 3D空間におけるレイの計算

はじめに

「ブロックテーマ」で利用可能な「フルサイト編集機能」は、ウェブサイトのヘッダーやフッター、サイドバーなど、従来のテーマでは制限されていた部分にも簡単に手を加えることができます
この機能により、管理画面のエディターを使用して、これらのエリアを直感的にカスタマイズできます

HTMLブロックは、ウェブページ上の任意の場所にコードを追加することができるので、今回は通常ほとんどのテンプレートで使用される「ヘッダーテンプレートパーツ」にコードを追加しました

注意:ただしHTMLブロックでは「&&(論理積演算子)」がエスケープされるため、利用できません

実装の手順

  1. WordPressのダッシュボードにログイン
  2. 左側のサイドバーから「外観」⇨「エディター」を選択
  3. 「ヘッダーテンプレートパーツ」の一番下に「HTMLブロック」を追加します

コード
*ユーザーがコンテンツに集中できるように、ページをスクロールするとエフェクトは消える実装にしました
float x = mix(-0.25, 3.0, fract(sin(i)*456.7));
ハートは右サイドに集中
float y = mix(-1.75, 3.0, fract(i+T*0.1));
発生時は見えるように

コードを見る

<canvas id="webgl-canvas-heart"></canvas> 
<div class="target-class"></div>
<style>
 body.hidden-class #webgl-canvas-heart {
   display: none;
  }
 #webgl-canvas-heart { 
   display:block; 
   position: fixed;
   top: 0;
   left: 0;
   width: 100%; 
   height: 100%; 
   z-index:1000;
   pointer-events: none;
   }  
 </style>
<script>
  const target = document.querySelector('.target-class');
  const callback = function(entries, observer) { 
    entries.forEach(entry => {
      if (!entry.isIntersecting) {
        document.body.classList.add('hidden-class');
      } else {
        document.body.classList.remove('hidden-class');
      }
    });
  };
  const observer = new IntersectionObserver(callback);
  observer.observe(target);

let canvas, gl, programInfo;  
main();  
function main() {
  canvas = document.querySelector("#webgl-canvas-heart");
  gl = canvas.getContext("webgl",{ alpha: true });
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  gl.viewport(0, 0, canvas.width, canvas.height);
  if (gl === null) return;
const vsSource = `
   attribute vec2 aPosition;
   void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
   }
  `;  
const fsSource = `
  precision mediump float;
  uniform vec2 uResolution;
  uniform float uTime;
  #define HEARTCOL vec3(1.0, 0.2, 0.2)
  #define T (uTime*2.0)  
  float Heart(vec2 uv){
    float scale =3.0;
    uv *= scale; 
    uv.x *= .7;
    uv.y -= sqrt(abs (uv.x))*-.5;
    float d = length(uv);
    return smoothstep(.2, .15, d);
  }
  vec3 Transform(vec3 p) {
    return -p;
  }  
 vec4 Heart3D(vec3 ro, vec3 rd, vec3 pos) {
    vec3 col = vec3(0.0);
    float alpha = 0.0;  
    float t = dot(pos - ro, rd);
    vec3 p = ro + rd * t;
    float y = length(pos - p); 
    if (y < 1.0) { 
        float x = sqrt(1. - y); 
        vec3 pF = ro + rd * (t - x) - pos;
        pF = Transform(pF);
        vec2 uvF = vec2(atan(pF.x, pF.z), pF.y);
        float c = Heart(uvF); 
        col = HEARTCOL * c;
        alpha = c;
    }   
    return vec4(col, alpha);
}
void main() {
    vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
    vec3 ro = vec3(0, 0, -3);
    vec3 rd = normalize(vec3(uv, 1));
    vec3 pos = vec3(0.0);
    vec4 totalColor = vec4(0.0);
    for(float i=0.0; i<1.0; i+=1.0/15.0){  
       float y = mix(-1.75, 3.0, fract(i+T*0.1));
       float x = mix(-0.25, 3.0, fract(sin(i)*456.7));
       float z = 1.0+i*2.0;
       vec4 colorAndAlpha = Heart3D(ro, rd, vec3(x, y, z));
       totalColor+=colorAndAlpha;
    }    
    gl_FragColor = totalColor;
}
  `;
  
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);  
const buffers = initBuffers(gl);
  
programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aPosition"),
  },
  uniformLocations: {
    time: gl.getUniformLocation(shaderProgram, "uTime"),
    resolution: gl.getUniformLocation(shaderProgram, "uResolution"),
  },
}; 
  window.addEventListener("resize", handleResize);  
  let then = 0;
  let deltaTime
  function render(now) {
    now *= 0.001; 
    deltaTime = now - then;
    then = now;  
    gl.uniform1f(programInfo.uniformLocations.time, now);
    gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);
    gl.clearColor(0.0, 0.0, 0.0, 0.0); 
    gl.clear(gl.COLOR_BUFFER_BIT);
    drawScene(gl, programInfo, buffers);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);    
}

function handleResize() {
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  gl.viewport(0, 0, canvas.width, canvas.height);
  if (programInfo.uniformLocations) {
    gl.useProgram(programInfo.program);
    gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);
  }
}
  
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) return null;
  return shaderProgram;
}
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}
function initBuffers(gl) {
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    const positions = [
        -1.0,  1.0, 
         1.0,  1.0, 
        -1.0, -1.0, 
         1.0, -1.0, 
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    return {
    position: positionBuffer,
  };
}
function drawScene(gl, programInfo, buffers) {
  setPositionAttribute(gl, buffers, programInfo);   
  gl.useProgram(programInfo.program);
  { 
    const offset = 0;
    const vertexCount = 4; 
    gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
  }
} 
function setPositionAttribute(gl, buffers, programInfo) {
  const numComponents = 2; 
  const type = gl.FLOAT;
  const normalize = false; 
  const stride = 0; 
  const offset = 0; 
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
  gl.vertexAttribPointer( 
    programInfo.attribLocations.vertexPosition,
    numComponents,
    type,
    normalize,
    stride,
    offset,
    );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}    
</script>

基本的なWebGLのセットアップ

とりあえず、WebGLを使用してブラウザにフルスクリーンの赤い四角形を描画
*フルスクリーンでの描画を行う場合、最低限必要なのは画面を覆う四角形の頂点データです
この四角形は通常、2つの三角形で構成され、それぞれの三角形は2つの頂点を共有します
この四角形を描画するために、頂点データをバッファに格納し、そのバッファをGPUに送ります

let canvas, gl, programInfo;  
main();  
function main() {
  canvas = document.querySelector("#webgl-canvas");
  gl = canvas.getContext("webgl",{ alpha: true });
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  gl.viewport(0, 0, canvas.width, canvas.height);
  if (gl === null) {
    alert(
      "WebGLに対応していない可能性があります。",
    );
    return;
  }
const vsSource = `
   attribute vec2 aPosition;
   void main() {
    gl_Position = vec4(aPosition, 0.0, 1.0);
   }
  `;
  
const fsSource = `
  precision lowp float;
  uniform vec2 uResolution;
  uniform float uTime;
  void main() {
    vec2 uv = (gl_FragCoord.xy -.5*uResolution.xy)/uResolution.y;
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); 
  }
  `;
  
const shaderProgram = initShaderProgram(gl, vsSource, fsSource);  
const buffers = initBuffers(gl);
  
programInfo = {
  program: shaderProgram,
  attribLocations: {
    vertexPosition: gl.getAttribLocation(shaderProgram, "aPosition"),
  },
  uniformLocations: {
    time: gl.getUniformLocation(shaderProgram, "uTime"),
    resolution: gl.getUniformLocation(shaderProgram, "uResolution"),
  },
}; 
 // プログラムの初期化後にリサイズハンドラをセットアップ
  window.addEventListener("resize", handleResize);  
  
  let then = 0;
  let deltaTime
  function render(now) {
    now *= 0.001; 
    deltaTime = now - then;
    then = now;  
    // ユニフォームの更新
    gl.uniform1f(programInfo.uniformLocations.time, now);
    gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);

    gl.clearColor(0.0, 0.0, 0.0, 0.0); 
    gl.clear(gl.COLOR_BUFFER_BIT);
    drawScene(gl, programInfo, buffers);
    requestAnimationFrame(render);
  }
  requestAnimationFrame(render);    
}

function handleResize() {
  canvas.width = window.innerWidth * window.devicePixelRatio;
  canvas.height = window.innerHeight * window.devicePixelRatio;
  gl.viewport(0, 0, canvas.width, canvas.height);
  if (programInfo && programInfo.uniformLocations) {
    gl.useProgram(programInfo.program);
    gl.uniform2f(programInfo.uniformLocations.resolution, canvas.width, canvas.height);
  }
  // 描画関数をここで呼び出さない場合は、現在の描画ループに任せます
  // drawScene(gl, programInfo, buffers);
}
  
function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
    alert(
      `シェーダープログラムを初期化できません: ${gl.getProgramInfoLog(
        shaderProgram,
      )}`,
    );
    return null;
  }
  return shaderProgram;
}
function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);

  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    alert(
      `シェーダーのコンパイル時にエラーが発生しました: ${gl.getShaderInfoLog(
        shader,
      )}`,
    );
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}
function initBuffers(gl) {
    const positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    // 画面を覆う四角形を形成するための頂点
    // 2つの三角形で全画面を覆います。
    const positions = [
        -1.0,  1.0,  // 左上
         1.0,  1.0,  // 右上
        -1.0, -1.0,  // 左下
         1.0, -1.0,  // 右下
    ];
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
    return {
    position: positionBuffer,
  };
}
function drawScene(gl, programInfo, buffers) {
  setPositionAttribute(gl, buffers, programInfo);   
  gl.useProgram(programInfo.program);
  { 
    const offset = 0;
    const vertexCount = 4; 
    gl.drawArrays(gl.TRIANGLE_STRIP, offset, vertexCount);
  }
} 
function setPositionAttribute(gl, buffers, programInfo) {
  const numComponents = 2; 
  const type = gl.FLOAT;
  const normalize = false; 
  const stride = 0; 
  const offset = 0; 
  gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
  gl.vertexAttribPointer( 
    programInfo.attribLocations.vertexPosition,
    numComponents,
    type,
    normalize,
    stride,
    offset,
    );
  gl.enableVertexAttribArray(programInfo.attribLocations.vertexPosition);
}   

備考
WebGLでは、フラグメントシェーダーにおける浮動小数点数(float)の変数に対して、精度の指定(precision)が必要です。
*WebGL 2.0では、フラグメントシェーダーの精度指定はオプションとなっていますが、互換性を保つためにも、特にモバイルデバイスでの動作を考慮して、引き続き精度指定を行うことが望ましい

  • lowp:低精度。最低8ビットの浮動小数点数を保証
  • mediump:中精度。最低10ビットの浮動小数点数を保証
  • highp:高精度。最低16ビットの浮動小数点数を保証

gl_FragCoordについて

  • gl_FragCoordはWebGLが提供する組み込み変数で、現在レンダリングされているフラグメント(ピクセル)の画面上の座標(x, y, z, 1/w)を表します。
  • フルスクリーンのシェーダーエフェクトを実装する場合は、gl_FragCoordを使用すると、UV座標のバッファを用意する必要はありません
  • gl_FragCoordは、フラグメントがレンダリングされる画面上の位置(x, y座標)を持っているので、これを利用して、フラグメントシェーダー内でピクセルごとに異なる色やエフェクトを生成することができる
  • *フルスクリーンではなく、特定の形状(例えば正方形)の描画領域を作成する場合は、頂点座標とUV座標のバッファを作成する必要があります

フラグメントシャーダーの実装

ハート型の描画と背景を透明にする

vec2 uvに、画面の中心を原点とし、縦方向の範囲が[-1, 1]で正規化された座標を格納

/*
gl_FragCoord.xy: これは現在処理中のフラグメント(ピクセル)の画面上の座標xとy

-.5*uResolution.xy: ここでは解像度(uResolution)の半分の値をそれぞれの座標から引く
これにより、座標系が画面の中心を基点とするように変換される

/uResolution.y: この部分では、座標を画面の縦方向の解像度で割る
これにより、座標は[-1, 1]の範囲に正規化され、アスペクト比が保持される
*/
vec2 uv = (gl_FragCoord.xy -.5*uResolution.xy)/uResolution.y;

円を描画する
*smoothstep はエッジを滑らかにするための関数で、値 t が区間 [a, b] 内にある場合、滑らかな曲線を使って0から1までの値を返します

/* 
dはuv座標(画面中心を原点とする)からの距離を計算しています。これは画面中心から現在のピクセルまでの距離
*/
float d = length(uv);
/*
smoothstep関数を使用して、距離dが特定の範囲(この場合は0.25から0.3)にある場合に滑らかなエッジを持つ値を生成
これにより、特定の半径の範囲に円形の境界が作られる
この場合0.3-0.25の値がblur(ぼかし)になる
*/
float c = smoothstep(.3, .25, d);
col = vec3(c);

キャンバスの背景は透明にして、キャンバスの下にあるHTML要素が見えるようする
WebGLコンテキストを初期化する際の { alpha: true } オプションと
描画ループ内での gl.clearColor(0.0, 0.0, 0.0, 0.0); の設定に加えて
フラグメントシェーダーでフラグメントシェーダーで特定の条件下でピクセルを透明にする必要がある

 float alpha = 1.0;
 if (col == vec3(0.0)) {
   alpha = 0.0; 
 }
 gl_FragColor = vec4(col, alpha);
/*
ここではsmoothstep関数は、指定された範囲の境界近くで滑らかな遷移を生成するため
この関数の出力を直接alphaに使用する
*/
float c = smoothstep(.3, .25, d);
float alpha = c;
col = vec3(1.0, 0.0, 0.0);
gl_FragColor = vec4(col, alpha);

ハート型にする

uv.y -= abs(uv.x);
float d = length(uv);

//一行だと
float d = length(vec2(uv.x, uv.y - abs(uv.x)));

x座標の絶対値を使用することで、x座標が正の場合も負の場合も、y座標が減少、xが中心に近いほどy座標はより少なく、xが中心から遠いほど、y座標の変化はより大きくなるので、UV座標系は「V」字型に変形
変形したUV座標系に基づいた距離が求められます
*注意:y座標の「減少」という表現は、実際には画面の上方向への移動を意味します

/*
横方向がやや広い楕円形にする
*/
 uv.x *= .7;
/*
sqrt関数で、平方根(ルート)を計算
xの値が大きくなるにつれて、その平方根が比例して増加しないようにする(緩やかにする)
*例:4の平方根は2、9の平方根は3
*/
 uv.y -= sqrt(abs (uv.x))*.5;

3D空間におけるレイの計算

最初の例では、float d = length(uv); で2D空間上のUV座標系における原点(通常は画面の中心)からの距離を計算していました
次は、
ro(Ray Origin: レイの起点)とrd(Ray Direction: レイの方向)を定義することで、3D空間における視点(カメラ)から特定の点(pos)までの距離を計算して、3Dのシーンにおいて物体と視点との相対的な位置関係や、物体までの距離の計算をする

vec3 ro = vec3(0,0,-3); //カメラ(視点)がz軸の負の方向に3単位離れた位置
vec3 rd = normalize(vec3(uv, 1));//レイの方向(2DUV座標を3D空間に拡張し、正規化する)
vec3 pos = vec3(0.0);
/*
レイが3D空間を進むことをシミュレートして
float y = length(pos - p); でレイの起点から物体(pos)までの直線距離を計算
*/
float t = dot(pos-ro, rd); //tはレイがrdの方向に進んだ距離を表す
vec3 p = ro+rd*t;
float y = length(pos-p);  //レイの現在位置pと原点 pos(ここでは (0,0,0))との間の距離yを計算
  

レイ(視線)が原点周辺に配置された半径1の球体と交差するかどうかをチェックする

/*
yが1以下であればレイは半径1の球体に接触または交差していると考えられる
*/
if(y<1.) { 
  float x= sqrt(1.- y); //レイが球体内部に位置している場合の、レイの現在位置から球体の表面までの距離
  vec3 pF = ro + rd *(t-x)-pos; //レイが球体を通過する際に最初に通過する点
  vec2 uvF= vec2(atan(pF.x, pF.z), pF.y); //pFの球体上でのUV座標(x座標とz座標を使用して経度を求め、y座標で緯度を表現する)
  vec3 pB = ro + rd * (t+x) - pos; //レイが球体の内部を通過して、反対側の表面に到達する点(背面通過点)
  vec2 uvB= vec2(atan (pB.x, pB.z), pB.y); //pBの球体上でのUV座標
col.rg += uvF; 
 }

3D空間において球体との交差を計算することで、特定の位置にあるハート形状オブジェクトの色と透明度を計算します
球体の表面にハートの形を描画することを試みています。ここで、Heart 関数は2D空間上でハートの形を生成し、球体の表面上の特定の点にこのハート形を適用する
*球体の表面にハートが描画される場合のみピクセルが不透明になり、ハートが描画されない部分は透明に

// ハート形状を生成する関数で、距離dが特定の範囲にある場合だけ.3,〜 .27の範囲の値がかえる
float Heart(vec2 uv, float scale){
    uv *= scale; 
    uv.x *= .7;
    uv.y -= sqrt(abs (uv.x)) * .5;
    float d = length(uv);
    return smoothstep(.3, .27, d);
}
#define HEARTCOL vec3(1.0, 0.1, 0.5)
void main() {
    vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
    vec3 col = vec3(0.0);
    float alpha =1.0; //とりあえずは球体の外側を表示するために1.0にしている
    
    vec3 ro = vec3(0,0,-3);
    vec3 rd = normalize(vec3(uv, 1));
    vec3 pos = vec3(0.0);
    float t = dot(pos-ro, rd);
    vec3 p = ro+rd*t;
    float y = length(pos-p); 
  
  if(y < 1.0) { 
    float x= sqrt(1.-y); 
    vec3 pF = ro + rd *(t-x)-pos;
    vec2 uvF= vec2(atan (pF.x, pF.z), pF.y);
    vec3 pB = ro + rd * (t+x) - pos; 
    vec2 uvB= vec2(atan (pB.x, pB.z), pB.y);
    float c = Heart(uvB, 1.0); //ハートが描画される場合のみアルファ値を設定
    col = HEARTCOL;
    alpha = c;
   }   
    gl_FragColor = vec4(col, alpha);
}

球体の表面に時間に依存する動的なビジュアルエフェクトを描画
*3D空間における点を動的に変換して、その変換された点に基づくUV座標を使う

//省略 
float Heart(vec2 uv, float scale){
  //省略
}
/*
2D平面上の点を特定の角度で回転させるためための、2Dの回転行列を生成する関数
*/ 
  mat2 Rot(float a) {
      float s = sin(a);
      float c = cos(a);
      return mat2(c, -s, s, c);
   }
/*
3D空間内の点pを回転させる関数
p.xzとp.xyにRot関数を適用して、それぞれの平面において点を回転
ここでは異なる回転角度を使用(angleとangle*0.7)
*/
  vec3 Transform(vec3 p, float angle) {
    p.xz *= Rot(angle);
    p.xy *= Rot(angle*.7);  
    return p;
  }

void main() {
    vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
    vec3 col = vec3(0.0);
    float alpha = 0.0;
    float angle = uTime*0.5;
    
    vec3 ro = vec3(0,0,-3);
    vec3 rd = normalize(vec3(uv, 1));
    vec3 pos = vec3(0.0);
    float t = dot(pos-ro, rd);
    vec3 p = ro+rd*t;
    float y = length(pos-p);
  
  
  if(y<1.5) { 
    float x= sqrt(1.-y); 
    vec3 pF = ro + rd *(t-x)-pos;
    pF = Transform(pF, angle);  //前面と交差する点にTransform関数を適用して時間に依存する角度で回転させる
    vec2 uvF= vec2(atan (pF.x, pF.z), pF.y);
    vec3 pB = ro + rd * (t+x) - pos; 
    pB = Transform(pB, angle); //背面と交差する点にTransform関数を適用して時間に依存する角度で回転させる
    vec2 uvB= vec2(atan (pB.x, pB.z), pB.y);
/*
球体の前面と背面に描画されるハート形を合成
これにより、球体の両面にハート形が描画される
*/
    float c = Heart(uvB, 1.0)+Heart(uvF, 1.0); 
    col = HEARTCOL;
    alpha = c;
   }   
    gl_FragColor = vec4(col, alpha);
}

関数にして切り出す
*vec4で受け取るようにする

 vec4 Heart3d(vec3 ro, vec3 rd, vec3 pos, float angle, float scale) {
    vec3 col = vec3(0.0);
    float alpha = 0.0;  
    float t = dot(pos - ro, rd);
    vec3 p = ro + rd * t;
    float y = length(pos - p);
  
    if (y < 1.0) { 
      //省略
    }   
    return vec4(col, alpha);  
}

void main() {
    vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
    vec3 ro = vec3(0, 0, -3);
    vec3 rd = normalize(vec3(uv, 1));
    vec3 pos = vec3(0.0);
   //vec4で受け取るようにする
    vec4 colorAndAlpha = Heart3d(ro, rd, pos, uTime * 0.5, 1.0);
    
    gl_FragColor = colorAndAlpha;
}

フラグメントシェーダー内で特定の視点からのオブジェクトの見え方やライティング効果を計算するのは複雑なので💦 フラグメントシェーダーを使用して3D空間上でオブジェクトを配置し、動かす基本的な方法のみ実装
ループを通じて、3D空間上で位置にハートのオブジェクトを配置(座標は時間で周期的に変化)、それらの色を計算
*オブジェクトが重なる場合の深度の処理やライティング効果は今回は考慮されていません

 precision lowp float;
  uniform vec2 uResolution;
  uniform float uTime;
  #define HEARTCOL vec3(1.0, 0.0, 0.2)
  #define T (uTime*2.0)
  
  float Heart(vec2 uv){
    float scale =3.0;
  	uv *= scale; 
    uv.x *= .7;
    uv.y -= sqrt(abs (uv.x))*-.5;
    float d = length(uv);
    return smoothstep(.2, .15, d);
  }
  vec3 Transform(vec3 p) {
    return -p;
  }
 /*
ハート形状の各点におけるUVマッピング座標に基づいてハート形状の塗りつぶしを計算
最初にレイが接触する表面の点を計算
*/ 
 vec4 Heart3D(vec3 ro, vec3 rd, vec3 pos) {
    vec3 col = vec3(0.0);
    float alpha = 0.0;  
    float t = dot(pos - ro, rd);
    vec3 p = ro + rd * t;
    float y = length(pos - p); 
    if (y < 1.0) { 
        float x = sqrt(1. - y); 
        vec3 pF = ro + rd * (t - x) - pos;
        pF = Transform(pF);
        vec2 uvF = vec2(atan(pF.x, pF.z), pF.y);
        float c = Heart(uvF); 
        col = HEARTCOL * c;
        alpha = c;
    }   
    return vec4(col, alpha);
}

void main() {
    vec2 uv = (gl_FragCoord.xy - .5 * uResolution.xy) / uResolution.y;
  
    vec3 ro = vec3(0, 0, -3);
    vec3 rd = normalize(vec3(uv, 1));
    vec3 pos = vec3(0.0);
    vec4 totalColor = vec4(0.0);
/*
異なる時間と位置にハート形状のオブジェクトを配置し、それらの色を計算
*/
    for(float i=0.0; i<1.0; i+=1.0/15.0){  
       float y = mix(-1.75, 3.0, fract(i+T*0.1));
       float x = mix(0.0, 1.75, fract(sin(i)*4570.3));
       float z = 1.0+i*2.0;
       vec4 colorAndAlpha = Heart3D(ro, rd, vec3(x, y, z));
       totalColor+=colorAndAlpha;
    }
    
    gl_FragColor = totalColor;
}
  • mix関数は2つの値の間を線形に補間します
    mix(a, b, t)は、aからbまでの間をtに基づいて補間した値を返します
  • fract関数は、与えられた数値の小数部分を返します
    数値の「周期的な」性質を作り出すのに役立ち、アニメーションで滑らかな繰り返し効果を生成