JS(WebGL)で作るガラスの中に浮かぶ写真の3Dクリスタル

アニメーション

JS(WebGL)で作るガラスの中に浮かぶ写真の3Dクリスタル

投稿日2026/02/02

更新日2026/1/28

写真をただ平面として表示するのではなく、
ガラスの中に閉じ込められた立体物として見せることができたら、
写真の印象は大きく変わります。

本デモでは、WebGLを使って透明なガラスオブジェクトを構築し、
その内部に写真が浮かんでいるように見える
3Dクリスタル表現を実装しました。

反射や屈折、環境光の映り込みによって、
写真は単なるテクスチャではなく、
光を受けて存在している“物体”として感じられる構成になっています。

Preview プレビュー

Code コード

body {
    margin: 0;
    overflow: hidden;
    background-color: #020202;
}
canvas {
    display: block;
}
import * as THREE from 'https://esm.sh/three@0.170.0';
import { OrbitControls } from 'https://esm.sh/three@0.170.0/examples/jsm/controls/OrbitControls.js';
import { RoundedBoxGeometry } from 'https://esm.sh/three@0.170.0/examples/jsm/geometries/RoundedBoxGeometry.js';
import { RGBELoader } from 'https://esm.sh/three@0.170.0/examples/jsm/loaders/RGBELoader.js';

// ==========================================
// 設定
// ==========================================

// 1. 中央に表示する写真のURL
const PHOTO_URL = 'https://kumonosu.net/wp-content/uploads/2026/01/IMG_1298.jpg.webp';

// 2. 反射させる環境マップ
const ENV_URL = 'https://dl.polyhaven.org/file/ph-assets/HDRIs/hdr/1k/soliltude_1k.hdr';

const settings = {
    photoScale: 1.2,
    envIntensity: 1.5, // 反射の明るさ
    rotationSpeed: 0.5
};

// --- SCENE SETUP ---
const scene = new THREE.Scene();
scene.background = new THREE.Color('#020202');

const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 4);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// --- 360° ENVIRONMENT (Reflections) ---
const rgbeLoader = new RGBELoader();
rgbeLoader.load(ENV_URL, (texture) => {
    texture.mapping = THREE.EquirectangularReflectionMapping;
    scene.environment = texture;
    scene.environmentIntensity = settings.envIntensity;
});

// --- STARK LIGHTS ---
const arcBlue = new THREE.PointLight(0x00ffff, 8, 15); 
arcBlue.position.set(2, 3, 4);
scene.add(arcBlue);

const ironRed = new THREE.PointLight(0xff0000, 6, 15); 
ironRed.position.set(-3, -2, 3);
scene.add(ironRed);

const group = new THREE.Group();
scene.add(group);

// --- PHOTO OBJECT ---
const photoMat = new THREE.MeshStandardMaterial({ 
    side: THREE.DoubleSide, 
    roughness: 0.2,
    metalness: 0.2
});
const photoMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), photoMat);
group.add(photoMesh);

const textureLoader = new THREE.TextureLoader();
textureLoader.load(PHOTO_URL, (tex) => {
    tex.colorSpace = THREE.SRGBColorSpace;
    photoMat.map = tex;
    const aspect = tex.image.width / tex.image.height;
    const s = settings.photoScale;
    if (aspect > 1) {
        photoMesh.scale.set(s, s / aspect, 1);
    } else {
        photoMesh.scale.set(s * aspect, s, 1);
    }
});

// --- GLASS OBJECT ---
const glassMat = new THREE.MeshPhysicalMaterial({
    color: 0xffffff,
    transmission: 1.0,
    thickness: 1.0,
    ior: 1.5,
    roughness: 0.0,
    metalness: 0.1,
    transparent: true,
    envMapIntensity: 2.2 
});

const glassMesh = new THREE.Mesh(new RoundedBoxGeometry(2.1, 2.1, 0.8, 32, 0.2), glassMat);
group.add(glassMesh);

// --- ANIMATION LOOP ---
const clock = new THREE.Clock();

function animate() {
    requestAnimationFrame(animate);
    const time = clock.getElapsedTime();

    group.rotation.y += 0.005 * settings.rotationSpeed; 
    group.rotation.x = Math.sin(time * 0.4) * 0.1;
    
    arcBlue.intensity = 6 + Math.sin(time * 2) * 2;

    controls.update();
    renderer.render(scene, camera);
}

animate();

window.onresize = () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
};

Explanation 詳しい説明

仕様

本デモは、Three.jsを用いてWebGLシーンを構築しています。
シーン内には写真用のプレーンと、その前面に配置したガラスオブジェクトを重ねています。

写真はテクスチャとして読み込み、ガラスには MeshPhysicalMaterial を使用することで、透明度、屈折、反射といった物理的な性質を再現しています。

環境マップにはHDRIを使用し、周囲の光景がガラスに映り込むことで、平面的になりがちな透明表現に奥行きを与えています。

光と反射の設計

反射表現を強調するため、環境光に加えて複数のポイントライトを配置しています。

ライトの色と位置を調整することで、ガラス表面に現れる反射が単調にならず、素材としての存在感が感じられるよう設計しています。

オブジェクトをゆっくり回転させることで、反射や屈折の変化が自然に見える点も特徴です。

カスタマイズ

以下の項目を変更することで、見た目を調整できます。

  • 写真の差し替え
    PHOTO_URL を変更することで、任意の画像を表示できます。
  • ガラスの質感
    transmissionthicknessiorroughness を調整すると、透明感や屈折の強さが変わります。
  • 反射の強さ
    環境マップの強度や envMapIntensity を変更することで、映り込みの印象を調整できます。
  • 回転速度
    回転量を調整することで、静的な展示にも動きのある演出にも対応できます。

注意点

透明マテリアルとHDRIを使用しているため、端末やGPU性能によっては描画負荷が高くなる場合があります。

また、反射表現は環境マップに強く依存するため、HDRIの種類によって印象が大きく変わります。
用途に応じた環境マップの選定が重要です。

まとめ

写真を単なる画像として扱うのではなく、光と素材を持つオブジェクトとして見せることで、表現の幅は大きく広がります。

本デモは、WebGLを使った質感表現の一例として、写真表現を一段引き上げるための実装例です。