diff --git a/index.html b/index.html index 1deccb1d7c7a780af5e10fd0d8ebfa503d1f01bc..d2a125a6c4a6830a12284dd3fbc28d067f07a5a9 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,6 @@ <script type="module" src="/src/cg/perspectiveDivide.ts"></script> <script type="module" src="/src/cg/walkToVec.ts"></script> <script type="module" src="/src/cg/basis.ts"></script--> - <script type="module" src="/src/cg/raytracing1.ts"></script> + <script type="module" src="/src/cg/raytracing2.ts"></script> </body> </html> diff --git a/src/cg/animation1 frames/render.frame.0.png b/src/cg/animation1 frames/render.frame.0.png new file mode 100644 index 0000000000000000000000000000000000000000..9023bea0ce683eb56f8a9149f59ec7f4e52bcf80 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.0.png differ diff --git a/src/cg/animation1 frames/render.frame.1.png b/src/cg/animation1 frames/render.frame.1.png new file mode 100644 index 0000000000000000000000000000000000000000..601be26d15159e44b40b002fade435a4c1f3f713 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.1.png differ diff --git a/src/cg/animation1 frames/render.frame.10.png b/src/cg/animation1 frames/render.frame.10.png new file mode 100644 index 0000000000000000000000000000000000000000..71574d343c1b746990b6ae5905acea9f1fd3138e Binary files /dev/null and b/src/cg/animation1 frames/render.frame.10.png differ diff --git a/src/cg/animation1 frames/render.frame.2.png b/src/cg/animation1 frames/render.frame.2.png new file mode 100644 index 0000000000000000000000000000000000000000..31d95714c83e04a363b103829f200131feed2055 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.2.png differ diff --git a/src/cg/animation1 frames/render.frame.4.png b/src/cg/animation1 frames/render.frame.4.png new file mode 100644 index 0000000000000000000000000000000000000000..cd4c48b9649df640d173da01bd606b19477b12c3 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.4.png differ diff --git a/src/cg/animation1 frames/render.frame.5.png b/src/cg/animation1 frames/render.frame.5.png new file mode 100644 index 0000000000000000000000000000000000000000..4d95157b2786dd0940b2157907367580d10848f9 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.5.png differ diff --git a/src/cg/animation1 frames/render.frame.6.png b/src/cg/animation1 frames/render.frame.6.png new file mode 100644 index 0000000000000000000000000000000000000000..c8166ae26ad816c30733c678f101af0d6f2bfc0d Binary files /dev/null and b/src/cg/animation1 frames/render.frame.6.png differ diff --git a/src/cg/animation1 frames/render.frame.7.png b/src/cg/animation1 frames/render.frame.7.png new file mode 100644 index 0000000000000000000000000000000000000000..3f996840ce41307bd287e561c6001b6844abb5ec Binary files /dev/null and b/src/cg/animation1 frames/render.frame.7.png differ diff --git a/src/cg/animation1 frames/render.frame.8.png b/src/cg/animation1 frames/render.frame.8.png new file mode 100644 index 0000000000000000000000000000000000000000..f294eaa6ad42e03a33ead3ad9a7b8a3e0d44f8d3 Binary files /dev/null and b/src/cg/animation1 frames/render.frame.8.png differ diff --git a/src/cg/animation1 frames/render.frame.9.png b/src/cg/animation1 frames/render.frame.9.png new file mode 100644 index 0000000000000000000000000000000000000000..33829f286078b48109a8275d78c56148cf7bf47a Binary files /dev/null and b/src/cg/animation1 frames/render.frame.9.png differ diff --git a/src/cg/animation1.ts b/src/cg/animation1.ts new file mode 100644 index 0000000000000000000000000000000000000000..9815ef8b9a23fe5a53fa4b15fa7e51893145bbb6 --- /dev/null +++ b/src/cg/animation1.ts @@ -0,0 +1,53 @@ +/*import Framebuffer from "./framebuffer"; + +const width = 100 +const height = 100 +const framebuffer = new Framebuffer(width, height); + +let currentFrame = 0 + +// Step from 0 to 1 +for (let i = 0; i <= 1; i += 0.1) { + framebuffer.clear() + + // remap 0 1 to -1 1 + const remapped = i * 2 - 1; console.info(remapped) + + // fit remapped i into actual values + // and use absolute value from remapped + const val = Math.round((height-1) * Math.abs(remapped)); + + framebuffer.draw(50, val, [255, 0, 0]) + + framebuffer.update(); + framebuffer.save("frame." + ++currentFrame) +}*/ + +import { easeOutBounce } from "./helper"; +import Framebuffer from "./framebuffer"; + +const width = 100 +const height = 100 +const framebuffer = new Framebuffer(width, height); + +let currentFrame = 0 + + +for (let i = 0; i <= 1; i += 0.1) { + framebuffer.clear(); + + // remap 0 1 to -1 1 + //const remapped = i * 2 - 1; console.info(remapped) + + // fit remapped i into actual values + // and use absolute value from remapped + //const val = Math.round((height-1) * Math.abs(remapped)); + + const c = height * easeOutBounce(i) + framebuffer.draw(width * i, Math.floor(c), [255, 0, 0]) + + framebuffer.update(); + framebuffer.save("frame." + ++currentFrame) + +} + diff --git a/src/cg/framebuffer.ts b/src/cg/framebuffer.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb7e2eddd5b8995b5ff7d8590f57755d05cf3668 --- /dev/null +++ b/src/cg/framebuffer.ts @@ -0,0 +1,164 @@ +export type Color3 = [number, number, number]; +export type Vector3 = [number, number, number]; + +export interface ICoord2D { + x: number, + y: number +} + +export default class Framebuffer { + private canvas: HTMLCanvasElement + private ctx + private buffer + private color: Color3 + private saveLink: HTMLAnchorElement + private saveButton: HTMLButtonElement + private logField: HTMLUListElement + + constructor(width = 100, height = 100) { + + // Remove stored images + for (var key in sessionStorage) { + if (key.indexOf("render.") === 0) { + sessionStorage.removeItem(key); + } + } + + this.color = [0, 0, 0] + const appEl = document.getElementById("app"); + appEl.style.display = "flex"; + appEl.style.height = "100vh"; + appEl.style.width = "100vw"; + appEl.style.flexDirection = "column"; + appEl.style.justifyContent = "center"; + appEl.style.alignItems = "center"; + + this.canvas = document.createElement("canvas"); + this.canvas.id = "framebuffer"; + this.canvas.width = width; + this.canvas.height = height; + this.canvas.style.borderWidth = "2px" + + const header = document.createElement("h2"); + header.innerText = "framebuffer: " + width + "x" + height; + appEl.appendChild(header); + appEl.appendChild(this.canvas); + + this.ctx = this.canvas.getContext("2d"); + + if (!this.ctx) { + console.warn("No canvas 2d context!") + return + } + + // Setup save link + this.saveLink = document.createElement("a"); + this.saveLink.id = "save"; + + this.saveLink.style.display = "none"; + appEl.appendChild(this.saveLink); + + // Save button + this.saveButton = document.createElement("button"); + this.saveButton.innerText = "Download Frames"; + appEl.appendChild(this.saveButton); + + this.saveButton.addEventListener("click", () => this.download()) + + // Log + this.logField = document.createElement("ul"); + this.logField.id = "log"; + appEl.appendChild(this.logField); + + // Get pixel data + this.buffer = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + + } + + + draw(x: number, y: number, color: Color3 = [80, 80, 80], parameters?: {}): void { + + const defaults = { + } + + const params = { ...defaults, ...parameters } + this.color = color; + + if (!this.buffer) return; + + // The first (red) component for this pixel + var offset = 4 * x + (this.buffer.width * 4) * y; + + const redIndex = offset; + const greenIndex = redIndex + 1; + const blueIndex = greenIndex + 1; + const alphaIndex = blueIndex + 1; + + this.buffer.data[redIndex] = this.color[0]; + this.buffer.data[greenIndex] = this.color[1]; + this.buffer.data[blueIndex] = this.color[2]; + this.buffer.data[alphaIndex] = 255; + } + + + update() { + if (!this.ctx) return; + + this.ctx.putImageData(this.buffer, 0, 0); + } + + save(name: string) { + sessionStorage.setItem("render." + name, this.canvas.toDataURL("image/png").replace("image/png", "image/octet-stream")); + + } + + download() { + for (var key in sessionStorage) { + if (key.indexOf("render.") === 0) { + sessionStorage.getItem(key) + console.info("Downloading", key) + this.saveLink.setAttribute('download', key + '.png'); + this.saveLink.setAttribute('href', sessionStorage[key]); + this.saveLink.click(); + } + } + } + + log(content: string) { + this.logField.innerText = content; + } + + clear() { + this.ctx.reset(); + this.buffer = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); + this.update(); + } + + // Type definition for a 3D vector + + + // Function to convert from raster space to screen space + rasterToScreen(x: number, y: number, width: number, height: number, distance: number): Vector3 { + // Convert from raster space to normalized device coordinates (NDC) + const ndcX = (x + 0.5) / width; + const ndcY = (y + 0.5) / height; + + // Convert from NDC to screen space + const screenX = 2 * ndcX - 1; + const screenY = 1 - 2 * ndcY; + + // Return the screen space coordinates as a Vector3 + return [screenX, screenY, distance]; + } + + /* Example usage: + const rasterX = 50; + const rasterY = 50; + const canvasWidth = 100; + const canvasHeight = 100; + + const screenCoords = this.rasterToScreen(this.rasterX, this.rasterY, this.canvasWidth, this.canvasHeight); + console.log(screenCoords); // Output: [0, 0, 0]*/ + + +} \ No newline at end of file diff --git a/src/cg/helper.ts b/src/cg/helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f146336d1b2dd0dfb9142b5bd5cfda8cfd6f54d --- /dev/null +++ b/src/cg/helper.ts @@ -0,0 +1,14 @@ +export function easeOutBounce(x: number): number { + const n1 = 7.5625; + const d1 = 2.75; + + if (x < 1 / d1) { + return n1 * x * x; + } else if (x < 2 / d1) { + return n1 * (x -= 1.5 / d1) * x + 0.75; + } else if (x < 2.5 / d1) { + return n1 * (x -= 2.25 / d1) * x + 0.9375; + } else { + return n1 * (x -= 2.625 / d1) * x + 0.984375; + } + } \ No newline at end of file diff --git a/src/cg/raytracing1.ts b/src/cg/raytracing1.ts index 97636d3ec4c93038a9813b1ff662819e8d4fddcb..9b44583d99891c7fdf06226d45d70f341078bec4 100644 --- a/src/cg/raytracing1.ts +++ b/src/cg/raytracing1.ts @@ -4,8 +4,8 @@ import { vecDotProduct, vecMultiplyScalar, vecSubtract } from "./utils"; const pg = new Playground(); const sphere = { - position: [0, .5, -3], //C - Center of sphere - radius: 0.5 + position: [0, .5, -4], //C - Center of sphere + radius: 2 } pg.visCamera(-1); pg.gridXZ() @@ -14,7 +14,7 @@ const o = [0, 0, 0]; //O - Origin const co = vecSubtract(o, sphere.position) // O - C const rsq = sphere.radius * sphere.radius; -const step = 1 / 8 +const step = 1/8 for (let yCoord = -1; yCoord <= 1; yCoord += step) { diff --git a/src/cg/raytracing2.ts b/src/cg/raytracing2.ts new file mode 100644 index 0000000000000000000000000000000000000000..4fec61b39b6946645072af844d217227607852df --- /dev/null +++ b/src/cg/raytracing2.ts @@ -0,0 +1,61 @@ +import Framebuffer from "./framebuffer"; +import { ISphere, Vec3, raySphereIntersect } from "./utils"; + +const width = 200 +const height = 200 +const framebuffer = new Framebuffer(width, height); +const imagePlaneDist = -1 + +const tNear = 1; +const tFar = 1000; + +const spheres = [ + { + position: [0, 0, -3],// <--- + radius: 2,// <--- + color: [255,0,0]// <--- + }, + { + position: [3,2,-3],// <--- + radius: 2,// <--- + color: [0,255,0]// <--- + }, + //... add more spheres if you like +] + +const o:Vec3 = [0,0,0]//<--- the camera/viewer origin + +// Loop over framebuffer pixels +for (let x = 0; x <= width; x++) { + for (let y = 0; y <= height; y++) { + + const v = framebuffer.rasterToScreen(x, y, width, height, imagePlaneDist)// <--- convert raster to screen space + + let closestSphere = null; + let closestIntersection = 9999; + + for (let i = 0; i < spheres.length; i++) { + + const [t1, t2] = raySphereIntersect(v, o, spheres[i] as ISphere)// <--- Calculate intersections + + if (t1 < closestIntersection && tNear < t1 && t1 < tFar) { + closestIntersection = t1; + closestSphere = spheres[i] + } + + if (t2 < closestIntersection && tNear < t2 && t2 < tFar) { + closestIntersection = t2; + closestSphere = spheres[i]; + } + } + if (!closestSphere) { + framebuffer.draw(x,y,[0,0,50]) // <--- Draw a background color + } else { + framebuffer.draw(x,y,closestSphere.color) // <--- Draw color of closest sphere + } + + } + +} +framebuffer.update(); +framebuffer.save("spheres."); \ No newline at end of file diff --git a/src/cg/utils.ts b/src/cg/utils.ts index 3441189d55ab570eff9a14df0e15637362918b24..1a4beb8a8ab71bc9f2674b95c9b9f9c0b9f372f6 100644 --- a/src/cg/utils.ts +++ b/src/cg/utils.ts @@ -167,6 +167,12 @@ export type Matrix4 = [ number, number, number, number ]; +export interface ISphere{ + position: Array<number>, + radius: number, + color?: Array<number> +} + export function multVec3Matrix4(v: Vec3, m: Matrix4):Vec3 { const v4: Vec4 = [v[0], v[1], v[2], 1] @@ -206,4 +212,22 @@ export function matrix3ToMatrix4(m: Matrix3): Matrix4 { ] return m4; } + +export function raySphereIntersect(v: Vec3, o: Vec3, sphere: ISphere): [number, number] { + const ov = vecSubtract(v, o); // V - O + + const co = vecSubtract(o, sphere.position) // O - C + const rsq = sphere.radius * sphere.radius; + + const a = vecDotProduct(ov, ov); // a = (V - O)*(V _ O) + const b = 2* vecDotProduct(ov, co); // b = 2 * ((V - O)*(O - C)) + const c = vecDotProduct(co, co) - rsq; // c = ((O - C)*(O - C))-r^2 + + const discriminant = (b*b) - (4*a*c); // b^2 - 4*a*c + + const t1 = (-b + Math.sqrt(discriminant)) / (2 * a) + const t2 = (-b - Math.sqrt(discriminant)) / (2 * a) + + return [t1, t2]; +} // \ No newline at end of file