1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GPUのパストレはやっぱり速くてよい

Last updated at Posted at 2025-12-06

レイトレをGPU実装するのは、再帰処理できない場合、大変。再帰的処理をコードで展開する必要があるから。

それに比べて、パストレをGPU実装するのは、楽である。確率で処理するので、上記の心配がない。ただし、確立計算に伴うノイズが絶対に生じる。デノイズ必須。

このGPUパストレの特徴

チェック床+普通の球、ガラス球、ミラー球
デノイズなし
caustics表現なし
VSYC回避のため frameRate(960)

操作

数字キーの1~9で、積算の度合いを調整。
1ならそのまま表示
6なら32回分を積算して表示
(2^(n-1)=32)
ノイズは軽減するが、フレームレートは落ちる。

計算速度

Processing 4.4.10
MBP M1
16 Core GPU
この環境で、32回積算時に、20FPSぐらい出るので、GPU処理の恩恵を感じる。
積算無しで、デノイズするのが、一番いいと思う。
ちなみに、解像度を変更しても、ほとんどFPSには影響しません。CPUと比べて、圧倒的に有利です。

image.png

他環境

MSIのノートPC
■iGPUの場合
32回積算時に、5FPS
■GeForce 3050Ti Laptop(電力半分版)
GPU優先にしたけれど、iGPUと使用率が半々になってしまって、性能出しきれない。
iGPUに引っ張られる状態で、
32回積算時に、28FPS

デスクトップPCで後日試す。

pathtraceShader.pde
PShader pathtracing;

// 見えない積算バッファ
PGraphics accum;

// 表示専用バッファ(確定フレームだけ表示)
PGraphics displayImg;

int subSamplesPerFrame = 1; // n = 1,2,4,8...
int subSampleIndex = 0;

float animTime = 0.0;
float timeStep = 0.005;

int logicalFrameCount = 0;   // 表示したフレーム数
float logicalFPS = 0;        // 表示FPS
int lastDisplayTime = 0;     // 最後に表示した時刻(millis)

void setup() {
  //fullScreen(P2D);
  size(1280, 720, P2D);
  pixelDensity(1);
  frameRate(960);            // VSync回避してGPU100%使うため

  pathtracing = loadShader("pathtracing.txt");

  accum      = createGraphics(width, height, P2D);
  displayImg = createGraphics(width, height, P2D);

  clearAccum();
  clearDisplay();

  lastDisplayTime = millis();
}

// 積算バッファだけ消す
void clearAccum() {
  accum.beginDraw();
  accum.background(0);
  accum.endDraw();
  subSampleIndex = 0;
}

// 表示バッファだけ消す
void clearDisplay() {
  displayImg.beginDraw();
  displayImg.background(0);
  displayImg.endDraw();
}

void draw() {

  // --------- 見えない所で1サンプル積む ---------
  subSampleIndex++;

  accum.beginDraw();
  accum.shader(pathtracing);
  pathtracing.set("u_resolution", (float)width, (float)height);
  pathtracing.set("u_prevFrame", accum);
  pathtracing.set("u_sampleCount", (float)subSampleIndex);
  pathtracing.set("u_time", animTime);   // n サンプルの間は固定
  accum.rect(0, 0, width, height);
  accum.resetShader();
  accum.endDraw();

  // --------- nサンプル積み終わった瞬間だけ確定表示 ---------
  if (subSampleIndex >= subSamplesPerFrame) {

    // 確定結果を displayImg にコピー
    displayImg.beginDraw();
    displayImg.image(accum, 0, 0);
    displayImg.endDraw();

    // 表示フレームのFPSを計算
    int now = millis();
    int dt = now - lastDisplayTime;
    if (dt > 0) {
      logicalFPS = 1000.0 / dt;  // ms → fps
    }
    lastDisplayTime = now;
    logicalFrameCount++;
    
    // 次フレームへ(ライトだけここで動く)
    animTime += timeStep * subSamplesPerFrame;

    // 次のフレーム用に accum をクリア
    clearAccum();
  }

  // --------- 画面には「確定済み」だけ表示 ---------
  image(displayImg, 0, 0);

  // デバッグ表示
  fill(255);
  textAlign(LEFT, TOP);
  text(
    "n = " + subSamplesPerFrame +
    "\n subSampleIndex = " + subSampleIndex +
    //"\n animTime = " + nf(animTime, 1, 2) +
    "\n FPS = " + nf(logicalFPS, 1, 1) +
    "\n Frames = " + logicalFrameCount,
    10, 10
    );
}


void keyPressed() {
  if (key >= '1' && key <= '9') {
    int k = key - '0';                  // '1'→1, '2'→2 ...
    subSamplesPerFrame = 1 << (k - 1);  // 2^(k-1) → 1,2,4,8...256
    clearAccum();
    clearDisplay();
  }
}
pathtracing.txt
#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

uniform vec2  u_resolution;
uniform float u_time;
uniform float u_sampleCount;
uniform sampler2D u_prevFrame;

const float PI = 3.14159265359;

// -------------------- 基本構造体 --------------------

struct Ray {
    vec3 ori;
    vec3 dir;
};

struct Hit {
    bool hit;
    float t;
    vec3 pos;
    vec3 normal;
    int  matId;
};

// マテリアルID
const int MAT_FLOOR  = 0;
const int MAT_DIFF   = 1;
const int MAT_GLASS  = 2;
const int MAT_MIRROR = 3;

// 球
const int NUM_SPHERES = 3;
vec3  sphereCenter[NUM_SPHERES];
float sphereRadius[NUM_SPHERES];
int   sphereMat[NUM_SPHERES];

vec3  lightPos;
vec3  lightColor = vec3(6.0);

// -------------------- 乱数 --------------------

float hash31(vec3 p) {
    p  = fract(p * 0.1031);
    p += dot(p, p.yzx + 33.33);
    return fract((p.x + p.y) * p.z);
}

float rand(inout vec3 seed) {
    float r = hash31(seed);
    seed += vec3(1.0, 1.0, 1.0);
    return r;
}

// コサイン重み付き半球サンプル
vec3 cosineSampleHemisphere(vec2 u) {
    float r = sqrt(u.x);
    float theta = 2.0 * PI * u.y;
    float x = r * cos(theta);
    float z = r * sin(theta);
    float y = sqrt(max(0.0, 1.0 - u.x));
    return vec3(x, y, z);
}

// -------------------- シーン初期化 --------------------

void initScene() {
    // 左:赤い拡散球
    sphereCenter[0] = vec3(-1.2, 0.5, 0.0);
    sphereRadius[0] = 0.5;
    sphereMat[0]    = MAT_DIFF;

    // 中央:ガラス球
    sphereCenter[1] = vec3( 0.0, 0.5, 0.0);
    sphereRadius[1] = 0.5;
    sphereMat[1]    = MAT_GLASS;

    // 右:ミラー球
    sphereCenter[2] = vec3( 1.2, 0.5, 0.0);
    sphereRadius[2] = 0.5;
    sphereMat[2]    = MAT_MIRROR;

    lightPos = vec3(2.0 * sin(u_time * 0.5), 3.0, 2.0 * cos(u_time * 0.5));
}

// -------------------- 交差計算 --------------------

bool intersectSphere(Ray ray, vec3 center, float radius, out float t) {
    vec3 oc = ray.ori - center;
    float b = dot(oc, ray.dir);
    float c = dot(oc, oc) - radius * radius;
    float h = b * b - c;
    if (h < 0.0) return false;
    h = sqrt(h);
    float t0 = -b - h;
    float t1 = -b + h;
    t = (t0 > 0.0) ? t0 : t1;
    return t > 0.0;
}

// 無限平面 y = 0
bool intersectPlane(Ray ray, float y, out float t) {
    if (abs(ray.dir.y) < 1e-4) return false;
    t = (y - ray.ori.y) / ray.dir.y;
    return t > 0.0;
}

// 背景(空)
vec3 backgroundColor(vec3 dir) {
    float t = 0.5 * (dir.y + 1.0);
    return mix(vec3(0.7, 0.8, 1.0), vec3(0.1, 0.1, 0.2), t);
}

// 床チェッカー
vec3 floorColor(vec3 p) {
    float scale = 2.0;
    float fx = floor(p.x * scale);
    float fz = floor(p.z * scale);
    float checker = mod(fx + fz, 2.0);
    return mix(vec3(0.9), vec3(0.1), checker);
}

// シーン全体の交差
Hit traceScene(Ray ray) {
    Hit h;
    h.hit = false;
    h.t = 1e20;
    h.matId = -1;

    float t;

    // 床
    if (intersectPlane(ray, 0.0, t)) {
        if (t < h.t) {
            h.hit = true;
            h.t = t;
            h.pos = ray.ori + ray.dir * t;
            h.normal = vec3(0.0, 1.0, 0.0);
            h.matId = MAT_FLOOR;
        }
    }

    // 球たち
    for (int i = 0; i < NUM_SPHERES; i++) {
        if (intersectSphere(ray, sphereCenter[i], sphereRadius[i], t)) {
            if (t < h.t) {
                h.hit = true;
                h.t   = t;
                h.pos = ray.ori + ray.dir * t;
                h.normal = normalize(h.pos - sphereCenter[i]);
                h.matId  = sphereMat[i];
            }
        }
    }
    return h;
}

// -------------------- シェーディング --------------------

// 拡散:床と赤球を共通処理
vec3 shadeDiffuse(Hit h, inout Ray ray, inout vec3 throughput, inout vec3 seed) {
    vec3 N = normalize(h.normal);

    vec3 baseColor;
    if (h.matId == MAT_FLOOR) {
        baseColor = floorColor(h.pos);
    } else {
        // 赤い拡散球
        baseColor = vec3(0.9, 0.2, 0.2);
    }

    // 直射光(全ての球でシャドウ)
    vec3 Ldir = normalize(lightPos - h.pos);
    float distL = length(lightPos - h.pos);

    Ray shadowRay;
    shadowRay.ori = h.pos + N * 1e-3;
    shadowRay.dir = Ldir;

    bool blocked = false;
    float tSh;

    for (int i = 0; i < NUM_SPHERES; i++) {
        if (intersectSphere(shadowRay, sphereCenter[i], sphereRadius[i], tSh) && tSh < distL) {
            blocked = true;
            break;
        }
    }

    float nl = max(dot(N, Ldir), 0.0);
    vec3 direct = vec3(0.0);
    if (!blocked && nl > 0.0) {
        float att = 1.0 / (1.0 + 0.1 * distL * distL);
        direct = baseColor * lightColor * nl * att;
    }

    vec3 ambient = baseColor * 0.05;

    // 次の間接レイ
    vec2 u = vec2(rand(seed), rand(seed));
    vec3 localDir = cosineSampleHemisphere(u);

    vec3 up = (abs(N.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
    vec3 T = normalize(cross(up, N));
    vec3 B = cross(N, T);

    vec3 worldDir = normalize(
        T * localDir.x +
        N * localDir.y +
        B * localDir.z
    );

    ray.ori = h.pos + N * 1e-3;
    ray.dir = worldDir;

    throughput *= baseColor;

    return direct + ambient;
}

// ガラス:確率的な反射 / 屈折
vec3 shadeGlass(Hit h, inout Ray ray, inout vec3 throughput, inout vec3 seed) {
    vec3 N = normalize(h.normal);
    float ior = 1.5;

    bool entering = dot(ray.dir, N) < 0.0;
    vec3 n = entering ? N : -N;
    float eta = entering ? (1.0 / ior) : (ior / 1.0);

    float cosI = clamp(-dot(ray.dir, n), 0.0, 1.0);
    float R0 = pow((1.0 - ior) / (1.0 + ior), 2.0);
    float F  = R0 + (1.0 - R0) * pow(1.0 - cosI, 5.0);

    float pReflect = clamp(F, 0.05, 0.95);
    float r = rand(seed);

    vec3 glassTint = vec3(0.98, 0.99, 1.0);

    if (r < pReflect) {
        // 反射
        vec3 reflDir = reflect(ray.dir, n);
        ray.ori = h.pos + n * 1e-3;
        ray.dir = normalize(reflDir);
    } else {
        // 屈折
        vec3 refrDir = refract(ray.dir, n, eta);
        if (length(refrDir) == 0.0) {
            // 全反射
            vec3 reflDir = reflect(ray.dir, n);
            ray.ori = h.pos + n * 1e-3;
            ray.dir = normalize(reflDir);
        } else {
            ray.ori = h.pos - n * 1e-3;
            ray.dir = normalize(refrDir);
        }
        throughput *= glassTint;
    }
    return vec3(0.0);
}

// ミラー:完全鏡面(レイの向きを変えるだけ)
vec3 shadeMirror(Hit h, inout Ray ray, inout vec3 throughput, inout vec3 seed) {
    vec3 N = normalize(h.normal);
    vec3 reflDir = reflect(ray.dir, N);
    ray.ori = h.pos + N * 1e-3;
    ray.dir = normalize(reflDir);
    // ミラー自体は発光しない
    return vec3(0.0);
}

// -------------------- main --------------------

void main() {
    initScene();

    vec2 fragCoord = gl_FragCoord.xy;
    vec2 uv = (fragCoord / u_resolution) * 2.0 - 1.0;
    uv.x *= u_resolution.x / u_resolution.y;

// ===== カメラ:y軸周りを回転しながら中央を見る =====
vec3 camTarget = vec3(0.0, 0.5, 0.0);  // 真ん中のガラス球あたりを見る

float camRadius = 3.0;   // 中心からの距離
float camHeight = 1.5;   // カメラの高さ
float camSpeed  = 0.2;   // 回転速度

// u_time は Processing 側の animTime から来ている
vec3 camPos = vec3(
    camRadius * sin(u_time * camSpeed),
    camHeight,
    camRadius * cos(u_time * camSpeed)
);

vec3 camDir   = normalize(camTarget - camPos);
vec3 up       = vec3(0.0, 1.0, 0.0);
vec3 camRight = normalize(cross(camDir, up));
vec3 camUp    = cross(camRight, camDir);

float fov   = radians(45.0);
float focal = 1.0 / tan(fov * 0.5);

vec3 dir = normalize(
    camRight * uv.x +
    camUp    * uv.y +
    camDir   * focal
);


    Ray ray;
    ray.ori = camPos;
    ray.dir = dir;

    vec3 seed = vec3(fragCoord, u_sampleCount);

    const int MAX_BOUNCES = 6;

    vec3 L = vec3(0.0);
    vec3 throughput = vec3(1.0);

    for (int bounce = 0; bounce < MAX_BOUNCES; bounce++) {
        Hit h = traceScene(ray);

        if (!h.hit) {
            L += throughput * backgroundColor(ray.dir);
            break;
        }

        if (h.matId == MAT_FLOOR || h.matId == MAT_DIFF) {
            vec3 direct = shadeDiffuse(h, ray, throughput, seed);
            L += throughput * direct;
        } else if (h.matId == MAT_GLASS) {
            vec3 direct = shadeGlass(h, ray, throughput, seed);
            L += throughput * direct;
        } else if (h.matId == MAT_MIRROR) {
            vec3 direct = shadeMirror(h, ray, throughput, seed);
            L += throughput * direct;
        }

        // ロシアンルーレット
        if (bounce > 2) {
            float p = max(throughput.r, max(throughput.g, throughput.b));
            p = clamp(p, 0.05, 1.0);
            if (rand(seed) > p) break;
            throughput /= p;
        }
    }

    // 累積
    vec2 texCoord = fragCoord / u_resolution;
    vec3 prev = texture2D(u_prevFrame, texCoord).rgb;
    float n = max(u_sampleCount, 1.0);
    vec3 accumColor = (prev * (n - 1.0) + L) / n;

    gl_FragColor = vec4(accumColor, 1.0);
}
1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?