レイトレーシング [2]
タエチャスク ナッチャノン 05241011
基本課題の拡張として、反射を実装することにした。
ECS のリファクタリング
-
ColorComponent を廃止し、MaterialComponent を導入した。(BRDF を実装しようとして、かなり複雑になったため、roughness, metallic, specular などのプロパティは使わなかった。)
-
EntityRegistry の
writeBuffer
メソッドをリファクタリングした。以前は更新が必要なエンティティごとに GPU にデータを書き込んでいたが、新しい実装では、まず CPU 側のデータ配列をすべて更新し、最後に各コンポーネントのバッファに一度だけまとめて書き込むように変更した。これにより、GPU への書き込み回数が大幅に削減された。 -
setup system の導入。初期化に役に立つ
MaterialComponent.ts
const MaterialType = {
Diffuse: 0,
Specular: 1, // 鏡面反射
} as const;
class MaterialComponent extends RayTracingComponent {
static readonly size = 8;
static readonly name = "Material" as const;
static readonly dataclass = Float32Array;
color: Vector4;
materialType: (typeof MaterialType)[keyof typeof MaterialType];
// ... other properties that i didn't use for brdf (lol)
constructor(options: MaterialComponentOptions = {}) {
super();
this.color = options.color ?? new Vector4(1, 1, 1, 1);
this.materialType = options.type ?? MaterialType.Diffuse;
// ...
}
}
再帰的レイトレーシングによる反射
メインに使われる関数は trace
と shade
である。WGSL は再帰的な関数呼び出しをサポートしていないため、for ループを使用した。
trace 関数
trace
関数は、与えられた光線 (Ray) に対して、シーン内のすべてのオブジェクト(床を含む)との交点を計算し、最も近い交点の情報 (Intersection) を返す。以前のようにオブジェクトごとに色を計算するのではなく、どのエンティティにヒットしたかという情報だけを返す、より純粋な交差判定関数となった。
fn trace(ray: Ray, intersection: Intersection) -> Intersection {
var closest = intersection;
// Check floor intersection
let t = -ray.origin.y / ray.direction.y;
if (t > 0.0 && ((closest.t < 0.0) || (t < closest.t))) {
let position = ray.origin + t * ray.direction;
let gridX = floor(position.x / floorGridSize);
let gridY = floor(position.z / floorGridSize);
let isEven = (gridX + gridY) % 2 == 0;
closest.t = t;
if (isEven) {
closest.e = -2; // -2 = base floor
} else {
closest.e = -3; // -3 = accent floor
}
}
// Check sphere intersections
for (var e = 0u; e < arrayLength(&entityMetadata); e++) {
let eMeta = entityMetadata[e];
if (eMeta.position == 1u && eMeta.sphere == 1u && eMeta.material == 1u) {
let ni = sphereIntersect(ray, e);
if (ni.t > 0.0 && ((closest.t > 0.0 && ni.t < closest.t) || closest.t <= 0.0)) {
closest = ni;
}
}
}
return closest;
}
trace はほとんど元の関数の一部をそのまま使用しているということ。変わった部分は、床のグリッドを別々のオブジェクトとして扱うようにした。
shade 関数
shade
関数は、ピクセルの最終的な色を決定する。この関数はループを使い、光の反射をシミュレートする。
- 最初の光線で
trace
を呼び出し、最も近い交点を見つける。initialIntersection 使わなくてもいい説はあるが、なぜか真っ青(空の色)になって、initialIntersection を使うことにしないと解決できないことがあった。バグを修正するのに時間がかかったので、そのままにした。 - 交点が見つからなかった場合、背景色を返して終了する。
- 交点が見つかった場合、その点の Material を取得する。
- マテリアルが Diffuse (拡散) の場合:
computeIrradiance
を呼び出して、光源からの直接光と環境光を計算し、その色を最終的な色としてループを終了する。 - マテリアルが Specular (鏡面反射) の場合:
- 反射光の寄与度 (Ks) をマテリアルの色で更新する。
- 光線の始点を交点から少しずらし、反射ベクトルを新しい光線の方向として設定する。
- ループの次のイテレーションに進み、反射した光線を追跡する。
- このループを
MAX_BOUNCES
回繰り返すことで、複数回の反射を表現する。
fn shade(initialRay: Ray, initialIntersection: Intersection) -> vec4<f32> {
var ray = initialRay;
var intersection = initialIntersection;
var finalColor = vec3<f32>(0.0);
var Ks = vec3<f32>(1.0);
for (var bounce = 0u; bounce < MAX_BOUNCES; bounce++) {
if (bounce > 0u) {
intersection = trace(ray, Intersection(-1.0, -1));
}
if (intersection.t <= 0.0) {
finalColor += Ks * background.rgb;
break;
}
let hitPosition = ray.origin + intersection.t * ray.direction;
var normal: vec3<f32>;
var material: Material;
if (intersection.e == -2) {
normal = floorNormal;
material = floorBaseMaterial;
} else if (intersection.e == -3) {
normal = floorNormal;
material = floorAccentMaterial;
} else if (intersection.e >= 0) {
let eu = u32(intersection.e);
material = materials[eu];
if (entityMetadata[eu].sphere == 1u) {
normal = calculateSphereNormal(ray, intersection);
}
} else {
finalColor += Ks * background.rgb;
break;
}
if (material.materialType == 0.0) {
let Kd = material.color.rgb;
let irradiance = computeIrradiance(ray, hitPosition, normal);
let diffuseColor = (Kd / PI) * irradiance;
finalColor += Ks * diffuseColor;
break;
} else if (material.materialType == 1.0) {
Ks *= material.color.rgb;
ray.origin = hitPosition + normal * 0.001;
ray.direction = reflect(ray.direction, normal);
} else {
finalColor += Ks * material.color.rgb;
break;
}
}
return vec4<f32>(finalColor, 1.0);
}
computeIrradiance
元の diffuse lighting を使っていたが、ポイントライトを実装した。
fn computeIrradiance(
ray: Ray,
hitPosition: vec3<f32>,
normal: vec3<f32>
) -> vec3<f32> {
let lightPosition = vec3<f32>(30.0, 80.0, 10.0);
let lightColor = vec3<f32>(1.0, 1.0, 1.0);
let lightPower = 10000.0;
let lightVector = lightPosition - hitPosition;
let lightDistance = length(lightVector);
let lightDir = lightVector / lightDistance;
let attenuation = lightPower / (lightDistance * lightDistance);
let cosTheta = max(dot(normal, lightDir), 0.0);
let diffuse = lightColor * attenuation * cosTheta;
return diffuse + background.rgb * 0.8;
}
ということで、反射を実装することができた。