From e4f4bf6760d76371389d6f8793f9b40fb146931d Mon Sep 17 00:00:00 2001 From: fdai7303 Date: Thu, 25 May 2023 08:03:42 +0200 Subject: [PATCH] added tasks 5 & 6 --- .vscode/settings.json | 4 + dist/manyspheres-finished.png | Bin 0 -> 2596 bytes dist/manyspheres.html | 39 +++++ dist/phong-finished.png | Bin 0 -> 24758 bytes dist/phong.html | 43 ++++++ dist/raytracing-finished.png | Bin 0 -> 3018 bytes dist/raytracing.html | 39 +++++ src/04/bresenhamsimple.ts | 19 +++ src/04/ddasimple.ts | 12 ++ src/05/camera.ts | 32 ++++ src/05/intersection.ts | 39 +++++ src/05/manyspheres.ts | 28 ++++ src/05/ray.ts | 38 +++++ src/05/raytracing.ts | 28 ++++ src/05/setup-manyspheres.ts | 51 +++++++ src/05/setup-raytracing.ts | 38 +++++ src/05/sphere.ts | 49 ++++++ src/05/vector.ts | 256 +++++++++++++++++++++++++++++++ src/06/phong.ts | 35 +++++ src/06/raytracing.ts | 32 ++++ src/06/setup-phong.ts | 62 ++++++++ test/ray-spec.ts | 50 +++++++ test/sphere-spec.ts | 78 ++++++++++ test/vector-spec.ts | 275 ++++++++++++++++++++++++++++++++++ 24 files changed, 1247 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 dist/manyspheres-finished.png create mode 100644 dist/manyspheres.html create mode 100644 dist/phong-finished.png create mode 100644 dist/phong.html create mode 100644 dist/raytracing-finished.png create mode 100644 dist/raytracing.html create mode 100644 src/05/camera.ts create mode 100644 src/05/intersection.ts create mode 100644 src/05/manyspheres.ts create mode 100644 src/05/ray.ts create mode 100644 src/05/raytracing.ts create mode 100644 src/05/setup-manyspheres.ts create mode 100644 src/05/setup-raytracing.ts create mode 100644 src/05/sphere.ts create mode 100644 src/05/vector.ts create mode 100644 src/06/phong.ts create mode 100644 src/06/raytracing.ts create mode 100644 src/06/setup-phong.ts create mode 100644 test/ray-spec.ts create mode 100644 test/sphere-spec.ts create mode 100644 test/vector-spec.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9a5620e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "mochaExplorer.require": "ts-node/register", + "mochaExplorer.files": "test/*.ts", +} \ No newline at end of file diff --git a/dist/manyspheres-finished.png b/dist/manyspheres-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..d6a2bcf62021c666b3d6910a2db11174acacd960 GIT binary patch literal 2596 zcmeHIYfuwc6u#S3)<=_7E7EZ=3q>qNfufa)NWhj?q5>j8lt*wJM?}R~P_S%BthOme zd9^AtzCcWyQ3vpiQkS$f2Jr=3z}Qwpqos+Kk=U(SBPPAeLZ^THQGa)5cFsB9eBZt2 zo;mlPvgE{g{@5vF0RVh)LQDz(2+jVZJs@(e$Gj7OF^|MC3sQ>@^mi}Fe~|D-?1H;) zDssQa6}{-(-4h^@f7~zgMe(`_ZjUgBMQI4n!YNgy*Y;Aiq*%63hpg zm7+G*d?mVA9>JOh8>Li&^lH)3Zi!n6>ps%3fPpz|=`Jnv1ylv_h@Y6#4 zyR}+n63CveV7}hxQXp>yE3Eg@oDPqi8hT|(iOr0w5X!iyd>$A_p^F3&KtshZgpr0a zN;pu$fnp9!7&7skF~)NdXN+-N#2I5O7jecIGe_daUu*JF#H1dl-G0F9XoWg&udDU- z-W_yjazEuh3ZGLs4tWq%N=+obC z$fUi|I$4HLKid-Vr#h^3rE67Jb3c~T$Q&AMGBjRSsyqs>@tT84FRzvX!?U_MwLjzf zHJ0b{0=#r{X)^y|6*_Q<5qx_=M$V5q;PKSNW0nz)$SzEF7+=IIp^=SV=B;();HW1* zq*>S|%Klt|hQ*;i5@BAy0Emz9OJ!elWKb?FELrYyaHrj9o+ZiGsfvetf(ZikeZ z--MNu%;LtGaKY3k$QZkVe#pTCXBZ*%t8U$;YOFYgz-;o4v)Omh`M%_KTFZbu*;Uq8 za{e9kmv_k$da)hpx_Am}UP?Hu`z<`(W-TsPGxs_}3~2JBc2Tduez0D)6RYjnQ>st2 zE9x=mpL>bVas>9s`ZuBWXGv6!nmN>22G`u6#hX4NZdqgCUgw@*>7M4<^Oh6f?1@2M z+U25M!ISXlpc!tJr+sSuxM685(Q5sWqc;bVvcfu;e#SF2bAmZ&MSw`PBF%*?yIiE1 z471t0N2&L;-#zjf+-dc1xIC6PpUcvhzl46aSigLP9wd~fjnFmd0ng#pdo0Kr!|>|= zk^Uc(fpn?;#gU<7w@39JV@f;Ob8LX;kdBmq!qO+0LoOx{SIpqpl{lHJm~^Dr?NK?> zkpfeg$(LMH2pcFb!-oDb(LBdl3;nEr`7A!k{J{{;`jgG!PPRc<&ObhE5P!0WYY+-+ zUO3Vqt8w{x#?`qGrm6V1?~f0B#R#ZhYt~&lg%wMN+Yh!}5UI(|BEW9lFT51XbL$>> znZA&SeEi8TvtbvoFl8yRNg-S#iLBkp@+=W~g)E$c z=>o~)aj*jQfq$wVTAt^z7xmaI%=$KYJ`T3h{Xn}a-_mk?GlWk~#H;~i>k7#B(?r#_ zFv4|GxaEydG_36L=_hbYlXpSRX(oFHo5{^kG=HLbqZT%mJ-ko98$JRF#w9tU`8R!0 z;Y4%M$YW6+KG4M6E79SkD5}Pzuaf6$e^FG>N^C8i%fB9qes3umd4t-0(XnNZFB^i1 zO{KZ~HW+Chw_zEB?Sp7;botzlXkwYG<4VYN<*q5eSMnBXpZ2lO-g`QkVD5WuSuL{$ z{6`6PI3K8ZA@p6*iFXt}UB_G9?+e1BV{LwQLajRqVWOSsx-B+$5=czI`Q0;LD97kW z+3we^(7JIXy;Ah{u_aFTcom;A#*~MH{YNWydKZ8&G42)K2i8}MW_^4eF-=pfe^A3~ z5<9JJp=i#Rw7qJTKtJ8)H_hm+T1*)eIAJv_l;8p34_ILuCp^OnmpD~deOX~BH*Ho@ z>i!K{4o2_r}t!? QV9OznO^j*yFz3_10iT?ze*gdg literal 0 HcmV?d00001 diff --git a/dist/manyspheres.html b/dist/manyspheres.html new file mode 100644 index 0000000..6b0ffbf --- /dev/null +++ b/dist/manyspheres.html @@ -0,0 +1,39 @@ + + + + + + + Graphische Datenverarbeitung - Raytracing - Many Spheres + + + + + + +
+
+
+
+ +
+ Implement a Raytracer by sending a ray into the scene for every pixel. Color the pixel with the color of the nearest sphere if the ray hits. +
+
+
+ +
Reference image
+
+
+
+ + + diff --git a/dist/phong-finished.png b/dist/phong-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..e88d4d8f2181f991e5ab54f84676d25b7c56a45b GIT binary patch literal 24758 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%0A7vpn)YUX`R>%Zy#U8}XX?A5y3j=3KMH*#vGLH; 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 literal 0 HcmV?d00001 diff --git a/dist/phong.html b/dist/phong.html new file mode 100644 index 0000000..3ad11a0 --- /dev/null +++ b/dist/phong.html @@ -0,0 +1,43 @@ + + + + + + + Graphische Datenverarbeitung - Raytracing - Phong + + + + + + +
+
+
+
+ +
+ Implement a Raytracer that uses the Phong Lighting model. +
+ + +
+
+
+
+ +
Reference image
+
+
+
+ + + \ No newline at end of file diff --git a/dist/raytracing-finished.png b/dist/raytracing-finished.png new file mode 100644 index 0000000000000000000000000000000000000000..db85207730e9038ca083eaa578d5c122f6f6add9 GIT binary patch literal 3018 zcmdUx4Nwzj8pk&Y5Dg!}FH}ymKsNFX(dKH;k^}@V`)-sXqNVsT+}Jz8s)u)}lZhnc zz&orbCcGvB8mn-5kDFxoq|$F?iD<)#Wp>^SX&)>T`k=jdmHIoow?pkZ>BSy z+nJsJ%+9{M@4Wx#`91$P@^W?Il4uErVc~$x$;U7O8@MNg@K5@#f4l?3LTf=zR)KNf z{lS%W!_P_GheFT~cp zsOjic8J0^`9B2_^UQn^?a1dsrD$17&t#2cIZ}}*;%KoJDR+ZRyfW1pJy@*YBX=A?{ z7IY@~|JHjq!n25ZY0=ss&o)pMSaXr| z{Q8hg5!17ZtHAa-ES;_k*jS7|Prpv++sk@WZcMbMeltU#DojZBV|ukPLGJfaF9o@e zu%cvzu#n=GI-y{3y5CO8gO!b}H?d}xXDYI}RN0u9(%R8ZL72jyKs_gpthY>XIfdIv ztp)86WX$pVz%=1VlD`)?g(YczH*ktd;`|Lj*qP|x4!CsdH6*R4n?x@V{zAab@*HL5 z3dV^2)`iD|yP`3ksv@(Hn`A$kn@MtGiuUP|c1=0>G&m#0%);y8Mv@y+wB!A+aJ`Db zi6ex07~Tt3)oIp)*G16)KS2oQyKIHfA|AGDiUBvfY$<0{Fk3xrm`!UzgKOqeBO zF=iSXRK+?jQ^AyYq^KtmtP@=XxQkdFKBh45gogRWefR<5M)+kt{uXgca+$)Lh*J?1 zh41I(GQb;&hmy-AzL$6yL6P_#Uh3*K^MEO-a0S;%FibIEypTRvb(^+PFu!{q>LCDA zJdw$Uv_W}+odi%q_W%+Tpe5)Uh1-alsKxPS6;!QPI@xX#wRWpePaJ3wFC+0CM2##g z-Aq80UU`!J1Bp_)-KZxPoCx*;{Ex(y=rDyj5~^NP?quhZ=(FxfR2>6Oh&MBGJH5QJ zl@*XEDQmImF+5omSu49pv`6L8)#>1HuuX4U16PF0oU-#odvwMe^%hX1Hs+W{;7L>e zYSS%PpcqL~mw~p)hTXC=#PZNCQo0hh1+S*7Q^8>69n?}sw`DD3vh?|b&2O>|ibJOU z98)hWP?yB1Uj=Ql;W}9d!6bJo)Wrb3T&6d9AbretoHZ)Iv2F$P2?f_w>59yT9bkjjJhJHjzmqA0CFo``Q4f!!UCS?uV5x`{u63anL$Tfi11X!AE&VVXPd5RSTK+7WCcl34hm<0Jrg}EG$kAHFc z*DjkL?w34;_`TTb8nAwvqh7NRbp6bIo2SHs zkjGH-OVAZPx`azmFdN1vicYkJ=H1&BQf}Ae18!YwK+Pubx^RIWKTAIp$)IKn_%vjE5Qa`5@r(3B z;X>4tUfO7Nv+|hIC)O+)7lAKA!shrB$UW)f(=|g-rPoe|?@1eSxDLB&ljl`r`xE{7 zw}W|#@+>QlOT^uFwknSK%DTvfPX||nQ_}psv!LY&;h*IxWvcb4LNE+1?-BwPw7gGW7O3hi zLo_8YI4!qm$#^tL4vDO_Jf``)qStOi?|&O&LU_wXiB}@W6VC6jYe2gXk>l~_5>^K- z1+E;dkJ7r)6oI4GQs$Bg98QbX6(w-r#@8~rSov;CgX=lLXcqTY-6NroX1%F?Ef!5` z@3hUt8fo0fN3uXNYoCe5GSbY~f;^e1sq&8GX@;wrjZL8{WYKag3O*^gHAURq!LE}x zy@1sO(%-v+k`(_hF2zS=iW!?;Q-&=dD>m?fQRmWL + + + + + + Graphische Datenverarbeitung - Raytracing - Basic Raytracing + + + + + + +
+
+
+
+ +
+ Implement a Raytracer by sending a ray into the scene for every pixel. Color the pixel black if the ray hits the given sphere. +
+
+
+ +
Reference image
+
+
+
+ + + diff --git a/src/04/bresenhamsimple.ts b/src/04/bresenhamsimple.ts index e680b39..8a2d794 100644 --- a/src/04/bresenhamsimple.ts +++ b/src/04/bresenhamsimple.ts @@ -21,4 +21,23 @@ export function bresenhamSimple(data: Uint8ClampedArray, pointA: [number, number // TODO: 1. Calculate dx and dy and set the start position x and y // TODO: 2. Calculate the initial epsilon of the bresenham algorithm // TODO: 3. Go from pointA[0] to pointB[0], and update epsilon in each step as given in the bresenham algorithm. Increase y when necessary. + + var dX = pointB[0] - pointA[0]; + var dY = pointB[1] - pointA[1]; + var x = pointA[0]; + var y = pointA[1]; + var err = 2 * dY - dX; + + while(x < pointB[0]) { + if(err <= 0) { + err += 2 * dY; + } else { + y = y + 1; + err += 2*dY - 2*dX; + } + setPixel(data, x, y, width, height); + x++; + } + + } diff --git a/src/04/ddasimple.ts b/src/04/ddasimple.ts index 0d3898c..bbd1f10 100644 --- a/src/04/ddasimple.ts +++ b/src/04/ddasimple.ts @@ -25,4 +25,16 @@ export function ddaSimple( // TODO: Calculcate the slope m for a line from pointA to pointB. // TODO: In this example, the main direction of the line is the x-direction. // TODO: Go from the x-coordinate of pointA (pointA[0]) to the x-coordinate of pointB (pointB[0]) and calculate the y-coordinate of the pixels in between. + pointA[0] = Math.round(pointA[0]); + pointA[1] = Math.round(pointA[1]); + pointB[0] = Math.round(pointB[0]); + pointB[1] = Math.round(pointB[1]); + + var m = (pointB[1] + pointA[1]) / (pointB[0] + pointA[0]); + setPixel(data, pointA[0], pointA[1], width, height); + setPixel(data, pointB[0], pointB[1], width, height); + + for(let i = 1; i < (pointB[0] - pointA[0]); i++) { + setPixel(data, pointA[0] + i, pointA[1] + Math.round( m * i), width, height); + } } diff --git a/src/05/camera.ts b/src/05/camera.ts new file mode 100644 index 0000000..19557be --- /dev/null +++ b/src/05/camera.ts @@ -0,0 +1,32 @@ +import Vector from '../05/vector'; + +/** + * A class representing a camera + */ +export default class Camera { + + public width: number; + public height: number; + public alpha: number; + public origin: Vector; + + /** + * Creates a new camera with an image canvas, a field of view, and a position in world space. + * For now, the camera is always viewing along the negative z-axis. + * @param width The width of the canvas + * @param height The height of the canvas + * @param alpha The field of view in X dimension of the camera + * @param origin The origin of the camera in world coordinates + */ + constructor( + width: number, + height: number, + alpha: number, + origin: Vector = new Vector(0, 0, 0, 1) + ) { + this.width = width; + this.height = height; + this.alpha = alpha; + this.origin = origin; + } +} diff --git a/src/05/intersection.ts b/src/05/intersection.ts new file mode 100644 index 0000000..45ba158 --- /dev/null +++ b/src/05/intersection.ts @@ -0,0 +1,39 @@ +import Vector from './vector'; + +/** + * Class representing a ray-sphere intersection in 3D space + */ +export default class Intersection { + + public t: number; + public point: Vector; + public normal: Vector; + + /** + * Create an Intersection + * @param t The distance on the ray + * @param point The intersection point + * @param normal The normal of the surface at the point of intersection + */ + constructor(t: number = Infinity, + point: Vector = null, + normal: Vector = null) { + + this.t = t; + this.point = point; + this.normal = normal; + } + + /** + * Determines whether this intersection + * is closer than the other + * @param other The other Intersection + * @return The result + */ + closerThan(other: Intersection): boolean { + if (this.t < other.t) + return true; + else + return false; + } +} diff --git a/src/05/manyspheres.ts b/src/05/manyspheres.ts new file mode 100644 index 0000000..db895b2 --- /dev/null +++ b/src/05/manyspheres.ts @@ -0,0 +1,28 @@ +import Camera from './camera'; +import Sphere from './sphere'; +import Intersection from './intersection'; +import Vector from './vector'; +import Ray from './ray'; + +/** + * Compute the color of the pixel (x, y) by raytracing + * using a given camera and multiple spheres. + * + * @param data The linearised pixel array + * @param camera The camera used for raytracing + * @param spheres The spheres to raytrace + * @param x The x coordinate of the pixel to convert + * @param y The y coordinate of the pixel to convert + * @param width The width of the canvas + * @param height The height of the canvas + */ +export function raytrace(data: Uint8ClampedArray, + camera: Camera, + spheres: Array, + x: number, y: number, + width: number, height: number) { + + // TODO: Generate ray and perform intersection with every sphere. + // TODO: On intersection set pixel color to color of the sphere + // TODO: containing the closest intersection point. +} diff --git a/src/05/ray.ts b/src/05/ray.ts new file mode 100644 index 0000000..ff38d30 --- /dev/null +++ b/src/05/ray.ts @@ -0,0 +1,38 @@ +import Vector from './vector'; +import Camera from './camera'; + +/** + * Class representing a ray + */ +export default class Ray { + + public origin: Vector = null; + public direction: Vector = null; + + /** + * Creates a new ray with origin and direction + * @param origin The origin of the Ray + * @param direction The direction of the Ray + */ + constructor(origin: Vector, direction: Vector) { + + this.origin = origin; + this.direction = direction; + } + + /** + * Creates a ray from the camera through the image plane. + * The image plane is positioned in direction of the negative z-axis. + * @param x The pixel's x-position in the canvas + * @param y The pixel's y-position in the canvas + * @param camera The Camera + * @return The resulting Ray + */ + static makeRay(x: number, y: number, camera: Camera): Ray { + // TODO: Generate a ray from the camera origin through pixel (x, y) + // TODO: on the image plane. In addition to the coordinates (x, y), you will need the + // TODO: width and height of the camera (i.e. the width and height of the camera's + // TODO: image plane), and the angle alpha specifying the camera's field of view. + return null; + } +} diff --git a/src/05/raytracing.ts b/src/05/raytracing.ts new file mode 100644 index 0000000..b05d695 --- /dev/null +++ b/src/05/raytracing.ts @@ -0,0 +1,28 @@ +import Camera from './camera'; +import Sphere from './sphere'; +import Ray from './ray'; + +/** + * Compute the color of the pixel (x, y) by raytracing + * using a given camera and a sphere. + * + * @param data The linearised pixel array + * @param camera The camera used for raytracing + * @param sphere The sphere to raytrace + * @param x The x coordinate of the pixel to convert + * @param y The y coordinate of the pixel to convert + * @param width The width of the canvas + * @param height The height of the canvas + */ + +export function raytrace(data: Uint8ClampedArray, + camera: Camera, + sphere: Sphere, + x: number, y: number, + width: number, height: number) { + + // TODO: Create a ray from the camera's position through the pixel + // TODO: (x, y) in the camera's image plane, and perform intersection + // TODO: with the given sphere. Set color of pixel (x, y) in the data + // TODO: array to black, if the ray hits the sphere. +} diff --git a/src/05/setup-manyspheres.ts b/src/05/setup-manyspheres.ts new file mode 100644 index 0000000..f2a68bd --- /dev/null +++ b/src/05/setup-manyspheres.ts @@ -0,0 +1,51 @@ +import 'bootstrap'; +import 'bootstrap/scss/bootstrap.scss'; +import Sphere from './sphere'; +import Vector from './vector'; +import Camera from './camera'; +import { raytrace } from './manyspheres'; + +window.addEventListener('load', evt => { + + const canvas = document.getElementById("result") as HTMLCanvasElement; + + if (canvas === null) + return; + + const ctx = canvas.getContext("2d"); + var pixel = ctx.createImageData(1, 1); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + const spheres: Sphere[] = [ + new Sphere( + new Vector(0, 0, -10, 1), + 2.0, + new Vector(1, 0, 0, 1) + ), + new Sphere( + new Vector(2, 0, -12, 1), + 1.5, + new Vector(0, 1, 0, 1) + ), + new Sphere( + new Vector(-2, 0, -8, 1), + 1.0, + new Vector(0, 0, 1, 1) + ) + ]; + + const camera = new Camera(canvas.width, canvas.height, Math.PI / 3); + + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + + raytrace(data, camera, spheres, x, y, canvas.width, canvas.height); + + // update pixel in HTML context2d + for (let i = 0; i < 4; i ++) + pixel.data[i] = data[(x + y * canvas.width) * 4 + i]; + ctx.putImageData(pixel, x, y); + } + } +}); diff --git a/src/05/setup-raytracing.ts b/src/05/setup-raytracing.ts new file mode 100644 index 0000000..891fbdc --- /dev/null +++ b/src/05/setup-raytracing.ts @@ -0,0 +1,38 @@ +import 'bootstrap'; +import 'bootstrap/scss/bootstrap.scss'; +import Sphere from './sphere'; +import Vector from './vector'; +import Camera from './camera'; +import { raytrace } from './raytracing'; + +window.addEventListener('load', evt => { + + const canvas = document.getElementById("result") as HTMLCanvasElement; + if (canvas === null) + return; + + const ctx = canvas.getContext("2d"); + var pixel = ctx.createImageData(1, 1); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + const sphere = new Sphere( + new Vector(0, 0, -10, 1), // position + 4.0, // radius + new Vector(0, 0, 0, 1) // color + ); + + const camera = new Camera(canvas.width, canvas.height, Math.PI / 3); + + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + + raytrace(data, camera, sphere, x, y, canvas.width, canvas.height); + + // update pixel in HTML context2d + for (let i = 0; i < 4; i ++) + pixel.data[i] = data[(x + y * canvas.width) * 4 + i]; + ctx.putImageData(pixel, x, y); + } + } +}); diff --git a/src/05/sphere.ts b/src/05/sphere.ts new file mode 100644 index 0000000..bbf22cc --- /dev/null +++ b/src/05/sphere.ts @@ -0,0 +1,49 @@ +import Vector from './vector'; +import Intersection from './intersection'; +import Ray from './ray'; + +/** + * A class representing a sphere + */ +export default class Sphere { + + public center: Vector; + public radius: number; + public color: Vector; + + /** + * Creates a new Sphere with center and radius + * @param center The center of the Sphere + * @param radius The radius of the Sphere + * @param color The color of the Sphere + */ + constructor( + center: Vector, + radius: number, + color: Vector + ) { + this.center = center; + this.radius = radius; + this.color = color; + } + + /** + * Calculates the intersection of the sphere with the given ray + * @param ray The ray to intersect with + * @return The intersection if there is one, null if there is none + */ + intersect(ray: Ray): Intersection | null { + + // TODO: Calculate the quadratic equation for ray-sphere + // TODO: intersection. You will need the origin of your ray as x0, + // TODO: the ray direction, and the radius of the sphere. + // TODO: Don't forget to translate your ray's starting position with + // TODO: respect to the center of the sphere. + // TODO: Calculate the discriminant c, and distinguish between the 3 + // TODO: possible outcomes: no hit, one hit, or two hits. + // TODO: Return an Intersection or null if there was no hit. In case + // TODO: of two hits, return the one closer to the start point of + // TODO: the ray. + return null; + } +} diff --git a/src/05/vector.ts b/src/05/vector.ts new file mode 100644 index 0000000..9efa4b4 --- /dev/null +++ b/src/05/vector.ts @@ -0,0 +1,256 @@ +/** + * Class representing a vector in 4D space + */ +export default class Vector { + /** + * The variable to hold the vector data + */ + data: [number, number, number, number]; + + /** + * Create a vector + * @param x The x component + * @param y The y component + * @param z The z component + * @param w The w component + */ + constructor(x: number, y: number, z: number, w: number) { + // TODO: Set the data member components to the given values + } + + /** + * Returns the x component of the vector + * @return The x component of the vector + */ + get x(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the x component of the vector to val + * @param val - The new value + */ + set x(val: number) { + // TODO: Set actual value + } + + /** + * Returns the first component of the vector + * @return The first component of the vector + */ + get r(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the first component of the vector to val + * @param val The new value + */ + set r(val: number) { + // TODO: Set actual value + } + + /** + * Returns the y component of the vector + * @return The y component of the vector + */ + get y(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the y component of the vector to val + * @param val The new value + */ + set y(val: number) { + // TODO: Set actual value + } + + /** + * Returns the second component of the vector + * @return The second component of the vector + */ + get g(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the second component of the vector to val + * @param val The new value + */ + set g(val: number) { + // TODO: Set actual value + } + + /** + * Returns the z component of the vector + * @return The z component of the vector + */ + get z(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the z component of the vector to val + * @param val The new value + */ + set z(val: number) { + // TODO: Set actual value + } + + /** + * Returns the third component of the vector + * @return The third component of the vector + */ + get b(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the third component of the vector to val + * @param val The new value + */ + set b(val: number) { + // TODO: Set actual value + } + + /** + * Returns the w component of the vector + * @return The w component of the vector + */ + get w(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the w component of the vector to val + * @param val The new value + */ + set w(val: number) { + // TODO: Set actual value + } + + /** + * Returns the fourth component of the vector + * @return The fourth component of the vector + */ + get a(): number { + // TODO: Return actual value + return null; + } + + /** + * Sets the fourth component of the vector to val + * @param val The new value + */ + set a(val: number) { + // TODO: Set actual value + } + + /** + * Creates a new vector with the vector added + * @param other The vector to add + * @return The new vector; + */ + add(other: Vector): Vector { + // TODO: Return new vector with result + return null; + } + + /** + * Creates a new vector with the vector subtracted + * @param other The vector to subtract + * @return The new vector + */ + sub(other: Vector): Vector { + // TODO: Return new vector with result + return null; + } + + /** + * Creates a new vector with the scalar multiplied + * @param other The scalar to multiply + * @return The new vector + */ + mul(other: number): Vector { + // TODO: Return new vector with result + return null; + } + + /** + * Creates a new vector with the scalar divided + * @param other The scalar to divide + * @return The new vector + */ + div(other: number): Vector { + // TODO: Return new vector with result + return null; + } + + /** + * Dot product + * @param other The vector to calculate the dot product with + * @return The result of the dot product + */ + dot(other: Vector): number { + // TODO: Compute and return dot product + return 0; + } + + /** + * Cross product + * Calculates the cross product using the first three components. + * @param other The vector to calculate the cross product with + * @return The result of the cross product as new Vector + */ + cross(other: Vector): Vector { + // TODO: Return new vector with result + // TODO: The fourth component should be set to 0 + return null; + } + + /** + * Normalizes this vector in place + * @returns this vector for easier function chaining + */ + normalize(): Vector { + // TODO: Normalize this vector and return it + return this; + } + + /** + * Compares the vector to another vector. + * @param other The vector to compare to. + * @return True if the vectors carry equal numbers. + */ + equals(other: Vector): boolean { + // TODO: Perform comparison and return result + // TODO: Respect inaccuracies: coordinates within 0.000001 of each other + // TODO: should be considered equal + return false; + } + + /** + * Calculates the length of the vector + * @return The length of the vector + */ + get length(): number { + // TODO: Calculate and return length + return 0; + } + + /** + * Returns an array representation of the vector + * @return An array representation. + */ + valueOf(): [number, number, number, number] { + return this.data; + } +} diff --git a/src/06/phong.ts b/src/06/phong.ts new file mode 100644 index 0000000..87361f2 --- /dev/null +++ b/src/06/phong.ts @@ -0,0 +1,35 @@ +import Vector from '../05/vector'; +import Intersection from '../05/intersection'; + +/** + * Calculate the color of an object at the intersection point according to the Phong Lighting model. + * @param color The color of the intersected object + * @param intersection The intersection information + * @param lightPositions The light positions + * @param shininess The shininess parameter of the Phong model + * @param cameraPosition The position of the camera + * @return The resulting color + */ +export function phong( + color: Vector, + intersection: Intersection, + lightPositions: Array, + shininess: number, + cameraPosition: Vector +): Vector { + + const lightColor = new Vector(0.8, 0.8, 0.8, 0); + const kA = 1.0; + const kD = 0.5; + const kS = 0.5; + + // TODO: Compute light intensity according to phong reflection model. + // TODO: Compute diffuse lighting using light color, diffuse + // TODO: reflectivity, light positions and an intersection point. + // TODO: Compute specular reflection using light color, specular + // TODO: reflectivity, shininess, light positions, an intersection + // TODO: point, and eye (camera) position. + // TODO: Return complete phong emission using object color, ambient, + // TODO: diffuse and specular terms. + return color; +} diff --git a/src/06/raytracing.ts b/src/06/raytracing.ts new file mode 100644 index 0000000..0c6040c --- /dev/null +++ b/src/06/raytracing.ts @@ -0,0 +1,32 @@ +import Camera from '../05/camera'; +import Sphere from '../05/sphere'; +import Intersection from '../05/intersection'; +import Vector from '../05/vector'; +import Ray from '../05/ray'; +import { phong } from './phong'; + +/** + * Compute the color of the pixel (x, y) by raytracing + * using a given camera and multiple spheres. + * + * @param data The linearised pixel array + * @param camera The camera used for raytracing + * @param spheres The spheres to raytrace + * @param x The x coordinate of the pixel to convert + * @param y The y coordinate of the pixel to convert + * @param width The width of the canvas + * @param height The height of the canvas + */ +export function raytracePhong(data: Uint8ClampedArray, + camera: Camera, + spheres: Array, + lightPositions: Array, + shininess: number, + x: number, y: number, + width: number, height: number) { + + // TODO: Create ray from camera through image plane at position (x, y). + // TODO: Compute closest intersection with spheres in the scene. + // TODO: Compute emission at point of intersection using phong model. + // TODO: Set pixel color accordingly. +} diff --git a/src/06/setup-phong.ts b/src/06/setup-phong.ts new file mode 100644 index 0000000..d07dc8a --- /dev/null +++ b/src/06/setup-phong.ts @@ -0,0 +1,62 @@ +import 'bootstrap'; +import 'bootstrap/scss/bootstrap.scss'; +import Vector from '../05/vector'; +import Sphere from '../05/sphere'; +import Ray from '../05/ray'; +import Camera from '../05/camera'; +import Intersection from '../05/intersection'; +import { raytracePhong } from './raytracing'; + +window.addEventListener('load', () => { + + const canvas = document.getElementById("result") as HTMLCanvasElement; + if (canvas === null) + return; + + const ctx = canvas.getContext("2d"); + var pixel = ctx.createImageData(1, 1); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + + const spheres: Sphere[] = [ + new Sphere(new Vector(.5, -.2, -2, 1), 0.4, new Vector(.3, 0, 0, 1)), + new Sphere(new Vector(-.5, -.2, -1.7, 1), 0.2, new Vector(0, 0, .3, 1)) + ]; + + const lightPositions = [ + new Vector(1, 1, -1, 1) + ]; + + let shininess = 10; + + const camera = new Camera( + canvas.width, canvas.height, Math.PI / 3, new Vector(0, 0, 0, 1) + ); + + function animate() { + + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + + raytracePhong(data, camera, spheres, lightPositions, shininess, x, y, canvas.width, canvas.height); + + // update pixel in HTML context2d + for (let i = 0; i < 4; i ++) + pixel.data[i] = data[(x + y * canvas.width) * 4 + i]; + ctx.putImageData(pixel, x, y); + } + } + } + + const shininessElement = + document.getElementById("shininess") as HTMLInputElement; + + shininessElement.onchange = function () { + shininess = Number(shininessElement.value); + window.requestAnimationFrame(animate); + } + + shininess = Number(shininessElement.value); + + window.requestAnimationFrame(animate); +}); diff --git a/test/ray-spec.ts b/test/ray-spec.ts new file mode 100644 index 0000000..321db33 --- /dev/null +++ b/test/ray-spec.ts @@ -0,0 +1,50 @@ +import Ray from "../src/05/ray"; +import Camera from "../src/05/camera"; + +import { assert, expect } from 'chai'; + +describe('Ray', () => { + + it('can be initialized with two numbers and a camera', () => { + const r: Ray = Ray.makeRay(0, 0, new Camera(128, 128, 45)); + expect(r).to.be.an('object'); + }); + + it('the origin of the ray is initialized correctly', () => { + const r: Ray = Ray.makeRay(0, 0, new Camera(128, 128, 45)); + expect(r).to.be.an('object'); + expect(r.origin.x).to.equal(0); + expect(r.origin.y).to.equal(0); + expect(r.origin.z).to.equal(0); + expect(r.origin.w).to.equal(1); + }); + + it('the direction is normalized', () => { + const r: Ray = Ray.makeRay(64, 64, new Camera(129, 129, 45)); + expect(r).to.be.an('object'); + expect(r.direction.length).to.equal(1); + }); + + it('the direction is initialized correctly', () => { + const r: Ray = Ray.makeRay(64, 64, new Camera(129, 129, 45)); + expect(r).to.be.an('object'); + expect(r.direction.x).to.be.closeTo(0, 0.01); + expect(r.direction.y).to.be.closeTo(0, 0.01); + expect(r.direction.z).to.be.closeTo(-1, 0.01); + expect(r.direction.w).to.be.closeTo(0, 0.01); + + const r2: Ray = Ray.makeRay(0, 0, new Camera(129, 129, 45)); + expect(r2).to.be.an('object'); + expect(r2.direction.x).to.be.closeTo(-0.435, 0.01); + expect(r2.direction.y).to.be.closeTo(0.435, 0.01); + expect(r2.direction.z).to.be.closeTo(-0.787, 0.01); + expect(r2.direction.w).to.be.closeTo(0, 0.01); + + const r3: Ray = Ray.makeRay(10, 7, new Camera(64, 64, 90)); + expect(r3).to.be.an('object'); + expect(r3.direction.x).to.be.closeTo(-0.564, 0.01); + expect(r3.direction.y).to.be.closeTo(0.642, 0.01); + expect(r3.direction.z).to.be.closeTo(-0.518, 0.01); + expect(r3.direction.w).to.equal(0); + }); +}); diff --git a/test/sphere-spec.ts b/test/sphere-spec.ts new file mode 100644 index 0000000..ea43650 --- /dev/null +++ b/test/sphere-spec.ts @@ -0,0 +1,78 @@ +import Sphere from "../src/05/sphere"; +import Intersection from "../src/05/intersection"; + +import { assert, expect } from 'chai'; +import Vector from "../src/05/vector"; +import Ray from "../src/05/ray"; + +describe('Sphere', () => { + + it('can be initialized with center, radius and color', () => { + const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0)); + expect(s).to.be.an('object'); + }); + + it('a sphere at origin and radius 1 can be intersected correctly with the x axis', () => { + + const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0)); + const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0))); + expect(s).to.be.an('object'); + expect(i).to.be.an('object'); + + expect(i.point.x).to.equal(-1); + expect(i.point.y).to.equal(0); + expect(i.point.z).to.equal(0); + }); + + it('a sphere at origin and radius 1 can be intersected correctly with the y axis', () => { + + const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0)); + const i: Intersection = s.intersect(new Ray(new Vector(0, -10, 0, 1), new Vector(0, 1, 0, 0))); + expect(s).to.be.an('object'); + expect(i).to.be.an('object'); + + expect(i.point.x).to.equal(0); + expect(i.point.y).to.equal(-1); + expect(i.point.z).to.equal(0); + }); + + it('a sphere at origin and radius 1 can be intersected correctly with the z axis', () => { + + const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 1, new Vector(0, 0, 0, 0)); + const i: Intersection = s.intersect(new Ray(new Vector(0, 0, -10, 1), new Vector(0, 0, 1, 0))); + expect(s).to.be.an('object'); + expect(i).to.be.an('object'); + + expect(i.point.x).to.equal(0); + expect(i.point.y).to.equal(0); + expect(i.point.z).to.equal(-1); + }); + + it('intersection is correct when radius != 1', () => { + + const s: Sphere = new Sphere(new Vector(0, 0, 0, 1), 2.5, new Vector(0, 0, 0, 0)); + const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0))); + expect(s).to.be.an('object'); + expect(i).to.be.an('object'); + + expect(i.point.x).to.equal(-2.5); + expect(i.point.y).to.equal(0); + expect(i.point.z).to.equal(0); + expect(i.t).to.equal(7.5); + }); + + it('intersection is correct when center != (0, 0, 0, 1)', () => { + + const s: Sphere = new Sphere(new Vector(1, 0, 0, 1), 2.5, new Vector(0, 0, 0, 0)); + const i: Intersection = s.intersect(new Ray(new Vector(-10, 0, 0, 1), new Vector(1, 0, 0, 0))); + expect(s).to.be.an('object'); + expect(i).to.be.an('object'); + + expect(i.point.x).to.equal(-1.5); + expect(i.point.y).to.equal(0); + expect(i.point.z).to.equal(0); + expect(i.t).to.equal(8.5); + }); +}); + +// TODO: Test normalization of normal diff --git a/test/vector-spec.ts b/test/vector-spec.ts new file mode 100644 index 0000000..6a74af3 --- /dev/null +++ b/test/vector-spec.ts @@ -0,0 +1,275 @@ +import Vector from "../src/05/vector"; + +import { expect } from 'chai'; + +describe('Vector', () => { + + it('can be initialized with 4 numbers', () => { + const v = new Vector(1, 2, 3, 4); + expect(v).to.be.an('object'); + expect(v.data[0]).to.be.a("number"); + expect(v.data[0]).to.equal(1); + expect(v.data[1]).to.be.a("number"); + expect(v.data[1]).to.equal(2); + expect(v.data[2]).to.be.a("number"); + expect(v.data[2]).to.equal(3); + expect(v.data[3]).to.be.a("number"); + expect(v.data[3]).to.equal(4); + }); + + it('x component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('x'); + v.x = 42; + expect(v.x).to.equal(42); + }); + + it('y component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('y'); + v.y = 42; + expect(v.y).to.equal(42); + }); + + it('z component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('z'); + v.z = 42; + expect(v.z).to.equal(42); + }); + + it('w component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('w'); + v.w = 42; + expect(v.w).to.equal(42); + }); + + it('r component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('r'); + v.r = 42; + expect(v.r).to.equal(42); + }); + + it('g component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('g'); + v.g = 42; + expect(v.g).to.equal(42); + }); + + it('b component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('b'); + v.b = 42; + expect(v.b).to.equal(42); + }); + + it('a component can be set and retrieved', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).has.property('a'); + v.a = 42; + expect(v.a).to.equal(42); + }); + + it('method add exists', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).to.respondTo('add'); + }); + + it('method sub exists', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).to.respondTo('sub'); + }); + + it('method mul exists', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).to.respondTo('mul'); + }); + + it('method div exists', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).to.respondTo('div'); + }); + + it('method normalize exists', () => { + const v = new Vector(0, 0, 0, 0); + expect(v).to.respondTo('normalize'); + }); + + it('length returns the correct result', () => { + const v = new Vector(0, 4, 3, 0); + expect(v).has.property('length'); + expect(v.length).to.equal(5); + }); + + it('addition adds the other vector', () => { + const a = new Vector(1, 2, 3, 1); + const b = new Vector(1, -2, 0, 0); + expect(a).to.respondTo('add'); + + const c: Vector = a.add(b); + expect(c).to.not.be.null; + expect(c).to.be.an('object'); + + expect(c.x).to.equal(2); + expect(c.y).to.equal(0); + expect(c.z).to.equal(3); + expect(c.w).to.equal(1); + }); + + it('addition does not change the value of "this"', () => { + + const a = new Vector(1, 2, 3, 1); + const b = new Vector(1, -2, 0, 0); + expect(a).to.respondTo('add'); + + const c: Vector = a.add(b); + expect(a).to.not.be.null; + expect(a).to.be.an('object'); + + expect(a.x).to.equal(1); + expect(a.y).to.equal(2); + expect(a.z).to.equal(3); + expect(a.w).to.equal(1); + }); + + it('subtraction subtracts the other vector', () => { + const a = new Vector(1, 3, 3, 0); + const b = new Vector(2, -2, 0, 0); + expect(a).to.respondTo('sub'); + + const c: Vector = a.sub(b); + expect(c).to.not.be.null; + expect(c).to.be.an('object'); + + expect(c.x).to.equal(-1); + expect(c.y).to.equal(5); + expect(c.z).to.equal(3); + expect(c.w).to.equal(0); + }); + + it('subtraction does not change the value of "this"', () => { + + const a = new Vector(1, 2, 3, 1); + const b = new Vector(1, -2, 0, 0); + expect(a).to.respondTo('sub'); + + const c: Vector = a.sub(b); + expect(a).to.not.be.null; + expect(a).to.be.an('object'); + + expect(a.x).to.equal(1); + expect(a.y).to.equal(2); + expect(a.z).to.equal(3); + expect(a.w).to.equal(1); + }); + + it('multiplication with a scalar multiplies correctly', () => { + const a = new Vector(1, 3, 3, 0); + expect(a).to.respondTo('mul'); + + const c: Vector = a.mul(4); + expect(c.x).to.equal(4); + expect(c.y).to.equal(12); + expect(c.z).to.equal(12); + expect(c.w).to.equal(0); + }); + + it('multiplication does not change the value of "this"', () => { + const a = new Vector(1, 3, 3, 0); + expect(a).to.respondTo('mul'); + + const c: Vector = a.mul(4); + expect(a.x).to.equal(1); + expect(a.y).to.equal(3); + expect(a.z).to.equal(3); + expect(a.w).to.equal(0); + }); + + + it('division by a scalar divides correctly', () => { + const a = new Vector(3, 12, 6, 0); + expect(a).to.respondTo('div'); + + const c: Vector = a.div(3); + expect(c.x).to.equal(1); + expect(c.y).to.equal(4); + expect(c.z).to.equal(2); + expect(c.w).to.equal(0); + }); + + it('division does not change the value of "this"', () => { + const a = new Vector(1, 3, 3, 0); + expect(a).to.respondTo('div'); + + const c: Vector = a.div(4); + expect(a.x).to.equal(1); + expect(a.y).to.equal(3); + expect(a.z).to.equal(3); + expect(a.w).to.equal(0); + }); + + it('cross product returns the correct result', () => { + + const a = new Vector(1, 3, 3, 0); + const b = new Vector(2, -2, 0, 0); + expect(a).to.respondTo('cross'); + const c = a.cross(b); + expect(c.x).to.equal(6); + expect(c.y).to.equal(6); + expect(c.z).to.equal(-8); + expect(c.w).to.equal(0); + }); + + it('cross product does not change the value of "this"', () => { + + const a = new Vector(1, 3, 3, 0); + const b = new Vector(2, -2, 0, 0); + expect(a).to.respondTo('cross'); + const c = a.cross(b); + expect(a.x).to.equal(1); + expect(a.y).to.equal(3); + expect(a.z).to.equal(3); + expect(a.w).to.equal(0); + }); + + it('dot product is correct', () => { + + const a = new Vector(1, 3, 3, 0); + const b = new Vector(2, 2, 1, 0); + const c = new Vector(2, -2, 0, 0); + expect(a).to.respondTo('dot'); + const d = a.dot(b); + const e = b.dot(c); + expect(d).to.equal(11); + expect(e).to.equal(0); + }); + + it('equality of different vectors returns false', () => { + + const a = new Vector(1, 3, 3, 1); + const b = new Vector(2, -2, 0, 1); + expect(a).to.respondTo('equals'); + + expect(a.equals(b)).to.equal(false); + }); + + it('equality of equal vectors returns true', () => { + + const a = new Vector(1, 3, 3, 1); + expect(a).to.respondTo('equals'); + + expect(a.equals(a)).to.equal(true); + }); + + it('vectors very close to each other are equal', () => { + + const a = new Vector(1, 3, 3, 1); + const b = new Vector(1.0000000000001, 3.000000000001, 2.9999999999999, 1); + expect(a).to.respondTo('equals'); + + expect(a.equals(b)).to.equal(true); + }); +});