diff --git a/makefile b/makefile index 08d8984..e60cffb 100644 --- a/makefile +++ b/makefile @@ -2,6 +2,10 @@ SRC_DIR = src BIN_DIR = bin RES_DIR = res +SHADER_DIR = $(SRC_DIR)/_shader +SHADER_ROOT_DIR = $(SHADER_DIR)/root +SHADER_INCLUDE_DIR = $(SHADER_DIR)/common + TARGET = $(BIN_DIR)/oglc CC = gcc LIBS = `pkg-config --static --libs glew sdl2` @@ -10,10 +14,21 @@ CFLAGS = -I$(SRC_DIR) -Wall SRC = $(shell find $(SRC_DIR) -name *.c) OBJ = $(SRC:%.c=%.o) +# find files in SHADER_ROOT_DIR +# top level compute shader programs +SHADERS = $(shell find $(SHADER_ROOT_DIR) -name *.glsl) + +# find files in SHADER_INCLUDE_DIR +# small chunks of shader code, included repeatedly in the top-level programs +SHADER_INCLUDES = $(shell find $(SHADER_INCLUDE_DIR) -name *.glsl) + # create dirs if they dont exist _dummy := $(shell mkdir -p $(BIN_DIR)) $(TARGET): $(OBJ) + # preprocess shaders and store results in bin/res/shader/ under root name + foreach root,$(SHADER_ROOT_DIR),$(echo $(root)) + $(CC) $(CFLAGS) -o $@ $^ $(LIBS) cp -r $(RES_DIR) $(BIN_DIR) diff --git a/res/shader/compute.glsl b/res/shader/compute.glsl index 38a7a60..4e822f1 100644 --- a/res/shader/compute.glsl +++ b/res/shader/compute.glsl @@ -1,22 +1,106 @@ #version 430 -layout (location = 1) uniform float t; +layout (location = 1) uniform vec4 t; -// size of local work group - 1 pixel -layout(local_size_x = 1, local_size_y = 1) in; +layout(local_size_x = 1, local_size_y = 1) in; // size of local work group - 1 pixel +layout(rgba32f, binding = 0) uniform image2D img_output; // rgba32f defines internal format, image2d for random write to output texture -// rgba32f defines internal format, image2d for random write to output texture -layout(rgba32f, binding = 0) uniform image2D img_output; +const float INF = 1000000.0f; +struct Sphere +{ + vec3 center; + float radius; +}; + +struct Ray +{ + vec3 origin; + vec3 direction; +}; + +struct RayHit +{ + vec3 position; + float distance; + vec3 normal; +}; + +void intersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere) +{ + vec3 d = ray.origin-sphere.center; + float p1 = -dot(ray.direction,d); + float p2sqr = p1*p1-dot(d,d)+sphere.radius*sphere.radius; + + if (p2sqr < 0) return; + + float p2 = sqrt(p2sqr); + float t = p1-p2 > 0 ? p1-p2 : p1+p2; + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t*ray.direction; + bestHit.normal = normalize(bestHit.position-sphere.center); + } +} + +Ray createCameraRay(vec2 uv) +{ + // transform -1..1 -> 0..1 + uv = uv*0.5+0.5; + //uv.x=1-uv.x; + + // transform camera origin to world space + // TODO: c2w matrix!! for now we just assume the camera is at the origin + // float3 origin = mul(_CameraToWorld, float4(0.0,0.0,0.0,1.0)).xyz; + + // TODO: offset from centre of the lens for depth of field + // float2 rd = _CameraLensRadius * randomInUnitDisk(); + // float3 offset = _CameraU * rd.x + _CameraV * rd.y; + + // ... + + float max_x = 5.0; + float max_y = 5.0; + + Ray ray; + ray.origin = vec3(uv.x * max_x, uv.y * max_y, 0.0); + ray.direction = vec3(0.0,0.0,1.0); // ortho forwards + + return ray; +} void main() { // base pixel colour for the image - vec4 pixel = vec4(1.0, t, 0.0, 1.0); + vec4 pixel = vec4(0.0, 0.0, 0.0, 1.0); // get index in global work group ie xy position ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); - // + // set up ray based on pixel position, project it forward with an orthographic projection + ivec2 dims = imageSize(img_output); // fetch image dimensions + vec2 uv; + uv.x = (float(pixel_coords.x * 2 - dims.x) / dims.x) * dims.x/dims.y; // account for aspect ratio + uv.y = (float(pixel_coords.y * 2 - dims.y) / dims.y); + + Ray ray = createCameraRay(uv); + + RayHit hit; + hit.position = vec3(0.0,0.0,0.0); + hit.distance = INF; + hit.normal = vec3(0.0,0.0,0.0); + + Sphere sphere; + sphere.center = vec3(0.0,0.0,10.0); + sphere.radius = 3.0+t.y; + + // ray-sphere intersection + intersectSphere(ray, hit, sphere); + + if (hit.distance < INF) + { + pixel = vec4(t.y,1.0-t.y,1.0,1.0); + } // output to a specific pixel in the image imageStore(img_output, pixel_coords, pixel); diff --git a/res/shader/rt.compute b/res/shader/rt.compute new file mode 100644 index 0000000..3974d70 --- /dev/null +++ b/res/shader/rt.compute @@ -0,0 +1,506 @@ +// Each #kernel tells which function to compile; you can have many kernels +#pragma kernel CSMain + +struct Sphere +{ + float3 position; + float radius; + float3 albedo; + float3 specular; + float3 emission; +}; + +struct Tube +{ + float3 position; + float3 axis; + float radius; + float height; + float thickness; +}; + +struct Unit +{ + float3 position; + int team; + int selected; +}; + +// Create a RenderTexture with enableRandomWrite flag and set it +// with cs.SetTexture +RWTexture2D Result; +float2 _Pixel; +float _Seed; +float _EmissionScale; +int _Bounces; +int _SamplesPerPixel; + +// camera +float2 _Resolution; +float4x4 _CameraToWorld; +float4x4 _CameraInverseProjection; +float3 _CameraW; +float3 _CameraU; +float3 _CameraV; +float3 _CameraHorizontal; +float3 _CameraVertical; +float3 _CameraLowerLeftCorner; +float _CameraLensRadius; +float _CameraFocusDistance; + +// environment +float _GroundHeight; +float3 _GroundColor; +float _SkyHeight; +float _SkyHoleRadius; +float3 _SkyColor; + +int _ActiveSpheres; +int _ActiveTubes; + +int _ActiveUnits; +float3 _UnitColor; +float _UnitRadius; + +StructuredBuffer _Units; +StructuredBuffer _Tubes; +StructuredBuffer _Spheres; + +#define GROUP_SIZE 32 + +static const float PI = 3.14159265f; +static const float BIG = 1000000.0f; // not infinity but close enough + +static const int MAT_LAMBERT = 0; +static const int MAT_DIELECTRIC = 1; + +struct Ray +{ + float3 origin; + float3 direction; + float3 energy; +}; + +Ray createRay(float3 origin, float3 direction) +{ + Ray ray; + ray.origin = origin; + ray.direction = direction; + ray.energy = float3(1.0f, 1.0f, 1.0f); + return ray; +} + +struct RayHit +{ + float3 position; + float distance; + float3 normal; + float3 albedo; + float3 specular; + float3 emission; +}; + +RayHit createRayHit() +{ + RayHit hit; + hit.position = float3(0.0f, 0.0f, 0.0f); + hit.distance = BIG; + hit.normal = float3(0.0f, 0.0f, 0.0f); + hit.albedo = float3(0.0f, 0.0f, 0.0f); + hit.specular = float3(0.0f, 0.0f, 0.0f); + hit.emission = float3(0.0f, 0.0f, 0.0f); + return hit; +} + +float rand() +{ + float result = frac(sin(_Seed / 100.0f * dot(_Pixel, float2(12.9898f, 78.233f))) * 43758.5453f); + _Seed += 1.0f; + return result; +} + +float sdot(float3 x, float3 y, float f = 1.0f) +{ + return saturate(dot(x, y) * f); +} + +float3x3 getTangentSpace(float3 normal) +{ + // helper vector for the cross product + float3 helper = float3(1, 0, 0); + if (abs(normal.x) > 0.99f) + { + helper = float3(0, 0, 1); + } + + // generate vectors + float3 tangent = normalize(cross(normal, helper)); + float3 binormal = normalize(cross(normal, tangent)); + return float3x3(tangent, binormal, normal); +} + +float3 sampleHemisphere(float3 normal) +{ + // uniformly sample hemisphere direction + float cosTheta = rand(); + float sinTheta = sqrt(max(0.0f, 1.0f - cosTheta * cosTheta)); + float phi = 2 * PI * rand(); + float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + + // transform direction to world space + return mul(tangentSpaceDir, getTangentSpace(normal)); +} + +float2 randomInUnitDisk() +{ + // pick a random radius and angle then convert to cartesian + float r = rand(); + float theta = rand() * 2 * PI; + return float2(cos(theta), sin(theta)) * r; +} + +Ray createCameraRay(float2 uv) +{ + // transform -1..1 -> 0..1 + uv = uv * 0.5 + 0.5; + uv.x = 1 - uv.x; + + // transform the camera origin to world space + float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz; + + // offset from centre of the lens for depth of field + float2 rd = _CameraLensRadius * randomInUnitDisk(); + float3 offset = _CameraU * rd.x + _CameraV * rd.y; + + origin += offset; + + // invert the perspective projection of the view-space position + float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz; + + // transform the direction from camera to world space and normalize + direction = mul(_CameraToWorld, float4(direction, 0.0f)).xyz; + + direction = _CameraLowerLeftConer + + uv.x * _CameraHorizontal + + uv.y * _CameraVertical + - origin; + + // direction = mul(_CameraInverseProjection, float4(direction, 0)).xyz; + + direction = normalize(direction); + return createRay(origin, direction); +} + + +void intersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere) +{ + // calculate distance along the ray where the sphere is intersected + float3 d = ray.origin - sphere.position; + float p1 = -dot(ray.direction, d); + float p2sqr = p1 * p1 - dot(d, d) + sphere.radius * sphere.radius; + + if (p2sqr < 0) return; + + float p2 = sqrt(p2sqr); + float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2; + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = normalize(bestHit.position - sphere.position); + bestHit.albedo = sphere.albedo; + bestHit.specular = sphere.specular; + bestHit.emission = sphere.emission; + } +} + +float intersectPlane(Ray ray, float3 p, float3 normal) +{ + float denom = dot(normal, ray.direction); + + if (abs(denom) > 0.0001) + { + float t = dot(p - ray.origin, normal) / denom; + if (t >= 0) return t; + } + return -1; +} + +// https://www.iquilezles.org/www/articles/intersectors/intersectors.htm +// cylinder defined in extremes pa and pb, and radius ra +float4 intersectCylinder(Ray ray, float3 pa, float3 pb, float ra, bool inner) +{ + float3 ro = ray.origin; + float3 rd = ray.direction; + + // central axis + float3 ca = pb - pa; + // eye to base + float3 oc = ro - pa; + + // dot products + float caca = dot(ca, ca); + float card = dot(ca, rd); + float caoc = dot(ca, oc); + + // find intersects + float a = caca - card * card; + float b = caca * dot(oc, rd) - caoc * card; + float c = caca * dot(oc, oc) - caoc * caoc - ra * ra * caca; + float h = b * b - a * c; + + if (h < 0.0) return float4(-1, 0, 0, 0); // no intersection + + h = sqrt(h); + h = inner?-h:h; + float t = (-b - h) / a; + + // body + float y = caoc + t * card; + if (y > 0.0 && y < caca) return float4(t, (oc+t*rd - ca*y/caca) / ra); + + // caps + t = ((y < 0.0 ? 0.0 : caca) - caoc) / card; + if (abs(b + a * t) < h) return float4(t, ca * sign(y) / caca); + + return float4(-1, 0, 0, 0); // no intersection +} + +float sdSegment(float3 p, float3 a, float3 b, float r) +{ + float3 pa = p - a, ba = b - a; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + return length(pa - ba * h) - r; +} + +float opSubtraction(float d1, float d2) { return max(-d1, d2); } + +void intersectTube(Ray ray, inout RayHit bestHit, Tube tube) +{ + // TODO: inner tube + + float height = tube.height; + + float3 axis = normalize(tube.axis); + + float3 pa = tube.position + axis * -height * 0.5; + float3 pb = tube.position + axis * height * 0.5; + + float r_inner = (tube.radius-tube.thickness)/tube.radius; + + // where the ray hit the outer surface + float4 outerHit = intersectCylinder(ray, pa, pb, tube.radius, false); + // outerHit = float4(-1,0,0,0); + // where we hit the inner surface + float4 innerHit = intersectCylinder(ray, pa, pb, tube.radius * r_inner, true); + // float4 innerHit = float4(-1,0,0,0); + // innerHit.yzw *= -1; + + if (outerHit.x < 0 && innerHit.x < 0) return; + + float3 pos_outer = ray.origin + outerHit.x * ray.direction; + float axis_distance = sdSegment(pos_outer, pa, pb, 0); + + float t = bestHit.distance; + + // hit the inner surface + if (innerHit.x > 0 && innerHit.x < bestHit.distance) + { + t = innerHit.x; + + bestHit.normal = normalize(innerHit.yzw); + bestHit.position = ray.origin + t * ray.direction; + bestHit.distance = t; + + bestHit.albedo = float3(.5, .5, .5); + bestHit.emission = float3(0, 0, 0); + bestHit.specular = float3(1, 1, 1); + } + + // hit the outer surface + if (outerHit.x > 0 && outerHit.x < bestHit.distance && axis_distance > tube.radius*r_inner) + { + t = outerHit.x; + + bestHit.normal = normalize(outerHit.yzw); + bestHit.position = ray.origin + t * ray.direction; + bestHit.distance = t; + + bestHit.albedo = float3(.5, .5, .5); + bestHit.emission = float3(0, 0, 0); + bestHit.specular = float3(1, 1, 1); + } +} + +void intersectGroundPlane(inout Ray ray, inout RayHit bestHit) +{ + float3 albedo = _GroundColor; + float3 specular = float3(0, 0, 0); + + // calculate distance along the ray where the ground plane is intersected + float t = -(ray.origin.y - _GroundHeight) / ray.direction.y; + + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = float3(0.0f, 1.0f, 0.0f); + bestHit.albedo = albedo; + bestHit.specular = specular; + bestHit.emission = float3(0, 0, 0); + } +} + +void intersectCeilingPlane(inout Ray ray, inout RayHit bestHit) +{ + float albedo = _SkyColor; + float3 specular = float3(0, 0, 0); + + // ignore plane if the ray is coming from above + if (ray.direction.y < 0) return; + + float t = -(ray.origin.y - _SkyHeight) / ray.direction.y; + float3 p = ray.origin + ray.direction * t; + + if (length(p.xz) < _SkyHoleRadius) return; + + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = float3(0.0f, -1.0f, 0.0f); + bestHit.albedo = albedo; + bestHit.specular = specular; + bestHit.emission = float3(0, 0, 0); + } +} + +void intersectWall(inout Ray ray, inout RayHit bestHit) +{ + // ignore collision if ray's angle is steep or negative + float a = dot(float3(0, 1, 0), ray.direction); + if (a > 0.2 || a < 0) return; + + Sphere sphere; + sphere.radius = BIG - 1; + sphere.albedo = float3(1, 1, 1) * 1.98; + sphere.specular = float3(0, 0, 0); + sphere.emission = float3(0, 0, 0); + sphere.position = float3(0, 0, 0); + + intersectSphere(ray, bestHit, sphere); +} + +RayHit trace(Ray ray) +{ + RayHit bestHit = createRayHit(); + + intersectWall(ray, bestHit); + intersectGroundPlane(ray, bestHit); + intersectCeilingPlane(ray, bestHit); + + uint numSpheres, numTubes, stride; + + // celestial bodies + // _Spheres.GetDimensions(_ActiveSpheres, stride); + for (uint i = 0; i < _ActiveSpheres; i++) + { + intersectSphere(ray, bestHit, _Spheres[i]); + } + + // _Tubes.GetDimensions(numTubes, stride); + for (uint i = 0; i < _ActiveTubes; i++) + { + intersectTube(ray, bestHit, _Tubes[i]); + } + + if (_ActiveUnits > 0) + { + // units + _Units.GetDimensions(numSpheres, stride); + for (uint i = 0; i < _ActiveUnits; i++) + { + Unit unit = _Units[i]; + + float3 color = float3 + (lerp(1, 0, unit.team), + 0, + lerp(0, 1, unit.team)); + + Sphere s; + s.albedo = color; + s.emission = color * unit.selected; + s.specular = float3(0, 0, 0); + s.radius = _UnitRadius; + s.position = unit.position; + + intersectSphere(ray, bestHit, s); + } + } + + return bestHit; +} + +float3 scatter_lambert(inout Ray ray, RayHit hit) +{ + ray.origin = hit.position + hit.normal * 0.001f; + ray.direction = sampleHemisphere(hit.normal); + ray.energy *= 2 * hit.albedo * sdot(hit.normal, ray.direction); + return 0.0f; +} + +float3 shade(inout Ray ray, RayHit hit) +{ + if (any(hit.emission)) return hit.emission; + + if (hit.distance < BIG) + { + return scatter_lambert(ray, hit) + hit.emission; + } + else + { + ray.energy = 0.0f; + + // float theta = acos(ray.direction.y) / -PI; + // float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f; + + return _SkyColor; + } +} + +[numthreads(GROUP_SIZE,GROUP_SIZE,1)] +void CSMain(uint3 id : SV_DispatchThreadID) +{ + _Pixel = id.xy; + + // get dimensions of render texture + uint width, height; + Result.GetDimensions(width, height); + + // transform pixel to -1, 1 range + float2 uv = float2(id.xy / float2(width, height) * 2.0f - 1.0f); + uv.x *= _Resolution.x / _Resolution.y; + + int samples = _SamplesPerPixel; + int bounces = _Bounces; + float3 result = float3(0, 0, 0); + + for (int i = 0; i < samples; i++) + { + // get a ray for the uv + Ray ray = createCameraRay(uv); + + // trace and shade + for (int i = 0; i < bounces; i++) + { + RayHit hit = trace(ray); + result += ray.energy * shade(ray, hit); + + if (!any(ray.energy)) break; + } + } + + result /= (float)samples; + + Result[id.xy] = float4(result, 1); +} diff --git a/src/_shader/root/compute.glsl b/src/_shader/root/compute.glsl new file mode 100644 index 0000000..4e822f1 --- /dev/null +++ b/src/_shader/root/compute.glsl @@ -0,0 +1,107 @@ +#version 430 + +layout (location = 1) uniform vec4 t; + +layout(local_size_x = 1, local_size_y = 1) in; // size of local work group - 1 pixel +layout(rgba32f, binding = 0) uniform image2D img_output; // rgba32f defines internal format, image2d for random write to output texture + +const float INF = 1000000.0f; + +struct Sphere +{ + vec3 center; + float radius; +}; + +struct Ray +{ + vec3 origin; + vec3 direction; +}; + +struct RayHit +{ + vec3 position; + float distance; + vec3 normal; +}; + +void intersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere) +{ + vec3 d = ray.origin-sphere.center; + float p1 = -dot(ray.direction,d); + float p2sqr = p1*p1-dot(d,d)+sphere.radius*sphere.radius; + + if (p2sqr < 0) return; + + float p2 = sqrt(p2sqr); + float t = p1-p2 > 0 ? p1-p2 : p1+p2; + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t*ray.direction; + bestHit.normal = normalize(bestHit.position-sphere.center); + } +} + +Ray createCameraRay(vec2 uv) +{ + // transform -1..1 -> 0..1 + uv = uv*0.5+0.5; + //uv.x=1-uv.x; + + // transform camera origin to world space + // TODO: c2w matrix!! for now we just assume the camera is at the origin + // float3 origin = mul(_CameraToWorld, float4(0.0,0.0,0.0,1.0)).xyz; + + // TODO: offset from centre of the lens for depth of field + // float2 rd = _CameraLensRadius * randomInUnitDisk(); + // float3 offset = _CameraU * rd.x + _CameraV * rd.y; + + // ... + + float max_x = 5.0; + float max_y = 5.0; + + Ray ray; + ray.origin = vec3(uv.x * max_x, uv.y * max_y, 0.0); + ray.direction = vec3(0.0,0.0,1.0); // ortho forwards + + return ray; +} + +void main() +{ + // base pixel colour for the image + vec4 pixel = vec4(0.0, 0.0, 0.0, 1.0); + // get index in global work group ie xy position + ivec2 pixel_coords = ivec2(gl_GlobalInvocationID.xy); + + // set up ray based on pixel position, project it forward with an orthographic projection + ivec2 dims = imageSize(img_output); // fetch image dimensions + vec2 uv; + uv.x = (float(pixel_coords.x * 2 - dims.x) / dims.x) * dims.x/dims.y; // account for aspect ratio + uv.y = (float(pixel_coords.y * 2 - dims.y) / dims.y); + + Ray ray = createCameraRay(uv); + + RayHit hit; + hit.position = vec3(0.0,0.0,0.0); + hit.distance = INF; + hit.normal = vec3(0.0,0.0,0.0); + + Sphere sphere; + sphere.center = vec3(0.0,0.0,10.0); + sphere.radius = 3.0+t.y; + + // ray-sphere intersection + intersectSphere(ray, hit, sphere); + + if (hit.distance < INF) + { + pixel = vec4(t.y,1.0-t.y,1.0,1.0); + } + + // output to a specific pixel in the image + imageStore(img_output, pixel_coords, pixel); +} diff --git a/src/_shader/root/rt.compute b/src/_shader/root/rt.compute new file mode 100644 index 0000000..3974d70 --- /dev/null +++ b/src/_shader/root/rt.compute @@ -0,0 +1,506 @@ +// Each #kernel tells which function to compile; you can have many kernels +#pragma kernel CSMain + +struct Sphere +{ + float3 position; + float radius; + float3 albedo; + float3 specular; + float3 emission; +}; + +struct Tube +{ + float3 position; + float3 axis; + float radius; + float height; + float thickness; +}; + +struct Unit +{ + float3 position; + int team; + int selected; +}; + +// Create a RenderTexture with enableRandomWrite flag and set it +// with cs.SetTexture +RWTexture2D Result; +float2 _Pixel; +float _Seed; +float _EmissionScale; +int _Bounces; +int _SamplesPerPixel; + +// camera +float2 _Resolution; +float4x4 _CameraToWorld; +float4x4 _CameraInverseProjection; +float3 _CameraW; +float3 _CameraU; +float3 _CameraV; +float3 _CameraHorizontal; +float3 _CameraVertical; +float3 _CameraLowerLeftCorner; +float _CameraLensRadius; +float _CameraFocusDistance; + +// environment +float _GroundHeight; +float3 _GroundColor; +float _SkyHeight; +float _SkyHoleRadius; +float3 _SkyColor; + +int _ActiveSpheres; +int _ActiveTubes; + +int _ActiveUnits; +float3 _UnitColor; +float _UnitRadius; + +StructuredBuffer _Units; +StructuredBuffer _Tubes; +StructuredBuffer _Spheres; + +#define GROUP_SIZE 32 + +static const float PI = 3.14159265f; +static const float BIG = 1000000.0f; // not infinity but close enough + +static const int MAT_LAMBERT = 0; +static const int MAT_DIELECTRIC = 1; + +struct Ray +{ + float3 origin; + float3 direction; + float3 energy; +}; + +Ray createRay(float3 origin, float3 direction) +{ + Ray ray; + ray.origin = origin; + ray.direction = direction; + ray.energy = float3(1.0f, 1.0f, 1.0f); + return ray; +} + +struct RayHit +{ + float3 position; + float distance; + float3 normal; + float3 albedo; + float3 specular; + float3 emission; +}; + +RayHit createRayHit() +{ + RayHit hit; + hit.position = float3(0.0f, 0.0f, 0.0f); + hit.distance = BIG; + hit.normal = float3(0.0f, 0.0f, 0.0f); + hit.albedo = float3(0.0f, 0.0f, 0.0f); + hit.specular = float3(0.0f, 0.0f, 0.0f); + hit.emission = float3(0.0f, 0.0f, 0.0f); + return hit; +} + +float rand() +{ + float result = frac(sin(_Seed / 100.0f * dot(_Pixel, float2(12.9898f, 78.233f))) * 43758.5453f); + _Seed += 1.0f; + return result; +} + +float sdot(float3 x, float3 y, float f = 1.0f) +{ + return saturate(dot(x, y) * f); +} + +float3x3 getTangentSpace(float3 normal) +{ + // helper vector for the cross product + float3 helper = float3(1, 0, 0); + if (abs(normal.x) > 0.99f) + { + helper = float3(0, 0, 1); + } + + // generate vectors + float3 tangent = normalize(cross(normal, helper)); + float3 binormal = normalize(cross(normal, tangent)); + return float3x3(tangent, binormal, normal); +} + +float3 sampleHemisphere(float3 normal) +{ + // uniformly sample hemisphere direction + float cosTheta = rand(); + float sinTheta = sqrt(max(0.0f, 1.0f - cosTheta * cosTheta)); + float phi = 2 * PI * rand(); + float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); + + // transform direction to world space + return mul(tangentSpaceDir, getTangentSpace(normal)); +} + +float2 randomInUnitDisk() +{ + // pick a random radius and angle then convert to cartesian + float r = rand(); + float theta = rand() * 2 * PI; + return float2(cos(theta), sin(theta)) * r; +} + +Ray createCameraRay(float2 uv) +{ + // transform -1..1 -> 0..1 + uv = uv * 0.5 + 0.5; + uv.x = 1 - uv.x; + + // transform the camera origin to world space + float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz; + + // offset from centre of the lens for depth of field + float2 rd = _CameraLensRadius * randomInUnitDisk(); + float3 offset = _CameraU * rd.x + _CameraV * rd.y; + + origin += offset; + + // invert the perspective projection of the view-space position + float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz; + + // transform the direction from camera to world space and normalize + direction = mul(_CameraToWorld, float4(direction, 0.0f)).xyz; + + direction = _CameraLowerLeftConer + + uv.x * _CameraHorizontal + + uv.y * _CameraVertical + - origin; + + // direction = mul(_CameraInverseProjection, float4(direction, 0)).xyz; + + direction = normalize(direction); + return createRay(origin, direction); +} + + +void intersectSphere(Ray ray, inout RayHit bestHit, Sphere sphere) +{ + // calculate distance along the ray where the sphere is intersected + float3 d = ray.origin - sphere.position; + float p1 = -dot(ray.direction, d); + float p2sqr = p1 * p1 - dot(d, d) + sphere.radius * sphere.radius; + + if (p2sqr < 0) return; + + float p2 = sqrt(p2sqr); + float t = p1 - p2 > 0 ? p1 - p2 : p1 + p2; + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = normalize(bestHit.position - sphere.position); + bestHit.albedo = sphere.albedo; + bestHit.specular = sphere.specular; + bestHit.emission = sphere.emission; + } +} + +float intersectPlane(Ray ray, float3 p, float3 normal) +{ + float denom = dot(normal, ray.direction); + + if (abs(denom) > 0.0001) + { + float t = dot(p - ray.origin, normal) / denom; + if (t >= 0) return t; + } + return -1; +} + +// https://www.iquilezles.org/www/articles/intersectors/intersectors.htm +// cylinder defined in extremes pa and pb, and radius ra +float4 intersectCylinder(Ray ray, float3 pa, float3 pb, float ra, bool inner) +{ + float3 ro = ray.origin; + float3 rd = ray.direction; + + // central axis + float3 ca = pb - pa; + // eye to base + float3 oc = ro - pa; + + // dot products + float caca = dot(ca, ca); + float card = dot(ca, rd); + float caoc = dot(ca, oc); + + // find intersects + float a = caca - card * card; + float b = caca * dot(oc, rd) - caoc * card; + float c = caca * dot(oc, oc) - caoc * caoc - ra * ra * caca; + float h = b * b - a * c; + + if (h < 0.0) return float4(-1, 0, 0, 0); // no intersection + + h = sqrt(h); + h = inner?-h:h; + float t = (-b - h) / a; + + // body + float y = caoc + t * card; + if (y > 0.0 && y < caca) return float4(t, (oc+t*rd - ca*y/caca) / ra); + + // caps + t = ((y < 0.0 ? 0.0 : caca) - caoc) / card; + if (abs(b + a * t) < h) return float4(t, ca * sign(y) / caca); + + return float4(-1, 0, 0, 0); // no intersection +} + +float sdSegment(float3 p, float3 a, float3 b, float r) +{ + float3 pa = p - a, ba = b - a; + float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0); + return length(pa - ba * h) - r; +} + +float opSubtraction(float d1, float d2) { return max(-d1, d2); } + +void intersectTube(Ray ray, inout RayHit bestHit, Tube tube) +{ + // TODO: inner tube + + float height = tube.height; + + float3 axis = normalize(tube.axis); + + float3 pa = tube.position + axis * -height * 0.5; + float3 pb = tube.position + axis * height * 0.5; + + float r_inner = (tube.radius-tube.thickness)/tube.radius; + + // where the ray hit the outer surface + float4 outerHit = intersectCylinder(ray, pa, pb, tube.radius, false); + // outerHit = float4(-1,0,0,0); + // where we hit the inner surface + float4 innerHit = intersectCylinder(ray, pa, pb, tube.radius * r_inner, true); + // float4 innerHit = float4(-1,0,0,0); + // innerHit.yzw *= -1; + + if (outerHit.x < 0 && innerHit.x < 0) return; + + float3 pos_outer = ray.origin + outerHit.x * ray.direction; + float axis_distance = sdSegment(pos_outer, pa, pb, 0); + + float t = bestHit.distance; + + // hit the inner surface + if (innerHit.x > 0 && innerHit.x < bestHit.distance) + { + t = innerHit.x; + + bestHit.normal = normalize(innerHit.yzw); + bestHit.position = ray.origin + t * ray.direction; + bestHit.distance = t; + + bestHit.albedo = float3(.5, .5, .5); + bestHit.emission = float3(0, 0, 0); + bestHit.specular = float3(1, 1, 1); + } + + // hit the outer surface + if (outerHit.x > 0 && outerHit.x < bestHit.distance && axis_distance > tube.radius*r_inner) + { + t = outerHit.x; + + bestHit.normal = normalize(outerHit.yzw); + bestHit.position = ray.origin + t * ray.direction; + bestHit.distance = t; + + bestHit.albedo = float3(.5, .5, .5); + bestHit.emission = float3(0, 0, 0); + bestHit.specular = float3(1, 1, 1); + } +} + +void intersectGroundPlane(inout Ray ray, inout RayHit bestHit) +{ + float3 albedo = _GroundColor; + float3 specular = float3(0, 0, 0); + + // calculate distance along the ray where the ground plane is intersected + float t = -(ray.origin.y - _GroundHeight) / ray.direction.y; + + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = float3(0.0f, 1.0f, 0.0f); + bestHit.albedo = albedo; + bestHit.specular = specular; + bestHit.emission = float3(0, 0, 0); + } +} + +void intersectCeilingPlane(inout Ray ray, inout RayHit bestHit) +{ + float albedo = _SkyColor; + float3 specular = float3(0, 0, 0); + + // ignore plane if the ray is coming from above + if (ray.direction.y < 0) return; + + float t = -(ray.origin.y - _SkyHeight) / ray.direction.y; + float3 p = ray.origin + ray.direction * t; + + if (length(p.xz) < _SkyHoleRadius) return; + + if (t > 0 && t < bestHit.distance) + { + bestHit.distance = t; + bestHit.position = ray.origin + t * ray.direction; + bestHit.normal = float3(0.0f, -1.0f, 0.0f); + bestHit.albedo = albedo; + bestHit.specular = specular; + bestHit.emission = float3(0, 0, 0); + } +} + +void intersectWall(inout Ray ray, inout RayHit bestHit) +{ + // ignore collision if ray's angle is steep or negative + float a = dot(float3(0, 1, 0), ray.direction); + if (a > 0.2 || a < 0) return; + + Sphere sphere; + sphere.radius = BIG - 1; + sphere.albedo = float3(1, 1, 1) * 1.98; + sphere.specular = float3(0, 0, 0); + sphere.emission = float3(0, 0, 0); + sphere.position = float3(0, 0, 0); + + intersectSphere(ray, bestHit, sphere); +} + +RayHit trace(Ray ray) +{ + RayHit bestHit = createRayHit(); + + intersectWall(ray, bestHit); + intersectGroundPlane(ray, bestHit); + intersectCeilingPlane(ray, bestHit); + + uint numSpheres, numTubes, stride; + + // celestial bodies + // _Spheres.GetDimensions(_ActiveSpheres, stride); + for (uint i = 0; i < _ActiveSpheres; i++) + { + intersectSphere(ray, bestHit, _Spheres[i]); + } + + // _Tubes.GetDimensions(numTubes, stride); + for (uint i = 0; i < _ActiveTubes; i++) + { + intersectTube(ray, bestHit, _Tubes[i]); + } + + if (_ActiveUnits > 0) + { + // units + _Units.GetDimensions(numSpheres, stride); + for (uint i = 0; i < _ActiveUnits; i++) + { + Unit unit = _Units[i]; + + float3 color = float3 + (lerp(1, 0, unit.team), + 0, + lerp(0, 1, unit.team)); + + Sphere s; + s.albedo = color; + s.emission = color * unit.selected; + s.specular = float3(0, 0, 0); + s.radius = _UnitRadius; + s.position = unit.position; + + intersectSphere(ray, bestHit, s); + } + } + + return bestHit; +} + +float3 scatter_lambert(inout Ray ray, RayHit hit) +{ + ray.origin = hit.position + hit.normal * 0.001f; + ray.direction = sampleHemisphere(hit.normal); + ray.energy *= 2 * hit.albedo * sdot(hit.normal, ray.direction); + return 0.0f; +} + +float3 shade(inout Ray ray, RayHit hit) +{ + if (any(hit.emission)) return hit.emission; + + if (hit.distance < BIG) + { + return scatter_lambert(ray, hit) + hit.emission; + } + else + { + ray.energy = 0.0f; + + // float theta = acos(ray.direction.y) / -PI; + // float phi = atan2(ray.direction.x, -ray.direction.z) / -PI * 0.5f; + + return _SkyColor; + } +} + +[numthreads(GROUP_SIZE,GROUP_SIZE,1)] +void CSMain(uint3 id : SV_DispatchThreadID) +{ + _Pixel = id.xy; + + // get dimensions of render texture + uint width, height; + Result.GetDimensions(width, height); + + // transform pixel to -1, 1 range + float2 uv = float2(id.xy / float2(width, height) * 2.0f - 1.0f); + uv.x *= _Resolution.x / _Resolution.y; + + int samples = _SamplesPerPixel; + int bounces = _Bounces; + float3 result = float3(0, 0, 0); + + for (int i = 0; i < samples; i++) + { + // get a ray for the uv + Ray ray = createCameraRay(uv); + + // trace and shade + for (int i = 0; i < bounces; i++) + { + RayHit hit = trace(ray); + result += ray.energy * shade(ray, hit); + + if (!any(ray.energy)) break; + } + } + + result /= (float)samples; + + Result[id.xy] = float4(result, 1); +} diff --git a/src/_shader/root/shader.frag b/src/_shader/root/shader.frag new file mode 100644 index 0000000..cadfb1e --- /dev/null +++ b/src/_shader/root/shader.frag @@ -0,0 +1,12 @@ +#version 430 core +out vec4 FragColor; + +in vec3 ourColor; +in vec2 TexCoord; + +uniform sampler2D ourTexture; + +void main() +{ + FragColor = texture(ourTexture, TexCoord); +} diff --git a/src/_shader/root/shader.vert b/src/_shader/root/shader.vert new file mode 100644 index 0000000..0e349b5 --- /dev/null +++ b/src/_shader/root/shader.vert @@ -0,0 +1,14 @@ +#version 430 core +layout (location = 0) in vec3 aPos; // position has attribute position 0 +layout (location = 1) in vec3 aColor; // color has attribute position 1 +layout (location = 2) in vec2 aTexCoord; // texture coordinate + +out vec3 ourColor; +out vec2 TexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; + TexCoord = aTexCoord; +} diff --git a/src/main.c b/src/main.c index 518af9f..9ede8bc 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,4 @@ #include "main.h" - #include "gfx.h" // forward declarations @@ -12,8 +11,8 @@ float time(); int main() { - int width = 800; - int height = 600; + int width = 420; + int height = 420; const char* texPath = "res/tex.png"; // create a window and opengl context @@ -40,9 +39,9 @@ int main() // update uniforms float t = time(); - t = (sin(t)/2.0f)+0.5f; + float sin_t = sin(t); int tLocation = glGetUniformLocation(computeProgram, "t"); - glUniform1f(tLocation, t); + glUniform4f(tLocation, t, (1.0 + sin_t)*0.5, 0.0f, 0.0f); // dispatch compute shader glDispatchCompute((GLuint)width, (GLuint)height, 1); @@ -54,8 +53,9 @@ int main() glUseProgram(quadProgram); glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - glActiveTexture(GL_TEXTURE0); // use computed texture + glActiveTexture(GL_TEXTURE0); // use computed texture glBindTexture(GL_TEXTURE_2D, textureOutput); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // swip swap diff --git a/todo.md b/todo.md index cedc233..8ab5c4a 100644 --- a/todo.md +++ b/todo.md @@ -1,4 +1,24 @@ * [x] basic opengl initialisation -* [x] render a texture to a full-screen quad -* [ ] output image to a file -* [ ] render image with compute shader +* [-] shader pre-processor + * [x] ppp.py + * [ ] shader src and out +* [ ] output frame to a file + * [ ] detect input keydown s + * [ ] get timestamp + * [ ] create and write to file (maybe with `stb_image.h`?) +* [-] render image with compute shader + * [x] render a texture to a full-screen quad + * [x] pass uniforms to texture to animate it + * [ ] ray tracing time + * [ ] acquire randomness + * [ ] acceleration time ! + * [ ] auxiliary textures: g buffer + * [ ] frame blending + * [ ] maybe do some fractals + * [ ] mandelbrot + * [ ] julia + * [ ] trongle +* [ ] command line arguments + * [ ] help + * [ ] window dimensions +* [ ] scene definition