From ccdaab8e783178f1acc8bec0e69991b10eb29066 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Mon, 5 Aug 2019 16:02:43 +0200 Subject: [PATCH] Add shader file --- Assets/raytrace/build.md | 4 - Assets/raytrace/raytrace.hlsl | 287 ++++++++++++++++++++++++++++++++++ Bundled/raytrace/raytrace.cso | Bin 0 -> 11740 bytes 3 files changed, 287 insertions(+), 4 deletions(-) create mode 100644 Assets/raytrace/raytrace.hlsl create mode 100644 Bundled/raytrace/raytrace.cso diff --git a/Assets/raytrace/build.md b/Assets/raytrace/build.md index bd00308f..57af4aac 100644 --- a/Assets/raytrace/build.md +++ b/Assets/raytrace/build.md @@ -1,5 +1 @@ -Build: -`node Kinc/make -g direct3d12 --raytrace dxr` - -Compile shader: `.\dxc.exe -Zpr -Fo ..\..\Bundled\raytrace\raytrace.cso -T lib_6_3 .\raytrace.hlsl` diff --git a/Assets/raytrace/raytrace.hlsl b/Assets/raytrace/raytrace.hlsl new file mode 100644 index 00000000..a40d116a --- /dev/null +++ b/Assets/raytrace/raytrace.hlsl @@ -0,0 +1,287 @@ + +struct Vertex { + float3 position; + float3 normal; + float2 tex; +}; + +struct RayGenConstantBuffer { + float4 eye; // xyz, frame + float4x4 inv_vp; +}; + +struct RayPayload { + float4 color; + float3 ray_origin; + float3 ray_dir; +}; + +RWTexture2D render_target : register(u0); +RaytracingAccelerationStructure scene : register(t0); +ByteAddressBuffer indices : register(t1); +StructuredBuffer vertices : register(t2); +ConstantBuffer constant_buffer : register(b0); + +Texture2D mytexture0 : register(t3); +Texture2D mytexture1 : register(t4); +Texture2D mytexture2 : register(t5); +Texture2D mytexture_env : register(t6); + +static uint seed; +static const float PI = 3.1415926535f; + +void create_basis(float3 normal, out float3 tangent, out float3 binormal) { + tangent = abs(normal.x) > abs(normal.y) ? + normalize(float3(0., normal.z, -normal.y)) : + normalize(float3(-normal.z, 0., normal.x)); + binormal = cross(normal, tangent); +} + +uint wang_hash(uint seed) { + seed = (seed ^ 61) ^ (seed >> 16); + seed *= 9; + seed = seed ^ (seed >> 4); + seed *= 0x27d4eb2d; + seed = seed ^ (seed >> 15); + return seed; +} + +void compute_seed(uint index, uint iteration, uint depth) { + seed = uint(wang_hash((1 << 31) | (depth << 22) | iteration) ^ wang_hash(index)); +} + +float rand() { + static const float png_01_convert = (1.0f / 4294967296.0f); + seed ^= uint(seed << 13); + seed ^= uint(seed >> 17); + seed ^= uint(seed << 5); + return float(seed * png_01_convert); +} + +float schlick(float cosine, float ri) { + float r0 = (1 - ri) / (1 + ri); + r0 = r0 * r0; + return r0 + (1 - r0) * pow(saturate(1 - cosine), 5); +} + +float schlick_weight(float cosTheta) { + float m = clamp(1. - cosTheta, 0., 1.); + float m2 = m * m; + return m2 * m2 * m; +} + +float2 calculate_concentric_sample_disk(float u, float v) { + // Maps a (u,v) in [0, 1)^2 to a 2D unit disk centered at (0,0). Based on PBRT. + float2 u_offset = 2.0f * float2(u, v) - float2(1, 1); + if (u_offset.x == 0 && u_offset.y == 0) { + return float2(0.0f, 0.0f); + } + float theta, r; + if (abs(u_offset.x) > abs(u_offset.y)) { + r = u_offset.x; + theta = PI / 4 * (u_offset.y / u_offset.x); + } + else { + r = u_offset.y; + theta = (PI / 2) - (PI / 4 * (u_offset.x / u_offset.y)); + } + return r * float2(cos(theta), sin(theta)); +} + +void generate_camera_ray(float2 screen_pos, out float3 ray_origin, out float3 ray_dir) { + screen_pos.y = -screen_pos.y; + float4 world = mul(float4(screen_pos, 0, 1), constant_buffer.inv_vp); + world.xyz /= world.w; + ray_origin = constant_buffer.eye.xyz; + ray_dir = normalize(world.xyz - ray_origin); + + // Depth of Field + // float lens_rad = 0.005f; + // float focal_dist = 0.4f; + // float3 plens = float3(lens_rad * calculate_concentric_sample_disk(rand(), rand()), 0.0f); + // float ft = focal_dist / abs(ray_dir.z); + // float3 pfocus = ray_dir * ft; + // ray_origin += plens; + // ray_dir = normalize(pfocus - plens); +} + +[shader("raygeneration")] +void raygeneration() { + uint2 sample_point = DispatchRaysIndex().xy; + uint2 sample_dim = DispatchRaysDimensions().xy; + uint id = sample_point.x + sample_dim.x * sample_point.y; + const int depth = 5; + compute_seed(id, constant_buffer.eye.w, depth); + + float2 xy = DispatchRaysIndex().xy + 0.5f; + xy.x += rand(); // AA + xy.y += rand(); + float2 screen_pos = xy / DispatchRaysDimensions().xy * 2.0 - 1.0; + + RayPayload payload; + payload.color = float4(1, 1, 1, -1); + generate_camera_ray(screen_pos, payload.ray_origin, payload.ray_dir); + + RayDesc ray; + ray.TMin = 0.01; + ray.TMax = 10.0; + ray.Origin = payload.ray_origin; + ray.Direction = payload.ray_dir; + + for (int i = 0; i < depth; ++i) { + TraceRay(scene, RAY_FLAG_FORCE_OPAQUE, ~0, 0, 1, 0, ray, payload); + compute_seed(id, constant_buffer.eye.w, i); + + if (payload.color.a != 0) break; + ray.Origin = payload.ray_origin; + ray.Direction = payload.ray_dir; + } + + float3 color = float3(render_target[DispatchRaysIndex().xy].xyz); + if (constant_buffer.eye.w == 1.0) color = float3(0, 0, 0); + float a = 1.0 / constant_buffer.eye.w; + float b = 1.0 - a; + + color = color * b + payload.color.xyz * a; + render_target[DispatchRaysIndex().xy] = float4(color.xyz, 0.0f); +} + +float3 hit_world_position() { + return WorldRayOrigin() + RayTCurrent() * WorldRayDirection(); +} + +float3 hit_attribute(float3 vertexAttribute[3], BuiltInTriangleIntersectionAttributes attr) { + return vertexAttribute[0] + + attr.barycentrics.x * (vertexAttribute[1] - vertexAttribute[0]) + + attr.barycentrics.y * (vertexAttribute[2] - vertexAttribute[0]); +} + +float2 hit_attribute2d(float2 vertexAttribute[3], BuiltInTriangleIntersectionAttributes attr) { + return vertexAttribute[0] + + attr.barycentrics.x * (vertexAttribute[1] - vertexAttribute[0]) + + attr.barycentrics.y * (vertexAttribute[2] - vertexAttribute[0]); +} + +float3 cos_weighted_random_hemisphere_direction(float3 n) { + float2 r = float2(rand(), rand()); + float3 uu = normalize(cross(n, float3(0.0, 1.0, 1.0))); + float3 vv = cross(uu, n); + float ra = sqrt(r.y); + float rx = ra * cos(6.2831 * r.x); + float ry = ra * sin(6.2831 * r.x); + float rz = sqrt(1.0 - r.y); + float3 rr = float3(rx * uu + ry * vv + rz * n); + return normalize(rr); +} + +[shader("closesthit")] +void closesthit(inout RayPayload payload, in BuiltInTriangleIntersectionAttributes attr) { + uint2 sample_point = DispatchRaysIndex().xy; + uint2 sample_dim = DispatchRaysDimensions().xy; + uint id = sample_point.x + sample_dim.x * sample_point.y; + compute_seed(id, constant_buffer.eye.w, 0); + + float3 hit_position = hit_world_position(); + + const uint triangleIndexStride = 12; // 3 * 4 + uint base_index = PrimitiveIndex() * triangleIndexStride; + uint3 indices_sample = indices.Load3(base_index); + + float3 vertex_normals[3] = { + float3(vertices[indices_sample[0]].normal), + float3(vertices[indices_sample[1]].normal), + float3(vertices[indices_sample[2]].normal) + }; + float3 n = normalize(hit_attribute(vertex_normals, attr)); + + float2 vertex_uvs[3] = { + float2(vertices[indices_sample[0]].tex), + float2(vertices[indices_sample[1]].tex), + float2(vertices[indices_sample[2]].tex) + }; + float2 tex_coord = hit_attribute2d(vertex_uvs, attr); + + float emittance = 0.0; + float3 texpaint0 = mytexture0.Load(uint3(tex_coord * 2048, 0)).rgb; + float3 texpaint1 = mytexture1.Load(uint3(tex_coord * 2048, 0)).rgb; + float3 texpaint2 = mytexture2.Load(uint3(tex_coord * 2048, 0)).rgb; + if (hit_position.z > 0.99) { + emittance = 1.0; + texpaint0 = float3(5, 5, 5); // temp + } + float3 color = payload.color.rgb * texpaint0.rgb; + + if (emittance == 0.0f) { + if (texpaint2.b >= 0.99) { + payload.ray_dir = lerp(reflect(WorldRayDirection(), n), cos_weighted_random_hemisphere_direction(n), texpaint2.g); + } + // else if (texpaint2.matid == glass) { + // float indexOfRefraction = 1.0f; + // // adjust eta & normal according to direction of ray (inside or outside mat) + // bool inside = dot(WorldRayDirection(), n) > 0.f; + // float3 tempNormal = n * (inside ? -1.0f : 1.0f); + // float eta = inside ? indexOfRefraction : (1.0f / indexOfRefraction); + // // normal refraction + // float3 newDir = refract(WorldRayDirection(), tempNormal, eta); + // // internal total reflection + // if (length(newDir) < 0.01f) { + // color *= 0; + // newDir = reflect(WorldRayDirection(), n); + // } + // // use schlick's approx + // float schlick_0 = pow((inside ? indexOfRefraction - 1.0f : 1.0f - indexOfRefraction) / + // (1.0f + indexOfRefraction), 2.0f); + // float schlick_coef = schlick_0 + + // (1 - schlick_0) * pow(1 - max(0.0f, dot(WorldRayDirection(), n)), 5); + // // based on coef, pick either a refraction or reflection + // newDir = schlick_coef < rand() ? reflect(WorldRayDirection(), n) : newDir; + // payload.ray_dir = newDir; + // } + else { + // TEMP + float fresnel = schlick(dot(n, payload.ray_dir), 1.35) * (1.0 - texpaint2.g); + if (rand() > fresnel) { + payload.ray_dir = cos_weighted_random_hemisphere_direction(n); + + // float3 wo = -payload.ray_dir; + // payload.ray_dir = cos_weighted_random_hemisphere_direction(n); + // float3 tangent = float3(0, 0, 0); + // float3 binormal = float3(0, 0, 0); + // create_basis(n, tangent, binormal); + // float3 wi = payload.ray_dir.x * tangent + payload.ray_dir.y * binormal + payload.ray_dir.z * n; + // float dotNL = dot(n, wo); + // float dotNV = dot(n, wi); + // float3 H = normalize(wo + wi); + // float dotLH = dot(wo, H); + // float FL = schlick_weight(dotNL); + // float FV = schlick_weight(dotNV); + // float Fss90 = dotLH * dotLH * texpaint2.g; + // float Fss = lerp(1.0, Fss90, FL) * lerp(1.0, Fss90, FV); + // float ss = 1.25 * (Fss * (1.0 / (dotNL + dotNV) - 0.5) + 0.5); + // color = (1 / PI) * ss * color; + // if (dotNL < 0.0 || dotNV < 0.0) color = float3(0, 0, 0); + } + else { + payload.ray_dir = reflect(WorldRayDirection(), n); + } + } + payload.ray_origin = hit_position + payload.ray_dir * 0.0001f; + } + + payload.color = float4(color.xyz, emittance); +} + +float2 equirect(float3 normal) { + const float PI = 3.1415926535; + const float PI2 = PI * 2.0; + float phi = acos(normal.z); + float theta = atan2(-normal.y, normal.x) + PI; + return float2(theta / PI2, phi / PI); +} + +[shader("miss")] +void miss(inout RayPayload payload) { + float2 tex_coord = equirect(normalize(payload.ray_dir)); + float3 texenv = mytexture_env.Load(uint3(tex_coord.x * 1024, tex_coord.y * 512, 0)).rgb * 3; + payload.color = float4(payload.color.rgb * texenv.rgb, -1); +} diff --git a/Bundled/raytrace/raytrace.cso b/Bundled/raytrace/raytrace.cso new file mode 100644 index 0000000000000000000000000000000000000000..034940d695e6ecaf94f9101d7e1be226fa274995 GIT binary patch literal 11740 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 literal 0 HcmV?d00001