フレネル発光の球

フレネル項で縁だけが光るシェーダーマテリアルの球が回転。内側は暗く縁が明るいリムライト風で、暗い背景に映えます。テック系の象徴ビジュアルに向きます。

外部ライブラリ: https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

#webgl#threejs#shader#fresnel

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:縁が光る球をペンライトの光に見立てたライブ告知ヒーロー -->
<section class="sk-live" aria-label="Sakura ライブ">
  <!-- 背景:フレネルで縁が光る球(ライブの光の象徴) -->
  <canvas id="scene" class="sk-live__canvas" aria-hidden="true"></canvas>
  <div class="sk-live__fallback" id="sk-fallback" hidden></div>

  <header class="sk-bar">
    <span class="sk-logo">🌸 Sakura</span>
    <span class="sk-bar__tag">LIVE TOUR 2026</span>
  </header>

  <div class="sk-live__body">
    <span class="sk-kicker">SPRING ONEMAN LIVE</span>
    <h1 class="sk-title">桜、満開ツアー</h1>
    <p class="sk-lead">全国5都市をめぐる春のワンマンツアー。<br>チケット最速先行、本日より受付開始。</p>

    <ul class="sk-dates">
      <li><span class="sk-dates__city">東京</span><span class="sk-dates__day">4.18 SAT</span></li>
      <li><span class="sk-dates__city">大阪</span><span class="sk-dates__day">4.25 SAT</span></li>
      <li><span class="sk-dates__city">名古屋</span><span class="sk-dates__day">5.02 SAT</span></li>
    </ul>
    <button class="sk-btn" type="button">先行に申し込む</button>
  </div>
</section>
CSS
/* Sakura:暗めの背景に光る球が映えるライブ告知 */
:root {
  --pink: #ffd1e0;
  --pink-deep: #ff8fb3;
  --white: #ffffff;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  height: 400px;
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", system-ui, sans-serif;
  background: #2a1320;
}

.sk-live {
  position: relative;
  width: 100%;
  height: 400px;
  overflow: hidden;
  /* ライブ会場の暗がりに桜色の光 */
  background:
    radial-gradient(90% 90% at 78% 45%, #5a2540 0%, #361426 55%, #220c18 100%);
  color: #fff;
}

/* 光る球は右寄りの背景に */
.sk-live__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.sk-live__fallback {
  position: absolute;
  right: 16%;
  top: 50%;
  width: 180px;
  height: 180px;
  transform: translateY(-50%);
  border-radius: 50%;
  background: radial-gradient(circle, transparent 45%, rgba(255, 143, 179, 0.85) 70%, transparent 80%);
  filter: blur(2px);
}

.sk-bar {
  position: relative;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 26px;
}
.sk-logo { font-size: 18px; font-weight: 800; letter-spacing: 0.04em; }
.sk-bar__tag {
  font-size: 11px;
  letter-spacing: 0.22em;
  font-weight: 700;
  color: var(--pink);
}

.sk-live__body {
  position: relative;
  z-index: 2;
  max-width: 420px;
  padding: 22px 26px;
}
.sk-kicker {
  font-size: 11px;
  letter-spacing: 0.26em;
  font-weight: 700;
  color: var(--pink-deep);
}
.sk-title {
  margin: 12px 0 12px;
  font-size: 34px;
  font-weight: 800;
  letter-spacing: 0.02em;
  text-shadow: 0 2px 18px rgba(255, 143, 179, 0.4);
}
.sk-lead {
  margin: 0 0 16px;
  font-size: 13.5px;
  line-height: 1.85;
  color: rgba(255, 255, 255, 0.82);
}

.sk-dates {
  margin: 0 0 20px;
  padding: 0;
  list-style: none;
  display: flex;
  gap: 8px;
}
.sk-dates li {
  flex: 1;
  text-align: center;
  padding: 9px 4px;
  border-radius: 12px;
  background: rgba(255, 209, 224, 0.1);
  border: 1px solid rgba(255, 143, 179, 0.35);
}
.sk-dates__city {
  display: block;
  font-size: 13px;
  font-weight: 700;
  color: var(--pink);
}
.sk-dates__day {
  display: block;
  font-size: 10px;
  margin-top: 3px;
  color: rgba(255, 255, 255, 0.7);
}

.sk-btn {
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, var(--pink-deep), #ff6f9c);
  border: none;
  padding: 12px 28px;
  border-radius: 999px;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(255, 111, 156, 0.5);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.sk-btn:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(255, 111, 156, 0.65); }
.sk-btn:active { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .sk-btn { transition: none; }
}
JavaScript
// Sakura ライブ告知ヒーロー:フレネル項で縁だけが桜色に光る球(ライブの光の象徴)
(function () {
  "use strict";
  const canvas = document.getElementById("scene");
  const fallback = document.getElementById("sk-fallback");
  // THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
  if (!canvas || typeof THREE === "undefined") {
    if (fallback) fallback.hidden = false;
    return;
  }

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  } catch (e) {
    if (fallback) fallback.hidden = false;
    return;
  }
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
  camera.position.set(0, 0, 4);

  // フレネル発光のシェーダーマテリアル
  const uniforms = {
    glowColor: { value: new THREE.Color(0xff8fb3) }, // 桜色のリム
    coreColor: { value: new THREE.Color(0x3a1626) }, // 暗い内側
    power: { value: 2.6 },
  };

  const vertexShader = `
    varying vec3 vNormal;
    varying vec3 vView;
    void main() {
      vNormal = normalize(normalMatrix * normal);
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      vView = normalize(-mv.xyz);
      gl_Position = projectionMatrix * mv;
    }
  `;

  const fragmentShader = `
    precision highp float;
    varying vec3 vNormal;
    varying vec3 vView;
    uniform vec3 glowColor;
    uniform vec3 coreColor;
    uniform float power;
    void main() {
      // 視線と法線の角度差から縁を明るく(フレネル)
      float f = pow(1.0 - abs(dot(vNormal, vView)), power);
      vec3 col = mix(coreColor, glowColor, f);
      gl_FragColor = vec4(col, 1.0);
    }
  `;

  const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
  const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
  sphere.position.x = 1.4; // 前景テキストを避けて右へ
  scene.add(sphere);

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  let running = true;
  let t = 0;
  function animate() {
    if (!reduceMotion) {
      t += 0.02;
      sphere.rotation.y += 0.005;
      // 光の脈動でライブの高揚感を演出
      uniforms.power.value = 2.6 + Math.sin(t) * 0.5;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate();

  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      if (running) { cancelAnimationFrame(raf); running = false; }
    } else if (!running) {
      running = true;
      raf = requestAnimationFrame(animate);
    }
  });

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

コード

HTML
<!-- フレネル項で縁が光る回転球。リムライト風 -->
<div class="stage">
  <canvas id="fresnel" aria-label="フレネル発光の球"></canvas>
  <div class="fallback" id="fr-fallback" hidden>WebGL を表示できない環境です</div>
  <div class="caption">
    <span class="badge">Fresnel</span>
    <h2>Glowing Sphere</h2>
    <p>縁だけが光るリムライト風シェーダーの球</p>
  </div>
</div>
CSS
/* 配色変数 */
:root {
  --ink: #eaf2ff;
  --accent: #6fe9ff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 暗い背景で縁の発光を際立たせる */
  background: radial-gradient(circle at 50% 42%, #0a1326 0%, #050a16 60%, #02040a 100%);
}

#fresnel {
  display: block;
  width: 100%;
  height: 100%;
}

/* WebGL非対応時のフォールバック表示 */
.fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  font-size: 14px;
  letter-spacing: .06em;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
}
/* hidden 属性を尊重(CSSの display 指定が [hidden] を上書きしないように) */
.fallback[hidden] { display: none; }

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(111, 233, 255, .15);
  border: 1px solid rgba(111, 233, 255, .45);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .75;
}
JavaScript
// フレネル発光の球:縁ほど強く光るリムライト風シェーダー
(function () {
  "use strict";
  const canvas = document.getElementById("fresnel");
  const fallback = document.getElementById("fr-fallback");
  // THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
  if (!canvas || typeof THREE === "undefined") {
    if (fallback) fallback.hidden = false;
    return;
  }

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // WebGL初期化(失敗時はフォールバック)
  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  } catch (e) {
    if (fallback) fallback.hidden = false;
    return;
  }
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
  camera.position.z = 4;

  // フレネル項(視線と法線の角度)で縁の発光量を決めるシェーダー
  const uniforms = {
    time: { value: 0 },
    glowColor: { value: new THREE.Color(0x6fe9ff) }, // 縁の発光色
    coreColor: { value: new THREE.Color(0x081326) }, // 内側の暗色
    power: { value: 2.6 }, // フレネルの鋭さ
  };

  const vertexShader = `
    varying vec3 vNormal;
    varying vec3 vView;
    void main() {
      // ビュー空間の法線と視線ベクトルを渡す
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      vNormal = normalize(normalMatrix * normal);
      vView = normalize(-mv.xyz);
      gl_Position = projectionMatrix * mv;
    }
  `;

  const fragmentShader = `
    precision highp float;
    varying vec3 vNormal;
    varying vec3 vView;
    uniform float time;
    uniform vec3 glowColor;
    uniform vec3 coreColor;
    uniform float power;

    void main() {
      // フレネル:正面ほど0、縁ほど1
      float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
      f = pow(f, power);
      // ゆるく脈動させる
      f *= 0.85 + 0.15 * sin(time * 1.5);
      // 内側暗・縁明をブレンド
      vec3 col = mix(coreColor, glowColor, clamp(f, 0.0, 1.0));
      // 縁を加算でさらに発光
      col += glowColor * f * 0.6;
      gl_FragColor = vec4(col, 1.0);
    }
  `;

  const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
  const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
  scene.add(sphere);

  // 縁の光をさらに広げる外殻(裏面描画の加算ハロー)
  const haloMat = new THREE.ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader: `
      precision highp float;
      varying vec3 vNormal;
      varying vec3 vView;
      uniform vec3 glowColor;
      uniform float power;
      void main() {
        float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
        f = pow(f, power * 0.6);
        gl_FragColor = vec4(glowColor, f * 0.5);
      }
    `,
    transparent: true,
    blending: THREE.AdditiveBlending,
    side: THREE.BackSide,
    depthWrite: false,
  });
  const halo = new THREE.Mesh(new THREE.SphereGeometry(1.45, 48, 48), haloMat);
  scene.add(halo);

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  let running = true;
  const start = performance.now();
  function animate(now) {
    const t = (now - start) * 0.001;
    uniforms.time.value = t;
    // 縁の発光色を時間でゆっくり巡回させ、変化をはっきり見せる(無地の球でも動きが分かる)
    uniforms.glowColor.value.setHSL((t * 0.08) % 1, 0.85, 0.62);
    if (!reduceMotion) {
      sphere.rotation.y = t * 0.4;
      sphere.rotation.x = t * 0.15;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate(start);

  // タブ非表示で停止、復帰で再開
  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      if (running) { cancelAnimationFrame(raf); running = false; }
    } else if (!running) {
      running = true;
      raf = requestAnimationFrame(animate);
    }
  });

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「フレネル発光の球」の効果を追加してください。

# 追加してほしい効果
フレネル発光の球(WebGL / Three.js)
フレネル項で縁だけが光るシェーダーマテリアルの球が回転。内側は暗く縁が明るいリムライト風で、暗い背景に映えます。テック系の象徴ビジュアルに向きます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- フレネル項で縁が光る回転球。リムライト風 -->
<div class="stage">
  <canvas id="fresnel" aria-label="フレネル発光の球"></canvas>
  <div class="fallback" id="fr-fallback" hidden>WebGL を表示できない環境です</div>
  <div class="caption">
    <span class="badge">Fresnel</span>
    <h2>Glowing Sphere</h2>
    <p>縁だけが光るリムライト風シェーダーの球</p>
  </div>
</div>

【CSS】
/* 配色変数 */
:root {
  --ink: #eaf2ff;
  --accent: #6fe9ff;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  overflow: hidden;
}

.stage {
  position: relative;
  width: 100%;
  height: 360px;
  /* 暗い背景で縁の発光を際立たせる */
  background: radial-gradient(circle at 50% 42%, #0a1326 0%, #050a16 60%, #02040a 100%);
}

#fresnel {
  display: block;
  width: 100%;
  height: 100%;
}

/* WebGL非対応時のフォールバック表示 */
.fallback {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  font-size: 14px;
  letter-spacing: .06em;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .6);
}
/* hidden 属性を尊重(CSSの display 指定が [hidden] を上書きしないように) */
.fallback[hidden] { display: none; }

.caption {
  position: absolute;
  left: 28px;
  bottom: 24px;
  color: var(--ink);
  text-shadow: 0 2px 14px rgba(0, 0, 0, .7);
  pointer-events: none;
}

.badge {
  display: inline-block;
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
  background: rgba(111, 233, 255, .15);
  border: 1px solid rgba(111, 233, 255, .45);
  color: var(--accent);
  margin-bottom: 10px;
}

.caption h2 {
  font-size: 22px;
  font-weight: 700;
}

.caption p {
  margin-top: 4px;
  font-size: 13px;
  opacity: .75;
}

【JavaScript】
// フレネル発光の球:縁ほど強く光るリムライト風シェーダー
(function () {
  "use strict";
  const canvas = document.getElementById("fresnel");
  const fallback = document.getElementById("fr-fallback");
  // THREE未読込やcanvas不在なら安全に終了し、フォールバックを表示
  if (!canvas || typeof THREE === "undefined") {
    if (fallback) fallback.hidden = false;
    return;
  }

  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // WebGL初期化(失敗時はフォールバック)
  let renderer;
  try {
    renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
  } catch (e) {
    if (fallback) fallback.hidden = false;
    return;
  }
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
  camera.position.z = 4;

  // フレネル項(視線と法線の角度)で縁の発光量を決めるシェーダー
  const uniforms = {
    time: { value: 0 },
    glowColor: { value: new THREE.Color(0x6fe9ff) }, // 縁の発光色
    coreColor: { value: new THREE.Color(0x081326) }, // 内側の暗色
    power: { value: 2.6 }, // フレネルの鋭さ
  };

  const vertexShader = `
    varying vec3 vNormal;
    varying vec3 vView;
    void main() {
      // ビュー空間の法線と視線ベクトルを渡す
      vec4 mv = modelViewMatrix * vec4(position, 1.0);
      vNormal = normalize(normalMatrix * normal);
      vView = normalize(-mv.xyz);
      gl_Position = projectionMatrix * mv;
    }
  `;

  const fragmentShader = `
    precision highp float;
    varying vec3 vNormal;
    varying vec3 vView;
    uniform float time;
    uniform vec3 glowColor;
    uniform vec3 coreColor;
    uniform float power;

    void main() {
      // フレネル:正面ほど0、縁ほど1
      float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
      f = pow(f, power);
      // ゆるく脈動させる
      f *= 0.85 + 0.15 * sin(time * 1.5);
      // 内側暗・縁明をブレンド
      vec3 col = mix(coreColor, glowColor, clamp(f, 0.0, 1.0));
      // 縁を加算でさらに発光
      col += glowColor * f * 0.6;
      gl_FragColor = vec4(col, 1.0);
    }
  `;

  const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });
  const sphere = new THREE.Mesh(new THREE.SphereGeometry(1.2, 64, 64), material);
  scene.add(sphere);

  // 縁の光をさらに広げる外殻(裏面描画の加算ハロー)
  const haloMat = new THREE.ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader: `
      precision highp float;
      varying vec3 vNormal;
      varying vec3 vView;
      uniform vec3 glowColor;
      uniform float power;
      void main() {
        float f = 1.0 - max(dot(normalize(vNormal), normalize(vView)), 0.0);
        f = pow(f, power * 0.6);
        gl_FragColor = vec4(glowColor, f * 0.5);
      }
    `,
    transparent: true,
    blending: THREE.AdditiveBlending,
    side: THREE.BackSide,
    depthWrite: false,
  });
  const halo = new THREE.Mesh(new THREE.SphereGeometry(1.45, 48, 48), haloMat);
  scene.add(halo);

  function resize() {
    const w = canvas.clientWidth || 1;
    const h = canvas.clientHeight || 1;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  resize();
  window.addEventListener("resize", resize);

  let raf = 0;
  let running = true;
  const start = performance.now();
  function animate(now) {
    const t = (now - start) * 0.001;
    uniforms.time.value = t;
    // 縁の発光色を時間でゆっくり巡回させ、変化をはっきり見せる(無地の球でも動きが分かる)
    uniforms.glowColor.value.setHSL((t * 0.08) % 1, 0.85, 0.62);
    if (!reduceMotion) {
      sphere.rotation.y = t * 0.4;
      sphere.rotation.x = t * 0.15;
    }
    renderer.render(scene, camera);
    raf = requestAnimationFrame(animate);
  }
  animate(start);

  // タブ非表示で停止、復帰で再開
  document.addEventListener("visibilitychange", () => {
    if (document.hidden) {
      if (running) { cancelAnimationFrame(raf); running = false; }
    } else if (!running) {
      running = true;
      raf = requestAnimationFrame(animate);
    }
  });

  window.addEventListener("beforeunload", () => cancelAnimationFrame(raf));
})();

# 外部ライブラリ
https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。