8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

光すら逃れられないブラックホール。重力の強さを調整できる、重力レンズゲームです。

Last updated at Posted at 2024-10-06

タイトル: 「歪んだ光のプログラム」

東京の夜は、まるで無数の星がビルの窓に宿ったかのように輝いている。中でも、渋谷のスクランブル交差点は、その光の海で最も煌びやかな場所だ。だが、プログラマーの翔太には、その光すらまるで別の世界の出来事のように見えていた。彼の目の前にあるのは、ディスプレイに映し出された何千ものコードの行列。それらが彼のすべての意識を占領していた。

「また、うまくいかないか…」

翔太は椅子にもたれかかり、大きくため息をついた。彼は現在、宇宙の現象をシミュレートする新しいプログラムに取り組んでいた。テーマは「重力レンズ効果」。ブラックホールや巨大な惑星が、その強大な重力で光を曲げる現象だ。光すら逃れられない巨大な力、しかしそれを美しく表現することは、意外にも繊細な調整を必要としていた。

「これさえ完成すれば…」

クライアントは彼にこの技術を使った新しいアートアプリケーションのプロトタイプを求めていた。ユーザーが画像をアップロードし、マウスでブラックホールのような歪みを操作して視覚的に楽しむ――そんな構想だ。翔太はそのアイデアに興味を持ち、数週間にわたり、昼夜を問わずこのプロジェクトに没頭していた。

スクリーンショット 2024-10-07 042612.png

今日も、翔太はディスプレイの前で時間を忘れていた。彼が最新の試作コードを実行すると、キャンバス上に画像が浮かび上がり、彼の指が触れるごとに微妙に歪み始める。しかし、何かがまだ違う。計算された歪み方が不自然なのだ。

「効果が強すぎるのか…?」

彼はスライダーを使って、重力の強さを調整できるようにした新しいコードの一部に目を通す。プログラムはマウスの動きを感知し、その座標に基づいて、光を引き寄せるかのようにピクセルを歪ませていく。計算式はシンプルだが、結果は複雑だ。ピクセルの距離、光のゆがみ、その距離の逆数を使った重力の増減。全てが微妙なバランスで成り立っていた。

「もう少し、リアルな挙動を再現できれば…」

翔太は、宇宙の無限の力を感じながらコードをいじり続けた。歪みの強さを表すスライダーを触るたびに、彼の画面上の世界が変わる。まるで、彼自身がその重力源を操作しているかのような感覚が湧いてきた。

ふと、彼はふいに思い出した。大学時代、夜空を見上げながら宇宙の果てに思いを馳せたことを。あの頃は、ブラックホールや銀河の膨張をただ遠い理論だと思っていた。だが今、彼の手の中でその力を操っている。どんなに小さくても、彼のプログラムはこの無限の宇宙の縮図のようだった。

スクリーンショット 2024-10-07 042652.png

「これでいい」

翔太は静かにマウスを動かし、コードの動作を確認した。光が滑らかに歪んでいく。ブラックホールが作り出す時空の歪みが、彼のコードによって現実のものとなっていた。

そしてその時、彼は思った。宇宙は巨大で遠いものだけれど、プログラムによってその一端を感じ、触れることができる。彼のコードが世界を歪め、光の道筋を変える。その美しさに、彼は少しだけ満足感を覚えた。

外では、東京の光が変わらず瞬いていたが、翔太の世界は少し違って見えていた。

簡単に重力レンズ効果を試すことができます。

コードをメモ帳などのテキストエディタに貼り付け、ファイル名を「index.html」として保存します。その後、保存したファイルをブラウザで開けば、コードが実行されます。

スクリーンショット 2024-10-07 042652.png

スクリーンショット 2024-10-07 042612.png

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>重力レンズ効果シミュレーション</title>
    <style>
        /* キャンバスに枠線を追加 */
        #canvas {
            border: 1px solid black;
        }
        /* 画像アップロードボタンのスタイル調整 */
        #upload {
            margin-bottom: 10px;
        }
        /* ステータス表示用のスタイル。画像の読み込み状態を表示 */
        #status {
            font-size: 24px;
            font-weight: bold;
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            background-color: rgba(0, 0, 0, 0.7);
            padding: 5px;
            border-radius: 5px;
        }
        /* スライダーのコンテナに余白を追加 */
        #slider-container {
            margin-top: 10px;
        }
        /* スライダーの値を表示する部分のスタイル */
        #strength-value {
            font-size: 16px;
            margin-left: 10px;
        }
    </style>
</head>
<body>

<h2>重力レンズ効果シミュレーション</h2>
<!-- 画像をアップロードするためのボタン -->
<input type="file" id="upload" accept="image/*">
<!-- 描画用のキャンバス -->
<canvas id="canvas" width="512" height="512"></canvas>
<!-- 画像の状態を表示する領域 -->
<div id="status">状態: 画像なし</div>

<!-- 効果の強さを調整するスライダー -->
<div id="slider-container">
    <label for="strength-slider">効果の強さ:</label>
    <input type="range" id="strength-slider" min="50" max="300" value="100">
    <span id="strength-value">100</span>
</div>

<script>
    // キャンバスと2Dコンテキストを取得
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    // ステータス表示用の要素
    const statusDiv = document.getElementById('status');

    // スライダーと強さ表示用の要素
    const strengthSlider = document.getElementById('strength-slider');
    const strengthValueDisplay = document.getElementById('strength-value');

    // 画像データを保持する変数
    let originalImageData = null;
    let distortedImageData = null;
    let strength = 100; // 初期の重力レンズ効果の強さ

    // 画像がアップロードされた際の処理
    document.getElementById('upload').addEventListener('change', (event) => {
        const file = event.target.files[0]; // 選択されたファイルを取得
        if (file) {
            const reader = new FileReader(); // ファイルを読み込むためのFileReaderを作成
            reader.onload = (e) => {
                const img = new Image(); // イメージ要素を作成
                img.onload = () => {
                    // 画像をキャンバスに描画
                    ctx.clearRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0, 512, 512);

                    // キャンバスから画像データを取得
                    originalImageData = ctx.getImageData(0, 0, 512, 512);
                    distortedImageData = ctx.createImageData(512, 512); // 歪み適用後のデータを保持する

                    // ステータスを更新
                    statusDiv.textContent = "状態: 画像読み込み完了";
                };
                img.src = e.target.result; // 読み込んだ画像をimg要素に設定
            };
            reader.readAsDataURL(file); // ファイルをデータURLとして読み込む
        }
    });

    // スライダーの値が変わった際に効果の強さを更新
    strengthSlider.addEventListener('input', (event) => {
        strength = event.target.value; // 新しい強さを取得
        strengthValueDisplay.textContent = strength; // 強さの値を表示
    });

    // キャンバス上でマウスが動いたときに歪みを適用
    canvas.addEventListener('mousemove', (event) => {
        if (originalImageData) {
            const mouseX = event.offsetX; // マウスのX座標
            const mouseY = event.offsetY; // マウスのY座標
            applyGravityLensEffect(mouseX, mouseY); // 重力レンズ効果を適用
        }
    });

    // 重力レンズ効果を適用する関数
    function applyGravityLensEffect(mouseX, mouseY) {
        const radius = 150; // 重力源の影響半径

        // キャンバス全体のピクセルを走査
        for (let y = 0; y < canvas.height; y++) {
            for (let x = 0; x < canvas.width; x++) {
                const dx = x - mouseX; // マウスからのX方向の距離
                const dy = y - mouseY; // マウスからのY方向の距離
                const distance = Math.sqrt(dx * dx + dy * dy); // マウスからの距離を計算

                // 重力レンズの影響範囲内でのみ歪ませる
                if (distance < radius) {
                    const distortionFactor = strength / (distance + 1); // 歪みの強さを計算
                    const offsetX = Math.floor(dx * distortionFactor); // X方向の歪み量
                    const offsetY = Math.floor(dy * distortionFactor); // Y方向の歪み量

                    // 元画像のピクセル位置を計算し、境界を超えないように調整
                    const srcX = Math.min(Math.max(0, x + offsetX), canvas.width - 1);
                    const srcY = Math.min(Math.max(0, y + offsetY), canvas.height - 1);

                    const srcIndex = (srcY * canvas.width + srcX) * 4; // 元画像のピクセル位置
                    const destIndex = (y * canvas.width + x) * 4; // 歪み後のピクセル位置

                    // 元の画像から歪んだピクセルを取得して適用
                    distortedImageData.data[destIndex] = originalImageData.data[srcIndex];
                    distortedImageData.data[destIndex + 1] = originalImageData.data[srcIndex + 1];
                    distortedImageData.data[destIndex + 2] = originalImageData.data[srcIndex + 2];
                    distortedImageData.data[destIndex + 3] = originalImageData.data[srcIndex + 3];
                } else {
                    // 影響範囲外のピクセルはそのままコピー
                    const srcIndex = (y * canvas.width + x) * 4;
                    distortedImageData.data[srcIndex] = originalImageData.data[srcIndex];
                    distortedImageData.data[srcIndex + 1] = originalImageData.data[srcIndex + 1];
                    distortedImageData.data[srcIndex + 2] = originalImageData.data[srcIndex + 2];
                    distortedImageData.data[srcIndex + 3] = originalImageData.data[srcIndex + 3];
                }
            }
        }

        // 歪みを適用した画像データをキャンバスに描画
        ctx.putImageData(distortedImageData, 0, 0);
    }
</script>

</body>
</html>

追記。迫力の3次元重力レンズゲーム。

マウスで視点操作できます。  

スクリーンショット 2024-10-08 152255.png

スクリーンショット 2024-10-08 152306.png

image.png

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Gravitational Lensing Effect with p5.js</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
</head>
<body>
  <script>
    let particles = [];
    const gravitySource = { x: 0, y: 0, z: 0, mass: 3000 };  // 大きな重力源
    const particleCount = 1000;
    const emissionPoint = { x: 0, y: 0, z: -500 };  // パーティクルの放出点
    let angleX = 0;
    let angleY = 0;

    // 初期設定
    function setup() {
      createCanvas(windowWidth, windowHeight, WEBGL);
      for (let i = 0; i < particleCount; i++) {
        particles.push(createParticle());
      }
    }

    // パーティクルの生成(放出点からランダムな方向に飛ばす)
    function createParticle() {
      return {
        position: createVector(emissionPoint.x, emissionPoint.y, emissionPoint.z),
        velocity: createVector(random(-1, 1), random(-1, 1), random(0.5, 1)).mult(2),  // ランダムな方向に加速
      };
    }

    // 重力レンズ効果の計算
    function applyGravitationalLensing(particle) {
      let dir = p5.Vector.sub(createVector(gravitySource.x, gravitySource.y, gravitySource.z), particle.position);
      let distance = dir.mag();
      let force = gravitySource.mass / (distance * distance + 50); // 重力源からの力
      dir.normalize();
      particle.velocity.add(dir.mult(force * 0.05)); // 重力による加速
    }

    // アニメーション
    function draw() {
      background(0);
      rotateX(angleX);
      rotateY(angleY);

      // 重力源の描画
      fill(255, 0, 0);
      noStroke();
      sphere(20); // 重力源の中心

      // パーティクルの更新と描画
      for (let particle of particles) {
        applyGravitationalLensing(particle);
        particle.position.add(particle.velocity);

        // パーティクルの描画
        push();
        translate(particle.position.x, particle.position.y, particle.position.z);
        fill(255);
        sphere(2);
        pop();
      }

      // マウスによる視点変更
      if (mouseIsPressed) {
        angleX += (mouseY - pmouseY) * 0.01;
        angleY += (mouseX - pmouseX) * 0.01;
      }
    }

    // ウィンドウリサイズ対応
    function windowResized() {
      resizeCanvas(windowWidth, windowHeight);
    }
  </script>
</body>
</html>

8
7
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?