2
4

Pythonのコードを使って楽しくレイトレーシング。

Last updated at Posted at 2024-07-28

image.png

ショートストーリー: 「小さな探検家とレイトレーシングの世界」

小学生のケンタは、最近の学校の課題でプログラミングに興味を持つようになりました。彼はコンピュータを使っていろいろなことができると知り、自分でも試してみたくなりました。ある日、彼の友達から「レイトレーシング」というものを教えてもらい、それが光と影を使って美しい画像を作り出す方法だと知りました。

「これってすごいな!僕も自分でレイトレーシングの画像を作ってみたい!」ケンタは興奮しながら考えました。彼のパソコンにPythonというプログラミング言語がインストールされていることを知っていたので、早速自分のプロジェクトを始めることに決めました。

ケンタはまず、Pythonのコードを使ってシンプルなレイトレーシングのプログラムを書きました。彼は「Gradio」というツールを使って、自分のプログラムに使いやすいインターフェースを追加しました。これで、スライダーを使って球体の数や光の強さを調整し、「レンダリング」ボタンを押すだけで自分だけのレイトレーシング画像が生成できるようになりました。

毎日のようにケンタは自分のプログラムを使って、カラフルな球体を画面に描き出しました。球体の鏡面反射や光の強さを調整するたびに、新しい美しい画像が生成されるのを見て、彼は目を輝かせました。彼の作った画像は、まるで自分の夢の中の景色が現れたようで、どれも素晴らしいものでした。

ある日、ケンタは特に気に入った画像を見つけました。それは、輝くゴールドの球体がいくつも並ぶ美しいシーンでした。「これを友達にも見せたいな」とケンタは思いました。グラディオのインターフェースを使って、彼はその画像を簡単にダウンロードすることができました。

「プログラミングって、こんなに楽しいんだ!」ケンタは嬉しそうに笑いました。彼は自分が作ったレイトレーシングの画像を見ながら、これからもたくさんの素敵な作品を作ることを決めました。

レイトレーシングの世界には、無限の可能性が広がっていることを知ったケンタ。彼の探検は、これからも続いていくことでしょう。

実行画面。

スクリーンショット 2024-07-29 045043.png

Gradioインターフェースの使い方:

スライダーを使って球体の数(num_spheres)、最小鏡面反射(min_specular)、最大鏡面反射(max_specular)、および光の強さ(light_strength)を調整できるようにしました。
ボタンを押すことで画像が生成されます。
このコードを実行すると、ブラウザにGradioのインターフェースが表示され、ユーザーがスライダーで設定を調整し、「Render」ボタンを押すことで画像が生成されます。

# 必要なライブラリのインストール
!pip install numpy matplotlib pillow gradio

import numpy as np
import matplotlib.pyplot as plt
import time
from PIL import Image
import gradio as gr

# ベクトルを表現するクラス Vec3
class Vec3:
    def __init__(self, x, y, z):
        # ベクトルの3つの成分を初期化する
        self.x = x
        self.y = y
        self.z = z

    def __add__(self, other):
        # ベクトル同士の加算
        return Vec3(self.x + other.x, self.y + other.y, self.z + other.z)

    def __sub__(self, other):
        # ベクトル同士の減算
        return Vec3(self.x - other.x, self.y - other.y, self.z - other.z)

    def __mul__(self, other):
        # ベクトル同士の乗算またはスカラーとの乗算
        if isinstance(other, Vec3):
            return Vec3(self.x * other.x, self.y * other.y, self.z * other.z)
        else:
            return Vec3(self.x * other, self.y * other, self.z * other)

    def dot(self, other):
        # ベクトル同士のドット積
        return self.x * other.x + self.y * other.y + self.z * other.z

    def normalize(self):
        # ベクトルの正規化
        length = np.sqrt(self.x**2 + self.y**2 + self.z**2)
        return self * (1.0 / length)

    def reflect(self, normal):
        # 法線ベクトルを使った反射ベクトルの計算
        return self - normal * 2 * self.dot(normal)

    def __neg__(self):
        # ベクトルの反転
        return Vec3(-self.x, -self.y, -self.z)

    def to_color(self):
        # ベクトルをRGBカラーに変換
        return (int(255 * np.clip(self.x, 0, 1)), int(255 * np.clip(self.y, 0, 1)), int(255 * np.clip(self.z, 0, 1)))

# 球を表現するクラス Sphere
class Sphere:
    def __init__(self, center, radius, color, specular):
        # 球の中心、半径、色、鏡面反射の強さを初期化する
        self.center = center
        self.radius = radius
        self.color = color
        self.specular = specular

    def intersect(self, ray_origin, ray_dir):
        # レイと球の交差判定
        oc = ray_origin - self.center
        a = ray_dir.dot(ray_dir)
        b = 2.0 * oc.dot(ray_dir)
        c = oc.dot(oc) - self.radius * self.radius
        discriminant = b * b - 4 * a * c

        if discriminant < 0:
            return False, None
        else:
            t1 = (-b - np.sqrt(discriminant)) / (2.0 * a)
            t2 = (-b + np.sqrt(discriminant)) / (2.0 * a)
            return True, min(t1, t2) if t1 > 0 else t2

def ray_trace(ray_origin, ray_dir, spheres, light, light_strength):
    # レイを使ってシーンを描画する関数
    color = Vec3(0, 0, 0)  # 初期の色は黒
    nearest_t = float('inf')  # 最も近い交点の初期化
    hit_sphere = None  # ヒットした球の初期化

    for sphere in spheres:
        hit, t = sphere.intersect(ray_origin, ray_dir)
        if hit and t < nearest_t:
            nearest_t = t
            hit_sphere = sphere

    if hit_sphere:
        hit_point = ray_origin + ray_dir * nearest_t
        normal = (hit_point - hit_sphere.center).normalize()
        view_dir = -ray_dir
        light_dir = (light - hit_point).normalize()
        reflect_dir = light_dir.reflect(normal)

        diffuse = max(normal.dot(light_dir), 0)
        specular = max(view_dir.dot(reflect_dir), 0) ** hit_sphere.specular
        color = hit_sphere.color * (diffuse * light_strength + specular)

    return color

def render(image_width, image_height, spheres, light, light_strength):
    # 画像をレンダリングする関数
    aspect_ratio = image_width / image_height
    camera_origin = Vec3(0, 0, -1)
    image = Image.new("RGB", (image_width, image_height))

    for y in range(image_height):
        for x in range(image_width):
            pixel_x = (2 * (x + 0.5) / image_width - 1) * aspect_ratio
            pixel_y = 1 - 2 * (y + 0.5) / image_height
            pixel_pos = Vec3(pixel_x, pixel_y, 0)

            ray_dir = (pixel_pos - camera_origin).normalize()
            color = ray_trace(camera_origin, ray_dir, spheres, light, light_strength)
            image.putpixel((x, y), color.to_color())

    return image

def generate_random_scene(num_spheres, min_specular, max_specular):
    # ランダムなシーンを生成する関数
    spheres = []
    for _ in range(num_spheres):
        center = Vec3(np.random.uniform(-1, 1), np.random.uniform(-1, 1), np.random.uniform(1, 3))
        radius = np.random.uniform(0.4, 0.6)
        color = Vec3(np.random.rand(), np.random.rand(), np.random.rand())  # カラフルな色
        specular = np.random.uniform(min_specular, max_specular)  # スライダーで指定された範囲
        spheres.append(Sphere(center, radius, color, specular))
    return spheres

def create_image(num_spheres, min_specular, max_specular, light_strength):
    # 画像を生成する関数
    image_width = 800
    image_height = 400
    light = Vec3(2, 2, -1)

    spheres = generate_random_scene(num_spheres, min_specular, max_specular)
    image = render(image_width, image_height, spheres, light, light_strength)

    return np.array(image)

# Gradioインターフェース
def generate_image(num_spheres, min_specular, max_specular, light_strength):
    # Gradioインターフェース用の画像生成関数
    image = create_image(num_spheres, min_specular, max_specular, light_strength)
    return Image.fromarray(image)

# Gradioインターフェースの設定
interface = gr.Interface(
    fn=generate_image,
    inputs=[
        gr.Slider(1, 10, value=5, step=1, label="Number of Spheres"),
        gr.Slider(10, 300, value=100, step=10, label="Min Specular"),
        gr.Slider(10, 300, value=200, step=10, label="Max Specular"),
        gr.Slider(0.1, 5.0, value=1.0, step=0.1, label="Light Strength")
    ],
    outputs="image",
    live=False,  # ユーザーが「レンダリング」ボタンを押さない限り、画像が更新されないようにする
    title="Metallic Sphere Renderer",
    description="Adjust the number of spheres, their specular highlights, and light strength. Press 'Render' to generate the image."
)

# Gradioインターフェースを起動
interface.launch()

このコードでは、以下の主要な部分を実装しています:

ベクトル演算(Vec3クラス): 3Dベクトルの基本的な演算(加算、減算、スカラー乗算、正規化など)を行います。

球体の定義(Sphereクラス): 球体の中心、半径、色、鏡面反射の強さを定義し、レイとの交差判定を行います。

レイ・トレーシング(ray_trace関数): レイと球体の交差を計算し、シェーディング(光の拡散と鏡面反射)を行います。

画像のレンダリング(render関数): 各ピクセルにレイを投射し、シーンを描画します。

=========================

参考。

光と円の冒険
ある日、放課後の理科室で中学生の男の子、タケシは放課後の理科室にやって来た。理科の先生が待っており、タケシに興味深い課題を出した。

「タケシ、今日はレイトレーシングについて学ぼう。光線と円がどう関わるのかを計算してみるんだ。」

タケシは目を輝かせた。「レイトレーシングって、光が物体に当たって反射する仕組みを計算するんですね。どうやってやるんですか?」

二次元の世界へ

先生は黒板に単位円を描き、その中心に原点を示した。「この円は半径1の単位円で、中心は原点だ。ここから、光線が発射され、円にぶつかるとどうなるかを見てみよう。まず、光線と円の交点を求めるところから始めるんだ。」

タケシはうなずきながら計算を始めた。「光線の方程式と円の方程式を使って交点を求めるんですね。」

光線と円の交点

タケシは光線の方程式と円の方程式を連立させて、交点を計算した。「光線の方程式は y = mx + b で、円の方程式は x^2 + y^2 = r^2 です。この2つを連立させることで、光線が円にぶつかる点を求めます。」

image.png

タケシは光線と円の方程式を連立させて、交点の座標を求めた。「これで光線が円にぶつかる点がわかりました。」

反射の計算

タケシは次に、光線が円に当たった後の反射を計算した。「光線が円に当たると、その点での接線が反射面になります。光線の反射は、この接線に対する鏡に映るようなものです。」

タケシは交点での接線を計算し、反射方向を求めた。「この接線を使って、光線がどのように反射するかがわかります。接線は光線の当たった点での円の表面に直角な方向です。」

image.png

結果を可視化する
タケシは黒板に光線、円、そして反射光線を描いた。光線が円に当たる点を赤で示し、その後の反射光線を緑で描いた。「これで、光線が円に当たってどのように反射するかがわかりますね。」

image.png

タケシは自信を持って先生に説明した。「光線が円にぶつかる点を計算し、その反射は円の接線に対する鏡のように考えれば、レイトレーシングが理解できます。」

理科室を後にして
先生は微笑みながらタケシに言った。「よくできたね、タケシ。光と円の関係を理解し、反射の計算ができたことは素晴らしいことだ。」

タケシは理科室を後にしながら、自分の理解が深まったことに満足感を感じた。光と円の冒険を通じて、彼は数学と物理の奥深さを少しずつ感じ始め、次の授業に向けて期待が高まっていた。

2
4
0

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
2
4