diff --git a/README.md b/README.md index 27b5336..74d5468 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,22 @@ # HW 6: Ray marching and SDFs -## Goal -In this assignment, you will be implementing SDF operators on various primitives and use a ray marcher to render them. Ray marching is a technique for rendering implicit surfaces where the ray-primitive intersection equation cannot be solved analytically. - -**Warning**: this assignment diverges significantly from marching cubes, so switching options midway can be costly for your time and effort. - -## Base code framework - -We have provided a preview scene, and a toggle for the ray marcher rendering. When you correctly implement the ray marcher, the image should match the preview scene containing the simple geometry. Your ray marching calculation should be performed in the fragment shader. - -### Ray Marcher (25 pts) - -The ray marcher should generate a ray direction, and march through the scene using the distances computed from sphere tracing. - -**Note**: Your scene won't be rendered until you have implemented the SDFs for primitives below. - -- Generate Rays (15 pts): for each fragment inside the fragment shader, compute a ray direction for the ray marcher -- Sphere Tracing (10 pts): compute the nearest distance from the scene SDFs and update the ray marching's step size. - -### SDF (50 pts) -##### Implement primitive SDFs (15pts): -These are simple primitives with well-defined SDFs. We encourage trying other SDFs not listed here, they are interesting! - - Sphere (3pts) - - Box (3pts) - - Cone (3pts) - - Torus (3pts) - - Cylinder (3pts) - -##### Useful Operators (15pts) -To create constructive geometry, and interesting shapes (such as holes, bumps, etc.), implement the following operators to combine your primitive SDFs. - - Intersection (2pts) - - Subtraction (3pts) - - Union (2pts) - - Transformation (8pts) - - translation and scaling -##### Compute normals based on gradient (15 pts) - -Compute the normals to use for shading your surface. -- Read Chapter 13 of [Morgan McGuire's notes](http://graphics.cs.williams.edu/courses/cs371/f14/reading/implicit.pdf) -##### Material (5pts) -Implement a simple Lambert material. Additional materials can earn extra points. - -### Custom Scene (25 pts) -##### Create a mechanical device or a scene of your choice using all operators - - intersection, subtraction, union, transformation (20pts) -##### Animate the scene (5pts) -Use time as an input to some of your functions to animate your scene! - -## Extra credits (Up to 30 pts) -- Implement SDF for [Mandelbulb](https://www.shadertoy.com/view/XsXXWS) (10pts) - - You need to implement naive raymarching (not sphere tracing) to get this to work -- Lighting effects - - Soft shadowing using secondary rays (5pts) - - Ambient occlusion (10pts) -- Additional materials besides Lambert. (5pts each) -- Additional SDFs besides the listed primitive. (5pts each) - -## Resources -http://graphics.cs.williams.edu/courses/cs371/f14/reading/implicit.pdf - -## Submission -- Update `README.md` to contain a solid description of your project -- Publish your project to gh-pages. `npm run deploy`. It should now be visible at http://username.github.io/repo-name -- Create a [pull request](https://help.github.com/articles/creating-a-pull-request/) to this repository, and in the comment, include a link to your published project. -- Submit the link to your pull request on Canvas. - -## Deploy -- `npm run build` -- Add and commit all changes -- `npm run deploy` -- If you're having problems with assets not linking correctly, make sure you wrap you're filepaths in `require()`. This will make the bundler package and your static assets as well. So, instead of `loadTexture('./images/thing.bmp')`, do `loadTexture(require('./images/thing.bmp'))`. \ No newline at end of file +## Project Description +Implemented a ray marcher with sphere tracing to render various primitives using SDFs and additional operators. + +Implemented SDFs: + - sphere + - box + - cone + - torus + - cylinder + - triangular prism + - hexagonal prism + +Additional operators: + - intersection + - subtraction + - union + - scaling + - computed normals based on gradients + + The original RayMarching option in the program now renders a torus with Lambertian shading. The Machine option renders a combination of a few SDFs and union/subtraction operators. I had trouble getting the ray marcher to work properly for awhile, so I couldn't spend as much time playing around with the SDFs to make something cool. The camera controls also don't work anymore I don't really know why lol \ No newline at end of file diff --git a/src/glsl/machine-frag.glsl b/src/glsl/machine-frag.glsl new file mode 100644 index 0000000..5d34116 --- /dev/null +++ b/src/glsl/machine-frag.glsl @@ -0,0 +1,157 @@ + +#define MAX_GEOMETRY_COUNT 100 +#ifdef GL_ES +precision highp float; +#endif + +/* This is how I'm packing the data +struct geometry_t { + vec3 position; + float type; +}; +*/ +uniform vec4 u_buffer[MAX_GEOMETRY_COUNT]; +uniform int u_count; +uniform float u_cameraFOV; +uniform mat4 u_cameraTransf; +uniform mat4 u_cameraProjectionInv; +uniform mat4 u_cameraViewInv; +uniform vec3 u_cameraPosition; +uniform float u_alpha; +uniform float u_aspect; +uniform float u_farClip; + +varying vec2 f_uv; +varying vec3 f_position; + +//------------------------------------------------------------------------- +float sphereSDF(vec3 p) { + return length(p) - 1.5; +} + +float cylinderSDF(vec3 p, vec3 c) { + return length(p.xz - c.xy) - c.z; +} + +float torusSDF(vec3 p, vec2 t) { + vec2 q = vec2(length(p.xz) - t.x, p.y); + return length(q) - t.y; +} + +float boxSDF(vec3 p, vec3 b) { + vec3 d = abs(p) - b; + return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d,0.0)); +} + +float coneSDF(vec3 p, vec2 c) { + // c normalized + float q = length(p.xy); + return dot(c, vec2(q, p.z)); +} + +float triPrisSDF(vec3 p, vec2 h) +{ + vec3 q = abs(p); + return max(q.z - h.y, max(q.x * 0.866025 + p.y * 0.5 , -p.y) -h.x * 0.5); +} + +float hexPrisSDF( vec3 p, vec2 h ) +{ + vec3 q = abs(p); + return max(q.z - h.y, max((q.x * 0.866025 + q.y * 0.5), q.y) - h.x); +} + +// operators +float opIntersect(float d1, float d2) { + return max(d1, d2); +} + +float opUnion(float d1, float d2) { + return min(d1, d2); +} + +float opSubtract(float d1, float d2) { + return max(-d1, d2); +} + +// vec3 opTranslate(vec3 p, mat4 m) { +// vec3 q = inverse(m) * p; +// return sceneSDF(q); +// } + +// total scene SDF +float sceneSDF(vec3 point) { + float dist1 = triPrisSDF(point, vec2(0.5,3.0)); + float dist2 = opUnion(boxSDF(point, vec3(0.5,2.0,0.5)), sphereSDF(point)); + float dist = opSubtract(dist1, dist2); + return dist; +} + +float opScale(vec3 p, float s) { + return sceneSDF(p/s) * s; +} + +vec3 estNormal(vec3 p) { + float EPSILON = 0.0001; + vec3 toRet = normalize(vec3( + sceneSDF(vec3(p.x + EPSILON, p.y, p.z)) - sceneSDF(vec3(p.x - EPSILON, p.y, p.z)), + sceneSDF(vec3(p.x, p.y + EPSILON, p.z)) - sceneSDF(vec3(p.x, p.y - EPSILON, p.z)), + sceneSDF(vec3(p.x, p.y, p.z + EPSILON)) - sceneSDF(vec3(p.x, p.y, p.z - EPSILON)) + )); + return toRet; +} +//----------------------------------------------------------------------------------------- +void main() { + + // RAYMARCHING + float t = 0.1; + float EPSILON = 0.001; + float end = 100.0; + vec3 color = vec3(0.5,0.5,0.5); + + // uv coordinates in NDC + float sx = (2.0 * f_uv.x) - 1.0; + float sy = 1.0 - (2.0 * f_uv.y); + + vec3 eye = u_cameraPosition; + vec3 F = normalize(vec3(0.0,0.0,0.0) - eye); + vec3 R = normalize(cross(F, vec3(0.0, 1.0, 0.0))); + vec3 U = normalize(cross(R, F)); + + // screen point to world point + vec3 ref = eye + t * F; + vec3 len = abs(ref - eye); + vec3 V = (U * len * u_alpha); + vec3 H = (R * len * u_aspect * u_alpha); + vec3 p = ref + (sx * H) + (sy * V); + + // get a ray from world point + vec3 ray_direction = normalize(p - eye); + vec3 point = eye + t * ray_direction; + + for (int i = 0; i < 100; i++) { + point = eye + t * ray_direction; + float dist1 = triPrisSDF(point, vec2(0.5,3.0)); + float dist2 = opUnion(boxSDF(point, vec3(0.5,2.0,0.5)),sphereSDF(point)); + float dist = opSubtract(dist1, dist2); + if (dist < EPSILON) { + // then you're inside the scene surface + // color the fragment + color = vec3(0.5,0.5,0.7); + } + t += dist; + if (t >= end) { + // gone too far, end + break; + } + } + + // some lame lambertian lighting, light source from the camera + vec3 normal = estNormal(point); + float d = clamp(dot(normal, normalize(u_cameraPosition - f_position)), 0.0, 1.0); + + // color the output + gl_FragColor = vec4(d * color * 1.2, 1); // frag with Lambertian shading + // gl_FragColor = vec4(color, 1); + +} \ No newline at end of file diff --git a/src/glsl/pass-vert.glsl b/src/glsl/pass-vert.glsl index 748eb5c..1b9faef 100644 --- a/src/glsl/pass-vert.glsl +++ b/src/glsl/pass-vert.glsl @@ -1,5 +1,8 @@ varying vec2 f_uv; +varying vec3 f_position; + void main() { f_uv = uv; + f_position = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); } \ No newline at end of file diff --git a/src/glsl/rayMarch-frag.glsl b/src/glsl/rayMarch-frag.glsl index 7c01941..c872904 100644 --- a/src/glsl/rayMarch-frag.glsl +++ b/src/glsl/rayMarch-frag.glsl @@ -1,5 +1,8 @@ #define MAX_GEOMETRY_COUNT 100 +#ifdef GL_ES +precision highp float; +#endif /* This is how I'm packing the data struct geometry_t { @@ -9,16 +12,146 @@ struct geometry_t { */ uniform vec4 u_buffer[MAX_GEOMETRY_COUNT]; uniform int u_count; +uniform float u_cameraFOV; +uniform mat4 u_cameraTransf; +uniform mat4 u_cameraProjectionInv; +uniform mat4 u_cameraViewInv; +uniform vec3 u_cameraPosition; +uniform float u_alpha; +uniform float u_aspect; +uniform float u_farClip; varying vec2 f_uv; +varying vec3 f_position; +//------------------------------------------------------------------------- +float sphereSDF(vec3 p) { + return length(p) - 1.0; +} + +float cylinderSDF(vec3 p, vec3 c) { + return length(p.xz - c.xy) - c.z; +} + +float torusSDF(vec3 p, vec2 t) { + vec2 q = vec2(length(p.xz) - t.x, p.y); + return length(q) - t.y; +} + +float boxSDF(vec3 p, vec3 b) { + vec3 d = abs(p) - b; + return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d,0.0)); +} + +float coneSDF(vec3 p, vec2 c) { + // c normalized + float q = length(p.xy); + return dot(c, vec2(q, p.z)); +} + +float triPrisSDF(vec3 p, vec2 h) +{ + vec3 q = abs(p); + return max(q.z - h.y, max(q.x * 0.866025 + p.y * 0.5 , -p.y) -h.x * 0.5); +} + +float hexPrisSDF( vec3 p, vec2 h ) +{ + vec3 q = abs(p); + return max(q.z - h.y, max((q.x * 0.866025 + q.y * 0.5), q.y) - h.x); +} + +// total scene SDF +float sceneSDF(vec3 p) { + return torusSDF(p, vec2(1.0,1.0)); +} + +// operators +float opIntersect(float d1, float d2) { + return max(d1, d2); +} + +float opUnion(float d1, float d2) { + return min(d1, d2); +} + +float opSubtract(float d1, float d2) { + return max(-d1, d2); +} + +// vec3 opTranslate(vec3 p, mat4 m) { +// vec3 q = invert(m) * p; +// return sceneSDF(q); +// } + +float opScale(vec3 p, float s) { + return sceneSDF(p/s) * s; +} + +vec3 estNormal(vec3 p) { + float EPSILON = 0.0001; + vec3 toRet = normalize(vec3( + sceneSDF(vec3(p.x + EPSILON, p.y, p.z)) - sceneSDF(vec3(p.x - EPSILON, p.y, p.z)), + sceneSDF(vec3(p.x, p.y + EPSILON, p.z)) - sceneSDF(vec3(p.x, p.y - EPSILON, p.z)), + sceneSDF(vec3(p.x, p.y, p.z + EPSILON)) - sceneSDF(vec3(p.x, p.y, p.z - EPSILON)) + )); + return toRet; +} +//----------------------------------------------------------------------------------------- void main() { - float t; - for (int i = 0; i < MAX_GEOMETRY_COUNT; ++i) { - if (i >= u_count) { - break; - } - } - - gl_FragColor = vec4(f_uv, 0, 1); + // trying to get a sphere to show up... sad + float t = 0.1; + float EPSILON = 0.001; + float end = 100.0; + vec3 color = vec3(0.5,0.5,0.5); + + // uv coordinates in NDC + float sx = (2.0 * f_uv.x) - 1.0; + float sy = 1.0 - (2.0 * f_uv.y); + + vec3 eye = u_cameraPosition; + vec3 F = normalize(vec3(0.0,0.0,0.0) - eye); + vec3 R = normalize(cross(F, vec3(0.0, 1.0, 0.0))); + vec3 U = normalize(cross(R, F)); + + // screen point to world point + vec3 ref = eye + t * F; + vec3 len = abs(ref - eye); + vec3 V = (U * len * u_alpha); + vec3 H = (R * len * u_aspect * u_alpha); + vec3 p = ref + (sx * H) + (sy * V); + + // get a ray from world point + vec3 ray_direction = normalize(p - eye); + vec3 point = eye + t * ray_direction; + + // once more, let us try a different way once more ) : + // vec4 p = u_cameraViewInv * u_cameraProjectionInv * vec4(sx, sy, 0.5, 1.0); + // vec3 eye = u_cameraPosition; + // vec3 ray_direction = normalize(p.xyz - eye); + // vec3 point = eye + t * ray_direction; + + for (int i = 0; i < 100; i++) { + point = eye + t * ray_direction; + float dist = torusSDF(point, vec2(1.0,1.0)); + if (dist < EPSILON) { + // then you're inside the scene surface + // color the fragment + color = vec3(0.5,0.5,0.7); + } + t += dist; + if (t >= end) { + // gone too far, end + break; + } + } + + // some lame lambertian lighting, with a light source from the camera + vec3 normal = estNormal(point); + float d = clamp(dot(normal, normalize(u_cameraPosition - f_position)), 0.0, 1.0); + + // color the output + gl_FragColor = vec4(d * color * 1.2, 1); // frag with Lambertian shading + // gl_FragColor = vec4(color, 1); + } \ No newline at end of file diff --git a/src/machine.js b/src/machine.js new file mode 100644 index 0000000..575d5aa --- /dev/null +++ b/src/machine.js @@ -0,0 +1,78 @@ +const THREE = require('three'); +const EffectComposer = require('three-effectcomposer')(THREE) + +import {PROXY_BUFFER_SIZE} from './proxy_geometry' + +export default function Machine(renderer, scene, camera) { + var composer = new EffectComposer(renderer); + + var m = new THREE.Matrix4(); + var inverseProj = m.getInverse(camera.projectionMatrix); + var n = new THREE.Matrix4(); + var inverseView = m.getInverse(camera.matrixWorldInverse); + var alphaTan = Math.tan((camera.fov / 2.0) * 180.0 / Math.PI); + var time = new Date(); + + var shaderPass = new EffectComposer.ShaderPass({ + uniforms: { + u_buffer: { + type: '4fv', + value: undefined + }, + u_count: { + type: 'i', + value: 0 + }, + u_cameraFOV: { + type: 'f', + value: camera.fov + }, + u_aspect: { + type: 'f', + value: window.innerWidth / window.innerHeight * 1.0 + }, + u_cameraTransf: { + type: '4m', + value: camera.matrix + }, + u_cameraProjectionInv: { + type: '4m', + value: inverseProj + }, + u_cameraViewInv: { + type: '4m', + value: inverseView + }, + u_cameraPosition: { + type: '3fv', + value: camera.position + }, + u_alpha: { + type: 'f', + value: alphaTan + }, + u_farClip: { + type: 'f', + value: camera.far + }, + u_time: { + type: 'f', + value: time + } + }, + vertexShader: require('./glsl/pass-vert.glsl'), + fragmentShader: require('./glsl/machine-frag.glsl') + }); + shaderPass.renderToScreen = true; + composer.addPass(shaderPass); + + return { + render: function(buffer) { + shaderPass.material.uniforms.u_buffer.value = buffer; + shaderPass.material.uniforms.u_count.value = buffer.length / PROXY_BUFFER_SIZE; + composer.render(); + // update camera + camera.updateProjectionMatrix(); + } + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index ade21ee..944f1d8 100644 --- a/src/main.js +++ b/src/main.js @@ -7,6 +7,7 @@ import DAT from 'dat-gui' import Stats from 'stats-js' import ProxyGeometry, {ProxyMaterial} from './proxy_geometry' import RayMarcher from './rayMarching' +import Machine from './machine' var BoxGeometry = new THREE.BoxGeometry(1, 1, 1); var SphereGeometry = new THREE.SphereGeometry(1, 32, 32); @@ -28,6 +29,9 @@ window.addEventListener('load', function() { renderer.setClearColor(0x999999, 1.0); document.body.appendChild(renderer.domElement); + // console.log("width: " + window.innerWidth); + // console.log("height: " + window.innerHeight); + var controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.enableZoom = true; @@ -47,7 +51,7 @@ window.addEventListener('load', function() { strategy: 'Proxy Geometry' } - gui.add(options, 'strategy', ['Proxy Geometry', 'Ray Marching']); + gui.add(options, 'strategy', ['Proxy Geometry', 'Ray Marching', 'Machine']); scene.add(new THREE.AxisHelper(20)); scene.add(new THREE.DirectionalLight(0xffffff, 1)); @@ -67,11 +71,13 @@ window.addEventListener('load', function() { scene.add(proxyGeometry.group); - camera.position.set(5, 10, 15); + // WHY ISNT CAMERA POSITION UPDATING???? + camera.position.set(10, 10, 10); camera.lookAt(new THREE.Vector3(0,0,0)); controls.target.set(0,0,0); var rayMarcher = new RayMarcher(renderer, scene, camera); + var machine = new Machine(renderer, scene, camera); (function tick() { controls.update(); @@ -81,7 +87,10 @@ window.addEventListener('load', function() { renderer.render(scene, camera); } else if (options.strategy === 'Ray Marching') { rayMarcher.render(proxyGeometry.buffer); + } else if (options.strategy === 'Machine') { + machine.render(scene, camera); } + camera.updateProjectionMatrix stats.end(); requestAnimationFrame(tick); })(); diff --git a/src/rayMarching.js b/src/rayMarching.js index 03c3680..9b78933 100644 --- a/src/rayMarching.js +++ b/src/rayMarching.js @@ -5,6 +5,14 @@ import {PROXY_BUFFER_SIZE} from './proxy_geometry' export default function RayMarcher(renderer, scene, camera) { var composer = new EffectComposer(renderer); + + var m = new THREE.Matrix4(); + var inverseProj = m.getInverse(camera.projectionMatrix); + var n = new THREE.Matrix4(); + var inverseView = m.getInverse(camera.matrixWorldInverse); + var alphaTan = Math.tan((camera.fov / 2.0) * 180.0 / Math.PI); + var time = new Date(); + var shaderPass = new EffectComposer.ShaderPass({ uniforms: { u_buffer: { @@ -15,6 +23,42 @@ export default function RayMarcher(renderer, scene, camera) { type: 'i', value: 0 }, + u_cameraFOV: { + type: 'f', + value: camera.fov + }, + u_aspect: { + type: 'f', + value: window.innerWidth / window.innerHeight * 1.0 + }, + u_cameraTransf: { + type: '4m', + value: camera.matrix + }, + u_cameraProjectionInv: { + type: '4m', + value: inverseProj + }, + u_cameraViewInv: { + type: '4m', + value: inverseView + }, + u_cameraPosition: { + type: '3fv', + value: camera.position + }, + u_alpha: { + type: 'f', + value: alphaTan + }, + u_farClip: { + type: 'f', + value: camera.far + }, + u_time: { + type: 'f', + value: time + } }, vertexShader: require('./glsl/pass-vert.glsl'), fragmentShader: require('./glsl/rayMarch-frag.glsl') @@ -26,8 +70,9 @@ export default function RayMarcher(renderer, scene, camera) { render: function(buffer) { shaderPass.material.uniforms.u_buffer.value = buffer; shaderPass.material.uniforms.u_count.value = buffer.length / PROXY_BUFFER_SIZE; - composer.render(); + // update camera + camera.updateProjectionMatrix(); } } } \ No newline at end of file