/*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ import { vec3, vec4 } from "gl-matrix"; export declare const ComputeShaderWgsl = "\nconst PI = 3.1415926535897932385f;\nconst TWO_PI = 6.2831853071795864769f;\nconst ROOT_THREE_OVER_TWO = 0.86602540378443864676f;\nconst ONE_OVER_LOG10 = 1f / log(10f);\n\nstruct ColorBuffer {\n values: array,\n}\n\n// (normal.x, normal.y, normal.z, depth)\nstruct NormalDepthBuffer {\n values: array,\n}\n\n// Min, max\n// TODO: Convert to atomic add with uint\nstruct DepthMinMaxBuffer {\n values: array>,\n}\n\nstruct Ray {\n origin: vec3,\n direction: vec3,\n time: f32,\n}\n\nstruct HitRecord {\n normal: vec3,\n t: f32,\n frontFace: bool,\n // materialId: u32,\n uv: vec2,\n id: u32,\n previousId: u32,\n position: vec3,\n previousPosition: vec3,\n isAbsorbing: bool,\n previousIsAbsorbing: bool,\n absorption: vec3,\n previousAbsorption: vec3,\n sdfBorder: bool,\n}\n\nstruct Camera {\n origin: vec3,\n lowerLeftCorner: vec3,\n horizontal: vec3,\n vertical: vec3,\n u: vec3,\n v: vec3,\n w: vec3,\n aspectRatio: f32,\n viewportWidth: f32,\n viewportHeight: f32,\n fov: f32,\n aperture: f32,\n focusDistance: f32,\n time0: f32,\n time1: f32,\n}\n\n // offest align size\nstruct Uniforms { // ------------------------------\n position: vec3, // 0 16 12\n width: f32, // 12 4 4\n right: vec3, // 16 16 12\n height: f32, // 28 4 4\n up: vec3, // 32 16 12\n seed: f32, // 44 4 4\n forward: vec3, // 48 16 12\n fov: f32, // 60 4 4\n backgroundColor: vec3, // 64 16 12\n time0: f32, // 76 4 4\n ambientColor: vec3, // 80 16 12\n time1: f32, // 92 4 4\n tilesX : f32, // 96 4 4\n tilesY : f32, // 100 4 4\n tileOffsetX : f32, // 104 4 4\n tileOffsetY : f32, // 108 4 4 \n lookAt: vec3, // 112 16 12\n aperture: f32, // 124 4 4\n focusDistance: f32, // 128 4 4\n raysPerFrame : f32, // 132 4 4\n // padding 136 4 4\n // ------------------------------\n // 16 144\n}\n\n// id type\n// ----------------\n// 0 none\n// 1 solidColor\n// 2 image\n// 3 sdfText\n// 4 checker\n// 5 grid\n // offest align size\nstruct Texture { // ------------------------------\n color0: vec3, // 0 16 12\n typeId: f32, // 12 4 4\n color1: vec3, // 16 12 12\n // padding 28 4 12\n size0: vec4, // 32 16 16\n size1: vec4, // 48 16 16\n clip: vec4, // 64 16 16\n offset: vec2, // 80 8 8\n} // padding 88 4 8\n // ------------------------------\n // 16 96\n\n\n// id type\n// ---------------\n// 0 lambertian\n// 1 metal\n// 2 dielectric\n// 3 diffuse light\n// 4 glossy\n// 5 isotropic\n// 6 varnitsh\n // offest align size\nstruct Material { // ------------------------------\n typeId: f32, // 0 4 4\n fuzz: f32, // 4 4 4\n refractiveIndex: f32, // 8 4 4\n textureId: f32, // 12 4 4\n color: vec3, // 16 16 12\n glossiness: f32, // 28 4 4\n idColor: vec4, // 32 16 16\n density: f32, // 48 4 4\n // padding 52 4 12\n // ------------------------------\n} // 16 64\n\n// id type\n// ----------------\n// 0 distant\n// 1 sphere\n// 2 rect\n// 3 disk\n// 4 cylinder\n// 5 dome\n // offest align size\nstruct Light { // ------------------------------\n rotation: vec4, // 0 16 16\n center: vec3, // 16 16 12\n typeId: f32, // 28 4 4\n size: vec3, // 32 16 12\n // padding 44 4 4\n color: vec3, // 48 16 12\n // padding 60 4 4\n} // ------------------------------\n // 16 64\n\n// id type\n// ----------------\n// 0 sphere\n// 1 box\n// 2 cylinder\n// 3 hexPrism\n// 4 rotatedBox\n// 5 xyRect\n// 6 xzRect\n// 7 yzRect\n// 8 rotatedXyRect\n// 9 fontXyRect\n// 10 rotatedFontXyRect\n// 11 boxSdf\n// 12 cylinderSdf\n// 13 hexPrismSdf\n// 14 constantMedium\n// 15 sdfXyRect\n// 16 rotatedSdfXyRect\n// 17 rotatedBoxSdf\n// 18 xyDisk\n// 19 rotatedXyDisk\n// 20 ringSdf\n// 21 rotatedRingSdf\n// 22 sphereSdf\n// 23 tubeSdf\n // offest align size\nstruct Hittable { // ------------------------------\n center0: vec3, // 0 16 12\n typeId: f32, // 12 4 4\n size0: vec3, // 16 16 12\n materialId: f32, // 28 4 4\n rotation0: vec4, // 32 16 16\n rotation1: vec4, // 48 16 16\n texCoord0: vec2, // 64 8 8\n texCoord1: vec2, // 72 8 8\n center1: vec3, // 80 16 12\n rounding: f32, // 92 4 4\n size1: vec3, // 96 16 12\n boundaryTypeId: f32, // 108 4 4\n time0: f32, // 112 4 4\n time1: f32, // 116 4 4\n texId: f32, // 120 4 4\n sdfBuffer: f32, // 124 4 4\n sdfBorder: f32, // 128 4 4\n parameter1: f32, // 132 4 4\n parameter2: f32, // 136 4 4\n materialFuzz: f32, // 140 4 4\n materialGloss: f32, // 144 4 4\n materialDensity: f32, // 148 4 4\n materialRefractiveIndex: f32, // 152 4 4\n // padding 156 4 4\n materialColor: vec3, // 160 16 12\n materialTypeId: f32, // 172 4 4\n segmentColor: vec4, // 176 16 16\n textureTypeId: f32, // 192 4 4\n textureId: f32, // 196 4 4\n // padding 200 4 8\n} // ------------------------------\n // 16 208\n\n // offest align size\nstruct LinearBVHNode { // ------------------------------\n center: vec3, // 0 16 12\n primitivesOffset: f32, // 12 4 4\n size: vec3, // 16 16 12\n secondChildOffset: f32, // 28 4 4\n nPrimitives: f32, // 32 4 4\n axis: f32, // 36 4 4\n} // padding 40 4 8\n // ------------------------------\n // 16 48\n\nstruct HittableBuffer {\n hittables: array,\n}\n\n// struct MaterialBuffer {\n// materials: array,\n// }\n\nstruct TextureBuffer {\n textures: array,\n}\n\nstruct LightBuffer {\n lights: array,\n}\n\nstruct LinearBVHNodeBuffer {\n nodes: array,\n}\n\n// Schlick's approximation for reflectance\nfn reflectance(cos: f32, refractiveIndex: f32) -> f32 {\n var r = (1f - refractiveIndex) / (1f + refractiveIndex);\n r = r * r;\n return r + (1f - r) * pow(1f - cos, 5f);\n}\n\nfn refraction(uv: vec3, n: vec3, etaiOverEtat: f32) -> vec3 {\n let cosTheta = min(dot(-uv, n), 1f);\n let rOutPerp = etaiOverEtat * (uv + cosTheta * n);\n let rOutParallel = -sqrt(abs(1f - dot(rOutPerp, rOutPerp))) * n;\n return rOutPerp + rOutParallel;\n}\n\nfn getCamera(uniforms: Uniforms) -> Camera {\n var camera: Camera;\n camera.aperture = uniforms.aperture;\n camera.aspectRatio = uniforms.width / uniforms.height;\n camera.fov = uniforms.fov;\n camera.viewportHeight = 2f * tan(camera.fov / 2f);\n camera.viewportWidth = camera.aspectRatio * camera.viewportHeight;\n camera.origin = uniforms.position;\n camera.u = uniforms.right;\n camera.v = uniforms.up;\n camera.w = uniforms.forward;\n let focusDistance = dot(camera.w, camera.origin - uniforms.lookAt) + uniforms.focusDistance;\n camera.horizontal = camera.u * camera.viewportWidth * focusDistance;\n camera.vertical = camera.v * camera.viewportHeight * focusDistance;\n camera.lowerLeftCorner = camera.origin - camera.horizontal / 2f + camera.vertical / 2f - camera.w * focusDistance;\n camera.time0 = uniforms.time0;\n camera.time1 = uniforms.time1;\n return camera;\n}\n\nfn getCameraRay(camera: Camera, seed: ptr, texCoord: vec2) -> Ray {\n // Depth of field\n let rd = camera.aperture * randomInUnitDisk(seed);\n let offset = camera.u * rd.x + camera.v * rd.y;\n\n var ray: Ray;\n ray.origin = camera.origin + offset;\n ray.direction = normalize(camera.lowerLeftCorner + texCoord.x * camera.horizontal - texCoord.y * camera.vertical - camera.origin - offset);\n ray.time = camera.time0 + random(seed) * (camera.time1 - camera.time0);\n return ray;\n}\n\nfn degreesToRadians(degrees: f32) -> f32 {\n return degrees * PI / 180f;\n}\n\n// See https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/\nfn random(seed: ptr) -> f32 {\n var random = ((*seed >> ((*seed >> 28u) + 4u)) ^ *seed) * 277803737u;\n random = (random >> 22u) ^ random;\n *seed = *seed * 747796405u + 2891336453u;\n return f32(random) / 4294967295f; // [0,1]\n}\n\nfn randomInUnitDisk(seed: ptr) -> vec2 {\n var p: vec2;\n loop {\n p = 2f * vec2(random(seed), random(seed)) - vec2(1f, 1f);\n if (dot(p, p) <= 1f) { break; }\n }\n return p;\n}\n\n// fn randomInUnitDisk(seed: ptr) -> vec2 {\n// let t = TWO_PI * random(seed);\n// let r = sqrt(random(seed));\n// return r * vec2(cos(t), sin(t));\n// }\n\nfn randomInUnitSphere(seed: ptr) -> vec3 {\n var p: vec3;\n loop {\n p = 2f * vec3(random(seed), random(seed), random(seed)) - vec3(1f, 1f, 1f);\n if (dot(p, p) <= 1f) { break; }\n }\n return p;\n}\n\nfn randomUnitVector(seed: ptr) -> vec3 {\n return normalize(randomInUnitSphere(seed));\n}\n\n// fn randomUnitVector(seed: ptr) -> vec3 {\n// let theta = TWO_PI * random(seed); // [0,2Pi]\n// let phi = acos(2f * random(seed) - 1f); // [-1,1]\n// return vec3(sin(phi) * cos(theta), sin(phi) * sin(theta), cos(phi));\n// }\n\nfn rayAt(ray: Ray, t: f32) -> vec3 {\n return ray.origin + ray.direction * t;\n}\n\nfn setFaceNormal(ray: Ray, outwardNormal: vec3, hitRecord: ptr) {\n (*hitRecord).frontFace = dot(ray.direction, outwardNormal) < 0f;\n (*hitRecord).normal = select(-outwardNormal, outwardNormal, (*hitRecord).frontFace);\n}\n\n// fn hitWorld(ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n// var hitAnything = false;\n// var closestSoFar = tMax;\n// let invDir = vec3(1f, 1f, 1f) / ray.direction;\n// var tempHitRecord: HitRecord;\n// for (var i: u32 = 0u; i < arrayLength(&hittableBuffer.hittables); i = i + 1u) {\n// if (hit(i, ray, invDir, tMin, closestSoFar, &tempHitRecord, seed)) {\n// hitAnything = true;\n// closestSoFar = tempHitRecord.t;\n// tempHitRecord.materialId = u32(hittableBuffer.hittables[i].materialId);\n// *hitRecord = tempHitRecord;\n// }\n// }\n// return hitAnything;\n// }\n\nfn hitBVH(ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n var hitAnything = false;\n var closestSoFar = tMax;\n let invDir = vec3(1f, 1f, 1f) / ray.direction;\n var tempHitRecord: HitRecord;\n var toVisitOffset = 0u;\n var currentNodeIndex = 0u;\n var nodesToVisit: array;\n loop {\n let node = &linearBVHNodeBuffer.nodes[currentNodeIndex];\n // Check ray against BVH node\n if (intersectBox((*node).center, (*node).size, ray, invDir, tMin, closestSoFar)) {\n let nPrimitives = u32((*node).nPrimitives);\n if (nPrimitives > 0u) {\n let primitiveOffset = u32((*node).primitivesOffset);\n for (var i: u32 = 0u; i < nPrimitives; i = i + 1u) {\n let id = primitiveOffset + i;\n if (hit(id, ray, invDir, tMin, closestSoFar, &tempHitRecord, seed)) {\n hitAnything = true;\n closestSoFar = tempHitRecord.t;\n // tempHitRecord.materialId = u32(hittableBuffer.hittables[id].materialId);\n tempHitRecord.id = id;\n }\n }\n if (toVisitOffset == 0u) { break; }\n toVisitOffset = toVisitOffset - 1u;\n currentNodeIndex = nodesToVisit[toVisitOffset];\n }\n else {\n // Put far BVH node on nodesToVisit stack, advance to near node\n if (ray.direction[u32((*node).axis)] < 0f) {\n nodesToVisit[toVisitOffset] = currentNodeIndex + 1u;\n currentNodeIndex = u32((*node).secondChildOffset);\n } else {\n nodesToVisit[toVisitOffset] = u32((*node).secondChildOffset);\n currentNodeIndex = currentNodeIndex + 1u;\n }\n toVisitOffset = toVisitOffset + 1u;\n }\n }\n else {\n if (toVisitOffset == 0u) { break; }\n toVisitOffset = toVisitOffset - 1u;\n currentNodeIndex = nodesToVisit[toVisitOffset];\n }\n }\n if (hitAnything) {\n tempHitRecord.previousId = (*hitRecord).id;\n tempHitRecord.previousPosition = (*hitRecord).position;\n tempHitRecord.previousIsAbsorbing = (*hitRecord).isAbsorbing;\n tempHitRecord.previousAbsorption = (*hitRecord).absorption;\n *hitRecord = tempHitRecord;\n return true;\n };\n return false;\n}\n\nfn hit(id: u32, ray: Ray, invDir: vec3, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n switch u32(hittableBuffer.hittables[id].typeId) {\n default: {\n return false;\n }\n case 0u: {\n return hitSphere(id, ray, tMin, tMax, hitRecord);\n }\n case 1u: {\n return hitBox(id, ray, invDir, tMin, tMax, hitRecord);\n }\n case 2u: {\n return hitCylinder(id, ray, tMin, tMax, hitRecord);\n }\n case 3u: {\n return hitHexPrism(id, ray, tMin, tMax, hitRecord);\n }\n case 4u: {\n return hitRotatedBox(id, ray, tMin, tMax, hitRecord);\n }\n case 5u: {\n return hitXyRect(id, ray, tMin, tMax, hitRecord);\n }\n case 6u: {\n return hitXzRect(id, ray, tMin, tMax, hitRecord);\n }\n // case 7u: {\n // return hitYzRect(hittable, ray, tMin, tMax, hitRecord);\n // }\n case 8u: {\n return hitRotatedXyRect(id, ray, tMin, tMax, hitRecord);\n }\n case 9u: {\n return hitFontXyRect(id, ray, tMin, tMax, hitRecord);\n }\n case 10u: {\n return hitRotatedFontXyRect(id, ray, tMin, tMax, hitRecord);\n }\n case 11u: {\n return hitBoxSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 12u: {\n return hitCylinderSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 13u: {\n return hitHexPrismSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 14u: {\n return hitConstantMedium(id, ray, invDir, tMin, tMax, hitRecord, seed);\n }\n case 15u: {\n return hitSdfXyRect(id, ray, tMin, tMax, hitRecord, seed);\n }\n case 16u: {\n return hitRotatedSdfXyRect(id, ray, tMin, tMax, hitRecord, seed);\n }\n case 17u: {\n return hitRotatedBoxSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 18u: {\n return hitXyDisk(id, ray, tMin, tMax, hitRecord);\n }\n case 19u: {\n return hitRotatedXyDisk(id, ray, tMin, tMax, hitRecord);\n }\n case 20u: {\n return hitRingSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 21u: {\n return hitRotatedRingSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 22u: {\n return hitSphereSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 23u: {\n return hitTubeSdf(id, ray, tMin, tMax, hitRecord);\n }\n }\n}\n\nfn intersectBox(center: vec3, size: vec3, ray: Ray, invDir: vec3, tMin: f32, tMax: f32) -> bool {\n let oc = ray.origin - center;\n let n = invDir * oc;\n let k = abs(invDir) * size; // Box size is from center to edge\n let t0 = -n - k;\n let t1 = -n + k;\n let tNear = max(max(t0.x, t0.y), t0.z);\n let tFar = min(min(t1.x, t1.y), t1.z);\n if (tNear > tFar) { return false; }\n return tNear < tMax && tFar > 0f; // Must return true when inside box, even if closestSoFar is closer than far box intersection\n}\n\nfn hitConstantMedium(id: u32, ray: Ray, invDir: vec3, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n let constantMedium = &hittableBuffer.hittables[id];\n let boundaryTypeId = u32((*constantMedium).boundaryTypeId);\n var tempHitRecord1: HitRecord;\n if (!hitConstantMediumBoundary(id, boundaryTypeId, ray, invDir, -100f, 100f, &tempHitRecord1)) { return false; }\n var tempHitRecord2: HitRecord;\n // When raymarching narrow grazing angles, adding SHADOW_OFFSET is < epsilon from surface, so hit function returns tN not tF, hence distanceInsideBoundary is incorrect\n // Add larger distance to t\n if (!hitConstantMediumBoundary(id, boundaryTypeId, ray, invDir, tempHitRecord1.t + 0.001f, 100f, &tempHitRecord2)) { return false; }\n if (tempHitRecord1.t < tMin) { tempHitRecord1.t = tMin; }\n if (tempHitRecord2.t > tMax) { tempHitRecord2.t = tMax; }\n if (tempHitRecord1.t >= tempHitRecord2.t) {\n return false;\n }\n tempHitRecord1.t = max(tempHitRecord1.t, 0f);\n let distanceInsideBoundary = tempHitRecord2.t - tempHitRecord1.t;\n // let negativeInverseDensity = -1f / materialBuffer.materials[u32((*constantMedium).materialId)].density;\n let negativeInverseDensity = -1f / constantMedium.materialDensity;\n let hitDistance = negativeInverseDensity * log(random(seed));\n if (hitDistance > distanceInsideBoundary) { return false; }\n let t = tempHitRecord1.t + hitDistance;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n return true;\n}\n\nfn hitConstantMediumBoundary(id: u32, boundaryTypeId: u32, ray: Ray, invDir: vec3, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n switch boundaryTypeId {\n default: {\n return false;\n }\n case 0u: {\n return hitSphere(id, ray, tMin, tMax, hitRecord);\n }\n case 1u: {\n return hitBox(id, ray, invDir, tMin, tMax, hitRecord);\n }\n case 2u: {\n return hitCylinder(id, ray, tMin, tMax, hitRecord);\n }\n case 3u: {\n return hitHexPrism(id, ray, tMin, tMax, hitRecord);\n }\n case 4u: {\n return hitRotatedBox(id, ray, tMin, tMax, hitRecord);\n }\n case 11u: {\n return hitBoxSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 12u: {\n return hitCylinderSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 13u: {\n return hitHexPrismSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 17u: {\n return hitRotatedBoxSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 20u: {\n return hitRingSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 21u: {\n return hitRotatedRingSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 22u: {\n return hitSphereSdf(id, ray, tMin, tMax, hitRecord);\n }\n case 23u: {\n return hitTubeSdf(id, ray, tMin, tMax, hitRecord);\n }\n }\n}\n\nfn hitSphere(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let sphere = &hittableBuffer.hittables[id];\n let radius = (*sphere).size0.x;\n let center = (*sphere).center0;\n let oc = ray.origin - center;\n let b = dot(oc, ray.direction);\n let c = dot(oc, oc) - radius * radius;\n var h = b * b - c;\n if (h < 0f) { return false; }\n h = sqrt(h);\n\n // Find the nearest root in range\n var root = -b - h;\n if (root < tMin || root > tMax) {\n root = -b + h;\n if (root < tMin || root > tMax) { return false; }\n }\n\n // (*hitRecord).t = root;\n // (*hitRecord).position = rayAt(ray, root);\n // let outwardNormal = ((*hitRecord).position - center) / radius;\n // setFaceNormal(ray, outwardNormal, hitRecord);\n\n // Reduce precision error in t by ensuring hit position is on sphere surface\n let outwardNormal = normalize(ray.origin + ray.direction * root - center);\n setFaceNormal(ray, outwardNormal, hitRecord);\n (*hitRecord).position = center + outwardNormal * radius; // Use outward normal with internal reflection\n (*hitRecord).t = root; // I should also re-calculate t, but this would involve another normalization. t is only used to check closest hit, so only important with overlapping geometry\n\n // UV\n let phi = atan2(outwardNormal.x, outwardNormal.z); // [-pi,pi]\n let theta = asin(outwardNormal.y); // [-pi/2, pi/2]\n (*hitRecord).uv = vec2(phi / TWO_PI + 0.5f, theta / PI + 0.5f); // [0,1]\n return true;\n}\n\nfn hitBox(id: u32, ray: Ray, invDir: vec3, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let box = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*box).time0) / ((*box).time1 - (*box).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*box).center0 + time * ((*box).center1 - (*box).center0);\n let size = (*box).size0 + time * ((*box).size1 - (*box).size0);\n let oc = ray.origin - center;\n let n = invDir * oc;\n let k = abs(invDir) * size; // Box size is from center to edge\n let t1 = -n - k;\n let t2 = -n + k;\n let tNear = max(max(t1.x, t1.y), t1.z);\n let tFar = min(min(t2.x, t2.y), t2.z);\n // if (tFar <= tNear) { return false; }\n if (tNear > tFar || tFar < 0f) { return false; }\n\n // Find nearest root in range\n var outwardNormal: vec3;\n var root = tNear;\n if (root < tMin || root > tMax) {\n root = tFar;\n if (root < tMin || root > tMax) { return false; }\n outwardNormal = sign(ray.direction) * step(t2.xyz, t2.yzx) * step(t2.xyz, t2.zxy);\n }\n else {\n outwardNormal = -sign(ray.direction) * step(t1.yzx, t1.xyz) * step(t1.zxy, t1.xyz);\n }\n\n (*hitRecord).t = root;\n (*hitRecord).position = rayAt(ray, root);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn hitRotatedBox(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedBox = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*rotatedBox).time0) / ((*rotatedBox).time1 - (*rotatedBox).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedBox).center0 + time * ((*rotatedBox).center1 - (*rotatedBox).center0);\n let rotation = slerpQuat((*rotatedBox).rotation0, (*rotatedBox).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let rotatedInvDir = vec3(1f, 1f, 1f) / rotatedRay.direction;\n let hit = hitBox(id, rotatedRay, rotatedInvDir, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn hitXyDisk(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let xyDisk = &hittableBuffer.hittables[id];\n let oc = ray.origin - (*xyDisk).center0;\n\n // Distance to plane, t\n let t = -oc.z / ray.direction.z;\n\n // If direction == 0, t = +/- infinity, which always returns false\n if (t < tMin || t > tMax) { return false; }\n\n // Intersection point in model space\n let p = oc + t * ray.direction;\n\n // Bounds\n let radius = (*xyDisk).size0.x;\n if (dot(p, p) > radius * radius) { return false; } // dot(p, p) is squared distance from disk center to intersection point\n\n // Texture coords\n var uv = vec2(0.5 * p.xy / radius + vec2(0.5f, 0.5f));\n let texCoord0 = (*xyDisk).texCoord0;\n let texCoord1 = (*xyDisk).texCoord1;\n uv = texCoord0 + uv * (texCoord1 - texCoord0);\n\n (*hitRecord).uv = uv;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n let outwardNormal = vec3(0f, 0f, 1f);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn hitRotatedXyDisk(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedXyDisk = &hittableBuffer.hittables[id];\n // let center = (*rotatedXyDisk).center0;\n let time = min(max((ray.time - (*rotatedXyDisk).time0) / ((*rotatedXyDisk).time1 - (*rotatedXyDisk).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedXyDisk).center0 + time * ((*rotatedXyDisk).center1 - (*rotatedXyDisk).center0);\n let rotation = slerpQuat((*rotatedXyDisk).rotation0, (*rotatedXyDisk).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitXyDisk(id, rotatedRay, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn hitXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let xyRect = &hittableBuffer.hittables[id];\n let oc = ray.origin - (*xyRect).center0;\n\n // Distance to plane, t\n let t = -oc.z / ray.direction.z;\n\n // If direction == 0, t = +/- infinity, which always returns false\n if (t < tMin || t > tMax) { return false; }\n\n // Intersection point in model space\n let p = oc + t * ray.direction;\n\n // Bounds\n let size = (*xyRect).size0;\n if (abs(p.x) > size.x || abs(p.y) > size.y) { return false; }\n\n // Texture coords\n var uv = vec2(0.5 * p.xy / size.xy + vec2(0.5f, 0.5f));\n let texCoord0 = (*xyRect).texCoord0;\n let texCoord1 = (*xyRect).texCoord1;\n uv = texCoord0 + uv * (texCoord1 - texCoord0);\n\n (*hitRecord).uv = uv;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n let outwardNormal = vec3(0f, 0f, 1f);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn hitRotatedXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedXyRect = &hittableBuffer.hittables[id];\n // let center = (*rotatedXyRect).center0;\n let time = min(max((ray.time - (*rotatedXyRect).time0) / ((*rotatedXyRect).time1 - (*rotatedXyRect).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedXyRect).center0 + time * ((*rotatedXyRect).center1 - (*rotatedXyRect).center0);\n let rotation = slerpQuat((*rotatedXyRect).rotation0, (*rotatedXyRect).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitXyRect(id, rotatedRay, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn hitXzRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let xzRect = &hittableBuffer.hittables[id];\n let oc = ray.origin - (*xzRect).center0;\n\n // Distance to plane, t\n let t = -oc.y / ray.direction.y;\n\n // If direction == 0, t = +/- infinity, which always returns false\n if (t < tMin || t > tMax) { return false; }\n\n // Intersection point in model space\n let p = oc + t * ray.direction;\n\n // Bounds\n let size = (*xzRect).size0;\n if (abs(p.x) > size.x || abs(p.z) > size.z) { return false; }\n\n // Texture coords\n var uv = vec2(0.5 * p.xz / size.xz + vec2(0.5f, 0.5f));\n let texCoord0 = (*xzRect).texCoord0;\n let texCoord1 = (*xzRect).texCoord1;\n uv = texCoord0 + uv * (texCoord1 - texCoord0);\n\n (*hitRecord).uv = uv;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n let outwardNormal = vec3(0f, 1f, 0f);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\n// fn hitYzRect(yzRect: Hittable, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n// let oc = ray.origin - yzRect.center0;\n\n// // Distance to plane, t\n// let t = -oc.x / ray.direction.x;\n\n// // If direction == 0, t = +/- infinity, which always returns false\n// if (t < tMin || t > tMax) { return false; }\n\n// // Intersection point in model space\n// let p = oc + t * ray.direction;\n\n// // Bounds\n// if (abs(p.y) > yzRect.size0.y || abs(p.z) > yzRect.size0.z) { return false; }\n\n// // Texture coords\n// var uv = vec2(0.5 * p.yz / yzRect.size0.yz + vec2(0.5f, 0.5f));\n// uv = yzRect.texCoord0 + uv * (yzRect.texCoord1 - yzRect.texCoord0);\n\n// (*hitRecord).uv = uv;\n// (*hitRecord).t = t;\n// (*hitRecord).position = rayAt(ray, t);\n// let outwardNormal = vec3(1f, 0f, 0f);\n// setFaceNormal(ray, outwardNormal, hitRecord);\n// return true;\n// }\n\n// TODO: Share hit function with XyRect\nfn hitFontXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let xyRect = &hittableBuffer.hittables[id];\n\n // let oc = ray.origin - (*xyRect).center0;\n let time = min(max((ray.time - (*xyRect).time0) / ((*xyRect).time1 - (*xyRect).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*xyRect).center0 + time * ((*xyRect).center1 - (*xyRect).center0);\n let oc = ray.origin - center;\n\n // Distance to plane, t\n let t = -oc.z / ray.direction.z;\n\n // If direction == 0, t = +/- infinity, which always returns false\n if (t < tMin || t > tMax) { return false; }\n\n // Intersection point in model space\n let p = oc + t * ray.direction;\n\n // Bounds\n let size = (*xyRect).size0;\n if (abs(p.x) > size.x || abs(p.y) > size.y) { return false; }\n\n // Texture coords\n var uv = vec2(0.5 * p.xy / size.xy + vec2(0.5f, 0.5f));\n let texCoord0 = (*xyRect).texCoord0;\n let texCoord1 = (*xyRect).texCoord1;\n uv = texCoord0 + uv * (texCoord1 - texCoord0);\n\n // Sample sdf\n let buffer = xyRect.sdfBuffer / 0xff;\n let r = textureSampleLevel(fontTexture, linearSampler, uv, 0f).r;\n if (r < buffer) { return false; }\n\n (*hitRecord).uv = uv;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n let outwardNormal = vec3(0f, 0f, 1f);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn hitRotatedFontXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedXyRect = &hittableBuffer.hittables[id];\n // let center = (*rotatedXyRect).center0;\n let time = min(max((ray.time - (*rotatedXyRect).time0) / ((*rotatedXyRect).time1 - (*rotatedXyRect).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedXyRect).center0 + time * ((*rotatedXyRect).center1 - (*rotatedXyRect).center0);\n let rotation = slerpQuat((*rotatedXyRect).rotation0, (*rotatedXyRect).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitFontXyRect(id, rotatedRay, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\n// TODO: Share hit function with FontXyRect, specifying texture\nfn hitSdfXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n let xyRect = &hittableBuffer.hittables[id];\n\n // let oc = ray.origin - (*xyRect).center0;\n let time = min(max((ray.time - (*xyRect).time0) / ((*xyRect).time1 - (*xyRect).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*xyRect).center0 + time * ((*xyRect).center1 - (*xyRect).center0);\n let oc = ray.origin - center;\n\n // Distance to plane, t\n let t = -oc.z / ray.direction.z;\n\n // If direction == 0, t = +/- infinity, which always returns false\n if (t < tMin || t > tMax) { return false; }\n\n // Intersection point in model space\n let p = oc + t * ray.direction;\n\n // Bounds\n // let size = (*xyRect).size0;\n let size = (*xyRect).size0 + time * ((*xyRect).size1 - (*xyRect).size0);\n if (abs(p.x) > (*xyRect).size0.x || abs(p.y) > (*xyRect).size0.y) { return false; }\n\n // Zero-thickness transparency\n // TODO: Pre-multiplied alpha\n // if (random(seed) > 0.5f) { return false; }\n\n // Texture coords\n var uv = vec2(0.5 * p.xy / size.xy + vec2(0.5f, 0.5f));\n let texCoord0 = (*xyRect).texCoord0;\n let texCoord1 = (*xyRect).texCoord1;\n uv = texCoord0 + uv * (texCoord1 - texCoord0);\n\n // Sample sdf\n let buffer = xyRect.sdfBuffer / 0xff;\n // let r = textureSampleLevel(atlasTexture, linearSampler, uv, 0f).r;\n var r: f32;\n if ((*xyRect).texId == 0f) {\n r = textureSampleLevel(atlasTexture, linearSampler, uv, 0f).r;\n }\n else {\n r = textureSampleLevel(fontTexture, linearSampler, uv, 0f).r;\n }\n if (r < buffer) { return false; }\n\n // sdfBorder\n let border = xyRect.sdfBorder / 0xff;\n (*hitRecord).sdfBorder = r - buffer < border;\n\n (*hitRecord).uv = uv;\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n let outwardNormal = vec3(0f, 0f, 1f);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn hitRotatedSdfXyRect(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr, seed: ptr) -> bool {\n let rotatedXyRect = &hittableBuffer.hittables[id];\n // let center = (*rotatedXyRect).center0;\n let time = min(max((ray.time - (*rotatedXyRect).time0) / ((*rotatedXyRect).time1 - (*rotatedXyRect).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedXyRect).center0 + time * ((*rotatedXyRect).center1 - (*rotatedXyRect).center0);\n let rotation = slerpQuat((*rotatedXyRect).rotation0, (*rotatedXyRect).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitSdfXyRect(id, rotatedRay, tMin, tMax, hitRecord, seed);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn rotateQuat(v: vec3, q: vec4) -> vec3 {\n return v + 2f * cross(q.xyz, cross(q.xyz, v) + q.w * v);\n}\n\nfn slerpQuat(q0: vec4, q1: vec4, t: f32) -> vec4 {\n var cosom = dot(q0, q1);\n var q2 = q1;\n\tif (cosom < 0f) {\n\t\tcosom = -cosom;\n\t\tq2 = -q2;\n\t}\n\tvar s0: f32;\n var s1: f32;\n\tif (1f - cosom > 0.000001f) {\n\t\t// SLERP\n\t\tlet omega = acos(cosom);\n\t\tlet sinom = sin(omega);\n\t\ts0 = sin((1f - t) * omega) / sinom;\n\t\ts1 = sin(t * omega) / sinom;\n\t}\n\telse {\n\t\t// Quaternions close enough for LERP\n\t\ts0 = 1f - t;\n\t\ts1 = t;\n\t}\n\treturn s0 * q0 + s1 * q2;\n}\n\nfn conjugate(q: vec4) -> vec4 {\n return vec4(-q.x, -q.y, -q.z, q.w);\n}\n\nfn hitCylinder(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let cylinder = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*cylinder).time0) / ((*cylinder).time1 - (*cylinder).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*cylinder).center0 + time * ((*cylinder).center1 - (*cylinder).center0);\n let size = (*cylinder).size0 + time * ((*cylinder).size1 - (*cylinder).size0);\n let rotation = slerpQuat((*cylinder).rotation0, (*cylinder).rotation1, time);\n let ra = size.x; // Radius\n let ca = rotateQuat(vec3(0f, 1f, 0f), rotation);\n let oc = ray.origin - center;\n let card = dot(ca, ray.direction);\n let caoc = dot(ca, oc);\n let a = 1f - card * card;\n let b = dot(oc, ray.direction) - caoc * card;\n let c = dot(oc, oc) - caoc * caoc - ra * ra;\n var h = b * b - a * c;\n if (h < 0f) { return false; }\n h = sqrt(h);\n let br0 = (-b - h) / a;\n let br1 = (-b + h) / a;\n\n // Body\n let ch = size.y; // Half-height\n let y0 = caoc + br0 * card;\n let y1 = caoc + br1 * card;\n let bt0 = select(10000000f, br0, abs(y0) < ch);\n let bt1 = select(-10000000f, br1, abs(y1) < ch);\n\n // Caps\n let sy0 = sign(y0);\n let sy1 = sign(y1);\n let cr0 = (sy0 * ch - caoc) / card;\n let cr1 = (sy1 * ch - caoc) / card;\n let ct0 = select(10000000f, cr0, abs(b + a * cr0) < h);\n let ct1 = select(-10000000f, cr1, abs(b + a * cr1) < h);\n\n // Find the nearest root in range\n let tN = min(bt0, ct0);\n let tF = max(bt1, ct1);\n var root = tN;\n if (root < tMin || root > tMax) {\n root = tF;\n if (root < tMin || root > tMax) { return false; }\n }\n\n // Normal\n var outwardNormal: vec3;\n if (root == bt0 || root == bt1) {\n let y = select(y1, y0, root == bt0);\n // outwardNormal = (oc + root * ray.direction - ca * y) / ra;\n\n // Reduce precision error in t by ensuring hit position is on cylinder surface\n outwardNormal = normalize(oc + root * ray.direction - ca * y);\n setFaceNormal(ray, outwardNormal, hitRecord);\n (*hitRecord).position = center + ca * y + outwardNormal * ra; // Use outward normal with internal reflection\n (*hitRecord).t = root;\n }\n else {\n let sy = select(sy1, sy0, root == ct0);\n outwardNormal = ca * sy;\n\n // TODO: Reduce precision error\n setFaceNormal(ray, outwardNormal, hitRecord);\n (*hitRecord).position = rayAt(ray, root);\n (*hitRecord).t = root;\n }\n\n // setFaceNormal(ray, outwardNormal, hitRecord);\n // (*hitRecord).position = rayAt(ray, root);\n // (*hitRecord).t = root;\n return true;\n}\n\nfn hitHexPrism(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let hexPrism = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*hexPrism).time0) / ((*hexPrism).time1 - (*hexPrism).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*hexPrism).center0 + time * ((*hexPrism).center1 - (*hexPrism).center0);\n let oc = ray.origin - center;\n let size = (*hexPrism).size0;\n let ra = size.x; // Distance from center to edge\n let he = size.y; // Half-height\n let rd = ray.direction;\n\n // Normals\n let n1 = vec3(1f, 0f, 0f);\n let n2 = vec3(0.5f, 0f, ROOT_THREE_OVER_TWO);\n let n3 = vec3(-0.5f, 0f, ROOT_THREE_OVER_TWO);\n let n4 = vec3(0f, 1f, 0f);\n\n // Slabs intersections\n var t1 = vec3((vec2(ra, -ra) - dot(oc, n1)) / dot(rd, n1), 1f);\n var t2 = vec3((vec2(ra, -ra) - dot(oc, n2)) / dot(rd, n2), 1f);\n var t3 = vec3((vec2(ra, -ra) - dot(oc, n3)) / dot(rd, n3), 1f);\n var t4 = vec3((vec2(he, -he) - dot(oc, n4)) / dot(rd, n4), 1f);\n\n // Inetsection selection\n if (t1.y < t1.x) { t1 = vec3(t1.yx, -1f); }\n if (t2.y < t2.x) { t2 = vec3(t2.yx, -1f); }\n if (t3.y < t3.x) { t3 = vec3(t3.yx, -1f); }\n if (t4.y < t4.x) { t4 = vec3(t4.yx, -1f); }\n\n var tN = vec4(t1.x, t1.z * n1);\n if (t2.x > tN.x) { tN = vec4(t2.x, t2.z * n2); }\n if (t3.x > tN.x) { tN = vec4(t3.x, t3.z * n3); }\n if (t4.x > tN.x) { tN = vec4(t4.x, t4.z * n4); }\n\n let tF = min(min(t1.y,t2.y),min(t3.y,t4.y));\n\n if (tN.x > tF || tF < 0f) { return false; }\n\n // Find the nearest root in range\n var outwardNormal: vec3;\n var root = tN.x;\n if (root < tMin || root > tMax) {\n root = tF;\n if (root < tMin || root > tMax) { return false; }\n\n // Normal\n if (root == t1.y) { outwardNormal = -t1.z * n1; }\n else if (root == t2.y) { outwardNormal = -t2.z * n2; }\n else if (root == t3.y) { outwardNormal = -t3.z * n3; }\n else if (root == t4.y) { outwardNormal = -t4.z * n4; }\n }\n else {\n outwardNormal = tN.yzw;\n }\n\n (*hitRecord).t = root;\n (*hitRecord).position = rayAt(ray, root);\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n}\n\nfn mapBoxSdf(p: vec3, b: vec3, r: f32) -> f32 {\n let q = abs(p) - b;\n return length(max(q, vec3(0f, 0f, 0f))) + min(max(q.x, max(q.y, q.z)), 0f) - r;\n}\n\n// Box frame\n// fn mapBoxSdf(p: vec3, b: vec3, r: f32) -> f32 {\n// let s = abs(p) - b;\n// // let e = b.y / 3f;\n// let e = 0.0002f;\n// let q = abs(s + e) - e;\n// return min(min(\n// length(max(vec3(s.x, q.y, q.z), vec3(0f, 0f, 0f))) + min(max(s.x, max(q.y,q.z)), 0f),\n// length(max(vec3(q.x, s.y, q.z), vec3(0f, 0f, 0f))) + min(max(q.x, max(s.y,q.z)), 0f)),\n// length(max(vec3(q.x, q.y, s.z), vec3(0f, 0f, 0f))) + min(max(q.x, max(q.y,s.z)), 0f));\n// }\n\nfn hitBoxSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let boxSdf = &hittableBuffer.hittables[id];\n var t = tMin;\n let r = (*boxSdf).rounding;\n // let size = (*boxSdf).size0 - r;\n let time = min(max((ray.time - (*boxSdf).time0) / ((*boxSdf).time1 - (*boxSdf).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*boxSdf).center0 + time * ((*boxSdf).center1 - (*boxSdf).center0);\n // TODO: r0, r1\n let size = (*boxSdf).size0 + time * ((*boxSdf).size1 - (*boxSdf).size0) - r;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapBoxSdf(oc, size, r));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.000001f; // replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapBoxSdf(oc + k.xyy * h, size, r) +\n k.yyx * mapBoxSdf(oc + k.yyx * h, size, r) +\n k.yxy * mapBoxSdf(oc + k.yxy * h, size, r) +\n k.xxx * mapBoxSdf(oc + k.xxx * h, size, r));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn mapSphereSdf(p: vec3, r: f32) -> f32 {\n return length(p) - r;\n}\n\nfn hitSphereSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let sphereSdf = &hittableBuffer.hittables[id];\n var t = tMin;\n let center = (*sphereSdf).center0;\n let r = (*sphereSdf).size0.x;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapSphereSdf(oc, r));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.000001f; // replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapSphereSdf(oc + k.xyy * h, r) +\n k.yyx * mapSphereSdf(oc + k.yyx * h, r) +\n k.yxy * mapSphereSdf(oc + k.yxy * h, r) +\n k.xxx * mapSphereSdf(oc + k.xxx * h, r));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn hitRotatedBoxSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedBoxSdf = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*rotatedBoxSdf).time0) / ((*rotatedBoxSdf).time1 - (*rotatedBoxSdf).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedBoxSdf).center0 + time * ((*rotatedBoxSdf).center1 - (*rotatedBoxSdf).center0);\n let rotation = slerpQuat((*rotatedBoxSdf).rotation0, (*rotatedBoxSdf).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitBoxSdf(id, rotatedRay, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn mapCylinderSdf(p: vec3, a: vec3, b: vec3, r0: f32, r1: f32) -> f32 {\n let ba: vec3 = b - a;\n let pa: vec3 = p - a;\n let baba: f32 = dot(ba, ba);\n let paba: f32 = dot(pa, ba);\n let x: f32 = length(pa * baba - ba * paba) - r0 * baba;\n let y: f32 = abs(paba - baba * 0.5f) - baba * 0.5f;\n let x2: f32 = x * x;\n let y2: f32 = y * y * baba;\n // let d: f32 = (max(x, y) < 0f) ? -min(x2, y2) : (((x > 0f) ? x2 : 0f) + ((y > 0f) ? y2 : 0f));\n let d: f32 = select(select(0f, x2, x > 0f) + select(0f, y2, y > 0f), -min(x2, y2), max(x, y) < 0f);\n return sign(d) * sqrt(abs(d)) / baba - r1;\n}\n\nfn hitCylinderSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let cylinderSdf = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*cylinderSdf).time0) / ((*cylinderSdf).time1 - (*cylinderSdf).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*cylinderSdf).center0 + time * ((*cylinderSdf).center1 - (*cylinderSdf).center0);\n let size = (*cylinderSdf).size0 + time * ((*cylinderSdf).size1 - (*cylinderSdf).size0);\n let rotation = slerpQuat((*cylinderSdf).rotation0, (*cylinderSdf).rotation1, time);\n var t = tMin;\n let r1 = (*cylinderSdf).rounding;\n let r0 = size.x - r1;\n let h0 = size.y - r1;\n let ca = rotateQuat(vec3(0f, 1f, 0f), rotation);\n let pa = ca * h0;\n let pb = -pa;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapCylinderSdf(oc, pa, pb, r0, r1));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.000001f; // replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapCylinderSdf(oc + k.xyy * h, pa, pb, r0, r1) +\n k.yyx * mapCylinderSdf(oc + k.yyx * h, pa, pb, r0, r1) +\n k.yxy * mapCylinderSdf(oc + k.yxy * h, pa, pb, r0, r1) +\n k.xxx * mapCylinderSdf(oc + k.xxx * h, pa, pb, r0, r1));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn mapHexPrismSdf(p: vec3, hx: f32, hy: f32, r: f32) -> f32 {\n let k = vec3(-0.8660254, 0.5, 0.57735); // (-sqrt(3)/2 or sin(60), 0.5, sqrt(3)/3 or tan(30))\n var p0 = abs(p.zxy);\n let p1 = p0.xy - 2f * min(dot(k.xy, p0.xy), 0f) * k.xy;\n let d = vec2(length(p1.xy - vec2(clamp(p1.x, -k.z * hx, k.z * hx), hx)) * sign(p1.y - hx), p0.z - hy);\n return min(max(d.x, d.y), 0f) + length(max(d, vec2(0f, 0f))) - r;\n}\n\nfn hitHexPrismSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let hexPrismSdf = &hittableBuffer.hittables[id];\n var t = tMin;\n let r = (*hexPrismSdf).rounding;\n let size = (*hexPrismSdf).size0;\n let time = min(max((ray.time - (*hexPrismSdf).time0) / ((*hexPrismSdf).time1 - (*hexPrismSdf).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*hexPrismSdf).center0 + time * ((*hexPrismSdf).center1 - (*hexPrismSdf).center0);\n let hx = size.x - r;\n let hy = size.y - r;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapHexPrismSdf(oc, hx, hy, r));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.000001f; // Replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapHexPrismSdf(oc + k.xyy * h, hx, hy, r) +\n k.yyx * mapHexPrismSdf(oc + k.yyx * h, hx, hy, r) +\n k.yxy * mapHexPrismSdf(oc + k.yxy * h, hx, hy, r) +\n k.xxx * mapHexPrismSdf(oc + k.xxx * h, hx, hy, r));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn mapRingSdf(p: vec3, n: vec2, r: f32, th: f32, h: f32, rounding: f32) -> f32 {\n let px = abs(p.x);\n // expand result of mat2x2(n.x,n.y,-n.y,n.x)*p;\n // let p2 = vec2(n.x * px + n.y * p.y, -n.y * px + n.x * p.y);\n // Column-major instead of row-major\n let p2 = vec2(n.x * px - n.y * p.y, n.y * px + n.x * p.y);\n let d = max(abs(length(p2) - r) - th * 0.5f, length(vec2(p2.x, max(0f, abs(r - p2.y) - th * 0.5f))) * sign(p2.x));\n\n // Extrude\n let w = vec2(d, abs(p.z) - h);\n \treturn min(max(w.x, w.y), 0f) + length(max(w, vec2(0f, 0f))) - rounding;\n}\n\nfn hitRingSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let ringSdf = &hittableBuffer.hittables[id];\n var t = tMin;\n let size = (*ringSdf).size0;\n let center = (*ringSdf).center0;\n let rounding = (*ringSdf).rounding;\n let outerr = size.x;\n let innerr = size.y;\n // Reduce angle such that inner radius arc reduces by rounding\n var angle = (*ringSdf).parameter1;\n angle -= angle * rounding / innerr * PI * 2;\n let cs = vec2(cos(angle), sin(angle));\n let r = (outerr + innerr) * 0.5f;\n let th = (outerr - innerr) - rounding;\n let e = size.z - rounding;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapRingSdf(oc, cs, r, th, e, rounding));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.00001f; // replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapRingSdf(oc + k.xyy * h, cs, r, th, e, rounding) +\n k.yyx * mapRingSdf(oc + k.yyx * h, cs, r, th, e, rounding) +\n k.yxy * mapRingSdf(oc + k.yxy * h, cs, r, th, e, rounding) +\n k.xxx * mapRingSdf(oc + k.xxx * h, cs, r, th, e, rounding));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn hitRotatedRingSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let rotatedRingSdf = &hittableBuffer.hittables[id];\n let time = min(max((ray.time - (*rotatedRingSdf).time0) / ((*rotatedRingSdf).time1 - (*rotatedRingSdf).time0), 0f), 1f); // Normalize time to [0,1]\n let center = (*rotatedRingSdf).center0 + time * ((*rotatedRingSdf).center1 - (*rotatedRingSdf).center0);\n let rotation = slerpQuat((*rotatedRingSdf).rotation0, (*rotatedRingSdf).rotation1, time);\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n rotatedRay.time = ray.time;\n let hit = hitRingSdf(id, rotatedRay, tMin, tMax, hitRecord);\n if (hit) {\n (*hitRecord).position = rotateQuat((*hitRecord).position - center, rotation) + center;\n (*hitRecord).normal = rotateQuat((*hitRecord).normal, rotation);\n return true;\n }\n return false;\n}\n\nfn mapTubeSdf(p: vec3, r: f32, th: f32, h: f32, rounding: f32) -> f32 {\n // Circle\n // return length(p) - r;\n // Annular\n // abs(sdShape(p)) - r\n // Annular circle\n let d = abs(length(p.xz) - r) - th / 2f;\n \n // Extrude\n let w = vec2(d, abs(p.y) - h);\n \treturn min(max(w.x, w.y), 0f) + length(max(w, vec2(0f, 0f))) - rounding;\n}\n\nfn hitTubeSdf(id: u32, ray: Ray, tMin: f32, tMax: f32, hitRecord: ptr) -> bool {\n let tubeSdf = &hittableBuffer.hittables[id];\n var t = tMin;\n let size = (*tubeSdf).size0;\n let center = (*tubeSdf).center0;\n let rounding = (*tubeSdf).rounding;\n let outerr = size.z;\n let innerr = size.x;\n let e = size.y - rounding;\n let r = (outerr + innerr) * 0.5f;\n let th = (outerr - innerr) - rounding;\n for (var i: u32 = 0u; i < 256u; i = i + 1u) {\n let position = rayAt(ray, t);\n let oc = position - center;\n let distance = abs(mapTubeSdf(oc, r, th, e, rounding));\n t = t + distance;\n if (t > tMax) { return false; }\n if (distance < 0.000001f) {\n (*hitRecord).t = t;\n (*hitRecord).position = rayAt(ray, t);\n\n // Normal\n let h = 0.00001f; // replace by an appropriate value\n let k = vec2(1f, -1f);\n let outwardNormal = normalize(\n k.xyy * mapTubeSdf(oc + k.xyy * h, r, th, e, rounding) +\n k.yyx * mapTubeSdf(oc + k.yyx * h, r, th, e, rounding) +\n k.yxy * mapTubeSdf(oc + k.yxy * h, r, th, e, rounding) +\n k.xxx * mapTubeSdf(oc + k.xxx * h, r, th, e, rounding));\n setFaceNormal(ray, outwardNormal, hitRecord);\n return true;\n }\n }\n return false;\n}\n\nfn hitLights(ray: Ray) -> vec3 {\n var hit: bool;\n for (var i: u32 = 0u; i < arrayLength(&lightBuffer.lights); i = i + 1u) {\n // let light = lightBuffer.lights[i];\n // TODO: Directional lights\n switch u32(lightBuffer.lights[i].typeId) {\n default: {\n hit = hitSphereLight(i, ray);\n }\n case 2u: {\n hit = hitRectLight(i, ray);\n }\n }\n if (hit) {\n return lightBuffer.lights[i].color;\n }\n }\n\n // Background color\n // return vec3(0f, 0f, 0f);\n // return vec3(1f, 1f, 1f);\n // return uniforms.backgroundColor;\n\n // Ambient light (not background color)\n return uniforms.ambientColor;\n\n // TODO: Dome light\n // let t = 0.5f * (ray.direction.y + 1f);\n // let background = (1f - t) * vec3(1f, 1f, 1f) + t * vec3(0.5f, 0.7f, 1.0f);\n // return background;\n}\n\nfn hitSphereLight(id: u32, ray: Ray) -> bool {\n let sphere = &lightBuffer.lights[id];\n let radius = (*sphere).size.x;\n let oc = ray.origin - (*sphere).center;\n let b = dot(oc, ray.direction);\n let c = dot(oc, oc) - radius * radius;\n var h = b * b - c;\n if (h < 0f) { return false; }\n return b < 0f; // Ensure ray towards light\n}\n\nfn hitRectLight(id: u32, ray: Ray) -> bool {\n let rotatedXyRect = &lightBuffer.lights[id];\n let center = (*rotatedXyRect).center;\n let rotation = (*rotatedXyRect).rotation;\n let invRotation = conjugate(rotation);\n var rotatedRay: Ray;\n rotatedRay.origin = rotateQuat(ray.origin - center, invRotation) + center;\n rotatedRay.direction = rotateQuat(ray.direction, invRotation);\n if (dot(rotatedRay.direction, vec3(0f, 0f, 1f)) < 0f) { return false; } // Directional light\n let oc = rotatedRay.origin - center;\n let t = -oc.z / rotatedRay.direction.z;\n if (t < 0f) { return false; }\n let p = oc + t * rotatedRay.direction;\n if (abs(p.x) > (*rotatedXyRect).size.x || abs(p.y) > (*rotatedXyRect).size.y) { return false; }\n return true;\n}\n\nfn nearZero(v: vec3) -> bool {\n return max(max(abs(v.x), abs(v.y)), abs(v.z)) < 0.00000001f; // 1e-8\n}\n\nfn scatterLambertian(ray: ptr, hitRecord: HitRecord, attenuation: ptr>, seed: ptr) -> bool {\n let scatterDirection = hitRecord.normal + randomUnitVector(seed);\n\n // Catch degenerate scatter direction\n (*ray).direction = select(normalize(scatterDirection), hitRecord.normal, nearZero(scatterDirection));\n\n (*ray).origin = hitRecord.position;\n (*attenuation) = textureValue(hitRecord);\n return true;\n}\n\nfn scatterMetal(ray: ptr, hitRecord: HitRecord, attenuation: ptr>, seed: ptr) -> bool {\n // let fuzz = materialBuffer.materials[hitRecord.materialId].fuzz;\n let fuzz = hittableBuffer.hittables[hitRecord.id].materialFuzz;\n (*ray).direction = normalize(reflect((*ray).direction, hitRecord.normal) + fuzz * randomInUnitSphere(seed));\n\n (*ray).origin = hitRecord.position;\n (*attenuation) = textureValue(hitRecord);\n\n // Absorb any rays which fuzz scatters below the surface\n return dot((*ray).direction, hitRecord.normal) > 0f;\n}\n\nfn scatterGlossy(ray: ptr, hitRecord: HitRecord, attenuation: ptr>, seed: ptr) -> bool {\n // Specular\n // let material = &materialBuffer.materials[hitRecord.materialId];\n // let fuzz = (*material).fuzz;\n // let refractiveIndex = (*material).refractiveIndex;\n // let glossiness = (*material).glossiness;\n let hittable = hittableBuffer.hittables[hitRecord.id];\n let fuzz = hittable.materialFuzz;\n let refractiveIndex = hittable.materialRefractiveIndex;\n let glossiness = hittable.materialGloss;\n let refractionRatio = select(refractiveIndex, 1f / refractiveIndex, hitRecord.frontFace);\n let cosTheta = min(dot(-(*ray).direction, hitRecord.normal), 1f);\n if (reflectance(cosTheta, refractionRatio) * glossiness > random(seed)) {\n (*ray).direction = normalize(reflect((*ray).direction, hitRecord.normal) + fuzz * randomInUnitSphere(seed));\n (*ray).origin = hitRecord.position;\n (*attenuation) = vec3(1f, 1f, 1f);\n\n // Absorb any rays which fuzz scatters below the surface\n return dot((*ray).direction, hitRecord.normal) > 0f;\n }\n else {\n // Lambertian\n return scatterLambertian(ray, hitRecord, attenuation, seed);\n }\n}\n\nfn scatterDielectric(ray: ptr, hitRecord: ptr, attenuation: ptr>, seed: ptr) -> bool {\n // let material = &materialBuffer.materials[(*hitRecord).materialId];\n // let refractiveIndex = (*material).refractiveIndex;\n let hittable = hittableBuffer.hittables[hitRecord.id];\n let refractiveIndex = hittable.materialRefractiveIndex;\n // TODO: If still inside another material, use its refractive index\n let refractionRatio = select(refractiveIndex, 1f / refractiveIndex, (*hitRecord).frontFace);\n let cosTheta = min(dot(-(*ray).direction, (*hitRecord).normal), 1f);\n let sinTheta = sqrt(1f - cosTheta * cosTheta);\n let cannotRefract = refractionRatio * sinTheta > 1f;\n // if (cannotRefract || reflectance(cosTheta, refractionRatio) > random(seed)) {\n // if (cannotRefract || reflectance(cosTheta, refractionRatio) * (*material).glossiness > random(seed)) {\n if (cannotRefract || reflectance(cosTheta, refractionRatio) * hittable.materialGloss > random(seed)) {\n (*ray).direction = reflect((*ray).direction, (*hitRecord).normal);\n }\n else {\n (*ray).direction = refraction((*ray).direction, (*hitRecord).normal, refractionRatio);\n }\n (*ray).origin = (*hitRecord).position;\n // (*ray).direction = normalize((*ray).direction + (*material).fuzz * randomInUnitSphere(seed));\n (*ray).direction = normalize((*ray).direction + hittable.materialFuzz * randomInUnitSphere(seed));\n\n // Did the ray enter/stay inside?\n (*attenuation) = vec3(1f, 1f, 1f);\n // if (dot((*ray).direction, (*hitRecord).normal) < 0f) {\n if (dot((*ray).direction, select(-(*hitRecord).normal, (*hitRecord).normal, (*hitRecord).frontFace)) < 0f) {\n (*hitRecord).isAbsorbing = true;\n // (*hitRecord).absorption = (*material).color;\n // (*hitRecord).absorption = hittable.materialColor;\n (*hitRecord).absorption = hittable.materialColor * hittable.materialDensity;\n\n // If already inside another absorbing dielectric, add to absorption\n if ((*hitRecord).previousIsAbsorbing && (*hitRecord).id != (*hitRecord).previousId) {\n (*hitRecord).absorption = (*hitRecord).absorption + (*hitRecord).previousAbsorption;\n }\n }\n return true;\n}\n\nfn scatterIsotropic(ray: ptr, hitRecord: HitRecord, attenuation: ptr>, seed: ptr) -> bool {\n (*ray).direction = randomUnitVector(seed);\n (*ray).origin = hitRecord.position;\n let hittable = hittableBuffer.hittables[hitRecord.id];\n (*attenuation) = hittable.materialColor;\n return true;\n}\n\nfn scatterVarnish(ray: ptr, hitRecord: HitRecord, attenuation: ptr>, seed: ptr) -> bool {\n // Front-face only (no internal reflection or refraction)\n // let material = &materialBuffer.materials[hitRecord.materialId];\n let hittable = hittableBuffer.hittables[hitRecord.id];\n // if (hitRecord.frontFace && (*material).glossiness > random(seed)) {\n if (hitRecord.frontFace && hittable.materialGloss > random(seed)) {\n // let refractiveIndex = (*material).refractiveIndex;\n let refractiveIndex = hittable.materialRefractiveIndex;\n let refractionRatio = select(refractiveIndex, 1f / refractiveIndex, hitRecord.frontFace);\n let cosTheta = min(dot(-(*ray).direction, hitRecord.normal), 1f);\n let sinTheta = sqrt(1f - cosTheta * cosTheta);\n let cannotRefract = refractionRatio * sinTheta > 1f;\n if (cannotRefract || reflectance(cosTheta, refractionRatio) > random(seed)) {\n (*ray).direction = reflect((*ray).direction, hitRecord.normal);\n }\n else {\n // Refraction improves definition at edges and deepens color on faces\n (*ray).direction = refraction((*ray).direction, hitRecord.normal, refractionRatio);\n }\n // (*ray).direction = normalize((*ray).direction + (*material).fuzz * randomInUnitSphere(seed));\n (*ray).direction = normalize((*ray).direction + hittable.materialFuzz * randomInUnitSphere(seed));\n }\n // Pass-through\n (*ray).origin = hitRecord.position;\n (*attenuation) = vec3(1f, 1f, 1f);\n return true;\n}\n\nfn textureValue(hitRecord: HitRecord) -> vec3 {\n // let textureId = materialBuffer.materials[hitRecord.materialId].textureId;\n let hittable = hittableBuffer.hittables[hitRecord.id];\n // let texture = &textureBuffer.textures[u32(textureId)];\n let texture = &textureBuffer.textures[u32(hittable.textureId)];\n switch u32((*texture).typeId) {\n // No texture\n default: {\n return vec3();\n }\n // Solid color\n case 1u: {\n if (hitRecord.sdfBorder) {\n return (*texture).color1;\n }\n else {\n // return (*texture).color0;\n return hittableBuffer.hittables[hitRecord.id].materialColor;\n }\n\n // Debug uv\n // return vec3(hitRecord.uv, 0f);\n }\n // Image\n case 2u: {\n // Sample in linear space\n return textureSampleLevel(backgroundTexture, linearSampler, hitRecord.uv, 0f).rgb;\n // return vec3(hitRecord.uv.x, hitRecord.uv.y, 0f);\n }\n // Checker\n case 4u: {\n let q = trunc((hitRecord.uv + (*texture).offset) / (*texture).size0.xy);\n return select((*texture).color0, (*texture).color1, (q.x + q.y) % 2f > 0f);\n }\n // Grid\n case 5u: {\n let size0 = (*texture).size0;\n let size1 = (*texture).size1;\n let clip = (*texture).clip;\n if (hitRecord.uv.x < clip.x || hitRecord.uv.y < clip.y || hitRecord.uv.x > clip.z || hitRecord.uv.y > clip.w) {\n return (*texture).color1;\n }\n let uv = hitRecord.uv + (*texture).offset;\n var d = uv / size0.xy;\n d = abs(d - round(d)) * size0.xy;\n if (d.x < size1.x || d.y < size1.y) {\n return (*texture).color0;\n }\n else {\n d = uv / size0.zw;\n d = abs(d - round(d)) * size0.zw;\n if (d.x < size1.z || d.y < size1.w) {\n return (*texture).color0;\n }\n return (*texture).color1;\n }\n }\n }\n}\n\nfn rayColor(ray: ptr, seed: ptr) -> vec3 {\n let maxDepth = 16u; // TODO: Pass as uniform\n var depth = 0u;\n // var result: Color;\n var color = vec3(1f, 1f, 1f);\n var attenuation = vec3(1f, 1f, 1f);\n var emitted = vec3(0f, 0f, 0f);\n var hitRecord: HitRecord;\n hitRecord.id = 4294967295; // -1 as u32\n var scatter: bool;\n loop {\n // if (hitWorld(*ray, 0.00001f, 100f, &hitRecord, seed)) {\n if (hitBVH(*ray, 0.00001f, 100f, &hitRecord, seed)) {\n // Debug normal, depth\n // First hit\n // if (depth == 0u) {\n // result.normal = hitRecord.normal * 0.5f + vec3(0.5f, 0.5f, 0.5f);\n // // result.normal = hitRecord.normal * 0.5f + vec3(0.5f, 0.5f, 0.5f)\n // // result.depth = 1f / hitRecord.t;\n // result.depth = -1f / dot(hitRecord.position - (*ray).origin, uniforms.forward);\n // }\n // return result;\n\n // Depth\n depth = depth + 1u;\n if (depth == maxDepth) {\n // Exceeded bounce limit, no more light is gathered\n // result.color = vec3(0f, 0f, 0f);\n // return result;\n return vec3(0f, 0f, 0f);\n }\n\n // Bounce\n // If last hit was travelling INTO a dielectric, use last hit position to calculate distance\n // and apply Beer's law to attenuate the light. Modify the dielectric scattering function to\n // recrord travelling INTO a dielectric based on hitRecord normal and ray direction.\n // Reset this flag each time here.\n if (hitRecord.previousIsAbsorbing) {\n // Beer's law\n let d = distance(hitRecord.previousPosition, hitRecord.position);\n color = color * exp(-d * hitRecord.previousAbsorption);\n }\n // Reset absorption\n hitRecord.isAbsorbing = false;\n hitRecord.absorption = vec3(0f, 0f, 0f);\n\n // switch u32(materialBuffer.materials[hitRecord.materialId].typeId) {\n switch u32(hittableBuffer.hittables[hitRecord.id].materialTypeId) {\n case 0u: {\n scatter = scatterLambertian(ray, hitRecord, &attenuation, seed);\n break;\n }\n case 1u: {\n scatter = scatterMetal(ray, hitRecord, &attenuation, seed);\n break;\n }\n case 2u: {\n scatter = scatterDielectric(ray, &hitRecord, &attenuation, seed);\n break;\n }\n case 3u: {\n scatter = scatterGlossy(ray, hitRecord, &attenuation, seed);\n break;\n }\n case 4u: {\n // Diffuse light\n scatter = false;\n // emitted = materialBuffer.materials[hitRecord.materialId].color;\n emitted = hittableBuffer.hittables[hitRecord.id].materialColor;\n break;\n }\n case 5u: {\n scatter = scatterIsotropic(ray, hitRecord, &attenuation, seed);\n break;\n }\n case 6u: {\n scatter = scatterVarnish(ray, hitRecord, &attenuation, seed);\n break;\n }\n default: {\n scatter = false;\n }\n }\n\n if (scatter) {\n // Attenuate\n color = color * attenuation;\n }\n else {\n // Emit\n // result.color = color * emitted;\n // return result;\n return color * emitted;\n }\n }\n else {\n // return color;\n\n // No hits\n if (depth > 0u) { // Hide lights, background\n return hitLights(*ray) * color;\n // result.color = hitLights(*ray) * color;\n // return result;\n }\n else {\n // return vec3(0f, 0f, 0f);\n return uniforms.backgroundColor;\n // result.color = uniforms.backgroundColor;\n // return result;\n }\n\n // Background\n // let t = 0.5f * ((*ray).direction.y + 1f);\n // let background = (1f - t) * vec3(1f, 1f, 1f) + t * vec3(0.5f, 0.7f, 1.0f);\n // return color * background;\n }\n }\n}\n\n// TODO: Try writing color directly using var outputTexture : texture_storage_2d;\n// textureStore(outputTexture, uv, vec3(1f, 1f, 1f));\n@group(0) @binding(0) var outputColorBuffer: ColorBuffer;\n@group(0) @binding(1) var uniforms: Uniforms;\n@group(0) @binding(2) var hittableBuffer: HittableBuffer;\n// @group(0) @binding(3) var materialBuffer: MaterialBuffer;\n@group(0) @binding(3) var textureBuffer: TextureBuffer;\n@group(0) @binding(4) var lightBuffer: LightBuffer;\n@group(0) @binding(5) var linearBVHNodeBuffer: LinearBVHNodeBuffer;\n@group(0) @binding(6) var linearSampler: sampler;\n@group(0) @binding(7) var fontTexture: texture_2d;\n@group(0) @binding(8) var backgroundTexture: texture_2d;\n@group(0) @binding(9) var atlasTexture: texture_2d;\n@group(0) @binding(10) var outputNormalDepthBuffer: NormalDepthBuffer;\n@group(0) @binding(11) var depthMinMaxBuffer: DepthMinMaxBuffer;\n\n// TODO: Move lighting to seperate bind group so I can update it independently\n\n@compute @workgroup_size(256, 1, 1)\nfn clear(@builtin(global_invocation_id) globalId : vec3) {\n var index = globalId.x * 3u;\n outputColorBuffer.values[index] = 0f;\n outputColorBuffer.values[index + 1u] = 0f;\n outputColorBuffer.values[index + 2u] = 0f;\n index = globalId.x * 4u;\n outputNormalDepthBuffer.values[index] = 0f; // Normal x\n outputNormalDepthBuffer.values[index + 1u] = 0f; // Normal y\n outputNormalDepthBuffer.values[index + 2u] = 0f; // Normal z\n outputNormalDepthBuffer.values[index + 3u] = 0f; // Depth\n atomicStore(&depthMinMaxBuffer.values[0], 4294967295u);\n atomicStore(&depthMinMaxBuffer.values[1], 0u);\n}\n\n@compute @workgroup_size(256, 1, 1)\nfn depthNormal(@builtin(global_invocation_id) globalId : vec3) {\n let imageSize = vec2(uniforms.width * uniforms.tilesX, uniforms.height * uniforms.tilesY);\n let tileSize = vec2(uniforms.width, uniforms.height);\n\n // Tex coords [0,1]\n // let id = f32(globalId.x);\n // let v = floor(id / imageSize.x);\n // let u = (id - v * imageSize.x);\n // let uv = vec2(u, v);\n // let texCoord = uv / imageSize;\n\n // Pixel coords ([0,width-1], [0,height-1])\n let id = f32(globalId.x);\n let tilePixelY = floor(id / tileSize.x);\n let tilePixelX = id - tilePixelY * tileSize.x;\n let imagePixelX = tilePixelX + uniforms.tileOffsetX * tileSize.x;\n let imagePixelY = tilePixelY + uniforms.tileOffsetY * tileSize.y;\n\n // Tex coords ([0,1], [0,1])\n let texCoord = vec2(imagePixelX / imageSize.x, imagePixelY / imageSize.y);\n\n // Camera\n var camera = getCamera(uniforms);\n\n // Sample position (sub-pixel sampling has same seed, but only sampled once per frame)\n let samplePos = vec2(texCoord);\n\n // Ray\n var seed = 0u; // No depth of field for depth, normal\n var ray = getCameraRay(camera, &seed, samplePos);\n\n // Result\n var normal = vec3(0f, 0f, 0f);\n var depth = 0f;\n var hitRecord: HitRecord;\n if (hitBVH(ray, 0.00001f, 100f, &hitRecord, &seed)) {\n normal = hitRecord.normal * 0.5f + vec3(0.5f, 0.5f, 0.5f);\n depth = -1f / dot(hitRecord.position - ray.origin, uniforms.forward);\n }\n\n let index = globalId.x * 4u;\n // let index = u32(tilePixelY * tileSize.x + tilePixelX) * 4u;\n outputNormalDepthBuffer.values[index] = normal.x;\n outputNormalDepthBuffer.values[index + 1u] = normal.y;\n outputNormalDepthBuffer.values[index + 2u] = normal.z;\n outputNormalDepthBuffer.values[index + 3u] = depth;\n\n // Min, max depth\n // When depth is 0, it means no hit, so ignore\n if (depth > 0f) {\n atomicMin(&depthMinMaxBuffer.values[0], u32(depth * 1000f));\n }\n atomicMax(&depthMinMaxBuffer.values[1], u32(depth * 1000f));\n}\n\n@compute @workgroup_size(256, 1, 1)\nfn depthNormalMultisampled(@builtin(global_invocation_id) globalId : vec3) {\n let imageSize = vec2(uniforms.width * uniforms.tilesX, uniforms.height * uniforms.tilesY);\n let tileSize = vec2(uniforms.width, uniforms.height);\n\n // Tex coords [0,1]\n // let id = f32(globalId.x);\n // let v = floor(id / imageSize.x);\n // let u = (id - v * imageSize.x);\n // let uv = vec2(u, v);\n // let texCoord = uv / imageSize;\n\n // Pixel coords ([0,width-1], [0,height-1])\n let id = f32(globalId.x);\n let tilePixelY = floor(id / tileSize.x);\n let tilePixelX = id - tilePixelY * tileSize.x;\n let imagePixelX = tilePixelX + uniforms.tileOffsetX * tileSize.x;\n let imagePixelY = tilePixelY + uniforms.tileOffsetY * tileSize.y;\n\n // Tex coords ([0,1], [0,1])\n let texCoord = vec2(imagePixelX / imageSize.x, imagePixelY / imageSize.y);\n\n // Camera\n var camera = getCamera(uniforms);\n\n // Sample position (sub-pixel sampling has same seed, but only sampled once per frame)\n var frameSeed = u32(uniforms.seed);\n var seed = u32(tilePixelY * tileSize.x + tilePixelX) + frameSeed * u32(tileSize.x * tileSize.y);\n let samplePos = vec2(texCoord) + vec2(random(&seed), random(&seed)) / imageSize;\n\n // Ray\n var ray = getCameraRay(camera, &seed, samplePos);\n\n // Result\n var normal = vec3(0f, 0f, 0f);\n var depth = 0f;\n var hitRecord: HitRecord;\n if (hitBVH(ray, 0.00001f, 100f, &hitRecord, &seed)) {\n normal = hitRecord.normal * 0.5f + vec3(0.5f, 0.5f, 0.5f);\n depth = -1f / dot(hitRecord.position - ray.origin, uniforms.forward);\n }\n\n let index = globalId.x * 4u;\n // let index = u32(tilePixelY * tileSize.x + tilePixelX) * 4u;\n outputNormalDepthBuffer.values[index] += normal.x;\n outputNormalDepthBuffer.values[index + 1u] += normal.y;\n outputNormalDepthBuffer.values[index + 2u] += normal.z;\n outputNormalDepthBuffer.values[index + 3u] += depth;\n\n // Min, max depth\n // When depth is 0, it means no hit, so ignore\n if (depth > 0f) {\n atomicMin(&depthMinMaxBuffer.values[0], u32(depth * 1000f));\n }\n atomicMax(&depthMinMaxBuffer.values[1], u32(depth * 1000f));\n}\n\n@compute @workgroup_size(256, 1, 1)\nfn segment(@builtin(global_invocation_id) globalId : vec3) {\n let imageSize = vec2(uniforms.width * uniforms.tilesX, uniforms.height * uniforms.tilesY);\n let tileSize = vec2(uniforms.width, uniforms.height);\n\n // Tex coords [0,1]\n // let id = f32(globalId.x);\n // let v = floor(id / imageSize.x);\n // let u = (id - v * imageSize.x);\n // let uv = vec2(u, v);\n // let texCoord = uv / imageSize;\n\n // Pixel coords ([0,width-1], [0,height-1])\n let id = f32(globalId.x);\n // let tilePixelY = floor(id / tileSize.x);\n // let tilePixelX = id - tilePixelY * tileSize.x;\n let tilePixelY = floor(id / (tileSize.x + 1)); // Overdispatched by 1\n let tilePixelX = id - tilePixelY * (tileSize.x + 1); // Overdispatched by 1\n let imagePixelX = tilePixelX + uniforms.tileOffsetX * tileSize.x;\n let imagePixelY = tilePixelY + uniforms.tileOffsetY * tileSize.y;\n\n // Tex coords ([0,1], [0,1]), can be > 1 with overdispatching\n let texCoord = vec2(imagePixelX / imageSize.x, imagePixelY / imageSize.y);\n\n // Camera\n var camera = getCamera(uniforms);\n\n // Sample position (sub-pixel sampling has same seed, but only sampled once per frame)\n let samplePos = vec2(texCoord);\n\n // Ray\n var seed = 0u; // No depth of field for depth, normal\n var ray = getCameraRay(camera, &seed, samplePos);\n\n // Result\n var color = vec4(1f, 1f, 1f, 0f);\n var hitRecord: HitRecord;\n if (hitBVH(ray, 0.00001f, 100f, &hitRecord, &seed)) {\n // color = materialBuffer.materials[hitRecord.materialId].idColor;\n\n // TODO: Generate unique id color in main if not supplied\n // let id = f32(hitRecord.id & 255u) / 255f;\n // color.x = id;\n // color.y = id;\n // color.z = id;\n\n color = hittableBuffer.hittables[hitRecord.id].segmentColor;\n\n // let color3 = textureValue(hitRecord);\n // color.x = color3.x;\n // color.y = color3.y;\n // color.z = color3.z;\n }\n\n let index = globalId.x * 4u;\n // let index = u32(tilePixelY * (tileSize.x + 1) + tilePixelX) * 4u; // Overdispatched by 1\n outputNormalDepthBuffer.values[index] = color.x;\n outputNormalDepthBuffer.values[index + 1u] = color.y;\n outputNormalDepthBuffer.values[index + 2u] = color.z;\n outputNormalDepthBuffer.values[index + 3u] = color.w;\n\n // TODO: Expand outputNormalDepthBuffer to store additional color channel for filling edges, or use separate buffer\n}\n\n// @builtin(local_invocation_id) localId : vec3,\n// @builtin(num_workgroups) numWorkgroups : vec3,\n// @builtin(workgroup_id) workgroupId : vec3\n// TODO: Use workgroup dimensions xy to get position directly froem globalId\n// Then store using textureStore\n// Check within bounds due to overdispatching\n\n@compute @workgroup_size(256, 1, 1)\nfn main(@builtin(global_invocation_id) globalId : vec3) {\n let imageSize = vec2(uniforms.width * uniforms.tilesX, uniforms.height * uniforms.tilesY);\n let tileSize = vec2(uniforms.width, uniforms.height);\n\n // TODO: Pixels at x=0 have noise when object overlaps right-side of screen at widths of 75, 150, 300, 600, 1200, 2400\n\n // Tex coords [0,1]\n // let id = f32(globalId.x);\n // TODO: Divide by (imageSize.x - 1)\n // let v = floor(id / imageSize.x);\n // let u = (id - v * imageSize.x);\n // let uv = vec2(u, v);\n // let texCoord = uv / imageSize;\n\n // Pixel coords ([0,width-1], [0,height-1])\n let id = f32(globalId.x);\n let tilePixelY = floor(id / tileSize.x);\n let tilePixelX = id - tilePixelY * tileSize.x;\n let imagePixelX = tilePixelX + uniforms.tileOffsetX * tileSize.x;\n let imagePixelY = tilePixelY + uniforms.tileOffsetY * tileSize.y;\n\n // Tex coords ([0,1], [0,1])\n let texCoord = vec2(imagePixelX / imageSize.x, imagePixelY / imageSize.y);\n\n // Camera\n var camera = getCamera(uniforms);\n\n // Frame seed\n var frameSeed = u32(uniforms.seed);\n let raysPerFrame = u32(uniforms.raysPerFrame);\n var color = vec3(0f, 0f, 0f);\n var depth = 0f;\n var normal = vec3(0f, 0f, 0f);\n var seed: u32;\n\n for (var i = 0u; i < raysPerFrame; i = i + 1u) {\n // Random number generator\n // See https://nelari.us/post/weekend_raytracing_with_wgpu_1/#implement-a-random-number-generator\n // fn initRng(pixel: vec2, resolution: vec2, frame: u32) -> u32 {\n // // Adapted from https://github.com/boksajak/referencePT\n // let seed = dot(pixel, vec2(1u, resolution.x)) ^ jenkinsHash(frame);\n // return jenkinsHash(seed);\n // }\n //\n // fn jenkinsHash(input: u32) -> u32 {\n // var x = input;\n // x += x << 10u;\n // x ^= x >> 6u;\n // x += x << 3u;\n // x ^= x >> 11u;\n // x += x << 15u;\n // return x;\n // }\n // TODO: Consider switching to u32 for uniforms and use vec3 arithmetic\n seed = u32(tilePixelY * tileSize.x + tilePixelX) + frameSeed * u32(tileSize.x * tileSize.y);\n\n // Sample position (sub-pixel sampling has same seed, but only sampled once per frame)\n let samplePos = vec2(texCoord) + vec2(random(&seed), random(&seed)) / imageSize;\n\n // Ray\n var ray = getCameraRay(camera, &seed, samplePos);\n\n // Color [0,1]\n // let color = result.color;\n // let color = clamp(result.color, vec3(0f, 0f, 0f), vec3(1f, 1f, 1f));\n // let color = clamp(result.color, vec3(0f, 0f, 0f), vec3(10f, 10f, 10f)); // Max light\n color += clamp(rayColor(&ray, &seed), vec3(0f, 0f, 0f), vec3(10f, 10f, 10f)); // Max light\n\n // Depth\n // let depth = 1f / rayColor(&ray, &seed).depth;\n // color = vec3(depth, depth, depth);\n // depth += result.depth;\n\n // Normal\n // normal += result.normal;\n\n // Next frame\n frameSeed = frameSeed + 1u;\n }\n let index = globalId.x * 3u;\n outputColorBuffer.values[index + 0u] = outputColorBuffer.values[index + 0u] + color.x;\n outputColorBuffer.values[index + 1u] = outputColorBuffer.values[index + 1u] + color.y;\n outputColorBuffer.values[index + 2u] = outputColorBuffer.values[index + 2u] + color.z;\n // outputDepthBuffer.values[globalId.x] = outputDepthBuffer.values[globalId.x] + depth;\n // outputNormalBuffer.values[index + 0u] = outputNormalBuffer.values[index + 0u] + normal.x;\n // outputNormalBuffer.values[index + 1u] = outputNormalBuffer.values[index + 1u] + normal.y;\n // outputNormalBuffer.values[index + 2u] = outputNormalBuffer.values[index + 2u] + normal.z;\n}"; export declare class ComputeUniformBufferData extends Float32Array { static readonly SIZE: number; readonly POSITION_OFFSET: number; readonly WIDTH_OFFSET: number; readonly RIGHT_OFFSET: number; readonly HEIGHT_OFFSET: number; readonly UP_OFFSET: number; readonly SEED_OFFSET: number; readonly FORWARD_OFFSET: number; readonly FOV_OFFSET: number; readonly BACKGROUND_COLOR_OFFSET: number; readonly TIME0_OFFSET: number; readonly AMBIENT_COLOR_OFFSET: number; readonly TIME1_OFFSET: number; readonly TILES_X: number; readonly TILES_Y: number; readonly TILE_OFFSET_X: number; readonly TILE_OFFSET_Y: number; readonly LOOKAT_OFFSET: number; readonly APERTURE_OFFSET: number; readonly FOCUS_DISTANCE_OFFSET: number; readonly RAYS_PER_FRAME_OFFSET: number; constructor(); getFocusDistance(): number; setFocusDistance(value: number): void; getWidth(): number; setWidth(value: number): void; getHeight(): number; setHeight(value: number): void; getSeed(): number; setSeed(value: number): void; getRaysPerFrame(): number; setRaysPerFrame(value: number): void; getFieldOfView(): number; setFieldOfView(value: number): void; getAperture(): number; setAperture(value: number): void; getPosition(value: vec3): void; setPosition(value: vec3): void; getRight(value: vec3): void; setRight(value: vec3): void; getUp(value: vec3): void; setUp(value: vec3): void; getForward(value: vec3): void; setForward(value: vec3): void; getBackgroundColor(value: vec4): void; setBackgroundColor(value: vec4): void; getAmbientColor(value: vec3): void; setAmbientColor(value: vec3): void; getTime0(): number; setTime0(value: number): void; getTime1(): number; setTime1(value: number): void; getTilesX(): number; setTilesX(value: number): void; getTilesY(): number; setTilesY(value: number): void; getTileOffsetX(): number; setTileOffsetX(value: number): void; getTileOffsetY(): number; setTileOffsetY(value: number): void; getLookAt(value: vec3): void; setLookAt(value: vec3): void; }