├── .gitignore ├── Exercises ├── Chapter 1 │ ├── Projectile │ │ ├── .vscode │ │ │ ├── launch.json │ │ │ └── tasks.json │ │ ├── Environment.cs │ │ ├── Program.cs │ │ ├── Projectile.cs │ │ └── projectile.csproj │ └── Results │ │ ├── Projectile-trajectory.xlsx │ │ └── trajectory.txt └── Chapter2 │ ├── DrawProjectile │ ├── .vscode │ │ ├── launch.json │ │ └── tasks.json │ ├── DrawProjectile.csproj │ ├── Environment.cs │ ├── Program.cs │ └── Projectile.cs │ └── Results │ ├── file-900x500.ppm │ └── file10x10.ppm ├── LICENSE ├── README.md ├── blog-posts ├── 1-intro.md ├── 2-Setting-up-the-environment.md ├── 3-Implementing-primitives-math.md ├── 4-writing-images.md └── images │ ├── 1-raytracer-challenge-cover.jpg │ ├── 2-initial-structure.png │ ├── 3-ballistic-trajectory.png │ ├── 3-text-output.png │ ├── 4-trajectory-10x10.png │ └── 4-trajectory-900x500.png ├── create-structure.sh ├── resources └── useful-links.md └── src ├── .vscode ├── launch.json └── tasks.json ├── ray-tracer-challenge.sln ├── raytracer ├── Canvas.cs ├── Color.cs ├── DoubleExtensions.cs ├── Tuple.cs └── codeclimber.raytracer.csproj └── xUnit ├── CanvasTest.cs ├── ColorTest.cs ├── PointTest.cs ├── TupleTest.cs ├── VectorTest.cs └── codeclimber.raytracer.xUnit.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/projectile.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/projectile.csproj" 11 | ], 12 | "problemMatcher": "$tsc" 13 | }, 14 | { 15 | "label": "publish", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "publish", 20 | "${workspaceFolder}/projectile.csproj" 21 | ], 22 | "problemMatcher": "$tsc" 23 | }, 24 | { 25 | "label": "watch", 26 | "command": "dotnet", 27 | "type": "process", 28 | "args": [ 29 | "watch", 30 | "run", 31 | "${workspaceFolder}/projectile.csproj" 32 | ], 33 | "problemMatcher": "$tsc" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/Environment.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter1.Projectile 2 | { 3 | using codeclimber.raytracer; 4 | public class Environment 5 | { 6 | public Environment(Tuple gravity, Tuple wind) 7 | { 8 | Gravity = gravity; 9 | Wind = wind; 10 | } 11 | 12 | public Tuple Gravity { get; set; } 13 | public Tuple Wind { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/Program.cs: -------------------------------------------------------------------------------- 1 | using sys=System; 2 | using codeclimber.raytracer; 3 | 4 | namespace Chapter1.Projectile 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | var p = new Projectile(Tuple.Point(0, 1, 0), Tuple.Vector(1, 1, 0).Normalize()); 11 | var e = new Environment(Tuple.Vector(0, -0.1, 0), Tuple.Vector(-0.01, 0, 0)); 12 | int i = 0; 13 | while (p.Position.Y >= 0) 14 | { 15 | i++; 16 | sys.Console.WriteLine($"{i} - {p}"); 17 | p = Update(p, e); 18 | } 19 | } 20 | 21 | public static Projectile Update(Projectile p, Environment e) 22 | { 23 | return new Projectile( 24 | p.Position.Add(p.Velocity), 25 | p.Velocity.Add(e.Gravity).Add(e.Wind) 26 | ); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/Projectile.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter1.Projectile 2 | { 3 | using codeclimber.raytracer; 4 | public class Projectile 5 | { 6 | public Projectile(Tuple position, Tuple velocity) 7 | { 8 | Position = position; 9 | Velocity = velocity; 10 | } 11 | 12 | public Tuple Position { get; set; } 13 | public Tuple Velocity { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | return $"{Position} (v={Velocity})"; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Exercises/Chapter 1/Projectile/projectile.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exe 9 | netcoreapp2.2 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Exercises/Chapter 1/Results/Projectile-trajectory.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/Exercises/Chapter 1/Results/Projectile-trajectory.xlsx -------------------------------------------------------------------------------- /Exercises/Chapter 1/Results/trajectory.txt: -------------------------------------------------------------------------------- 1 | 1 - [0, 1, 0],(1) (v=[0,707106781186547, 0,707106781186547, 0],(0)) 2 | 2 - [0,707106781186547, 1,70710678118655, 0],(1) (v=[0,697106781186547, 0,607106781186547, 0],(0)) 3 | 3 - [1,40421356237309, 2,31421356237309, 0],(1) (v=[0,687106781186547, 0,507106781186548, 0],(0)) 4 | 4 - [2,09132034355964, 2,82132034355964, 0],(1) (v=[0,677106781186547, 0,407106781186548, 0],(0)) 5 | 5 - [2,76842712474619, 3,22842712474619, 0],(1) (v=[0,667106781186547, 0,307106781186548, 0],(0)) 6 | 6 - [3,43553390593274, 3,53553390593274, 0],(1) (v=[0,657106781186547, 0,207106781186548, 0],(0)) 7 | 7 - [4,09264068711929, 3,74264068711928, 0],(1) (v=[0,647106781186547, 0,107106781186548, 0],(0)) 8 | 8 - [4,73974746830583, 3,84974746830583, 0],(1) (v=[0,637106781186547, 0,00710678118654753, 0],(0)) 9 | 9 - [5,37685424949238, 3,85685424949238, 0],(1) (v=[0,627106781186547, -0,0928932188134525, 0],(0)) 10 | 10 - [6,00396103067893, 3,76396103067893, 0],(1) (v=[0,617106781186547, -0,192893218813452, 0],(0)) 11 | 11 - [6,62106781186548, 3,57106781186548, 0],(1) (v=[0,607106781186547, -0,292893218813452, 0],(0)) 12 | 12 - [7,22817459305202, 3,27817459305202, 0],(1) (v=[0,597106781186547, -0,392893218813453, 0],(0)) 13 | 13 - [7,82528137423857, 2,88528137423857, 0],(1) (v=[0,587106781186547, -0,492893218813452, 0],(0)) 14 | 14 - [8,41238815542512, 2,39238815542512, 0],(1) (v=[0,577106781186547, -0,592893218813452, 0],(0)) 15 | 15 - [8,98949493661167, 1,79949493661167, 0],(1) (v=[0,567106781186547, -0,692893218813452, 0],(0)) 16 | 16 - [9,55660171779821, 1,10660171779821, 0],(1) (v=[0,557106781186547, -0,792893218813452, 0],(0)) 17 | 17 - [10,1137084989848, 0,31370849898476, 0],(1) (v=[0,547106781186547, -0,892893218813452, 0],(0)) -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/DrawProjectile.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/DrawProjectile.csproj" 11 | ], 12 | "problemMatcher": "$tsc" 13 | }, 14 | { 15 | "label": "publish", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "publish", 20 | "${workspaceFolder}/DrawProjectile.csproj" 21 | ], 22 | "problemMatcher": "$tsc" 23 | }, 24 | { 25 | "label": "watch", 26 | "command": "dotnet", 27 | "type": "process", 28 | "args": [ 29 | "watch", 30 | "run", 31 | "${workspaceFolder}/DrawProjectile.csproj" 32 | ], 33 | "problemMatcher": "$tsc" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/DrawProjectile.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Exe 9 | netcoreapp2.2 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/Environment.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter2.DrawProjectile 2 | { 3 | using codeclimber.raytracer; 4 | public class Environment 5 | { 6 | public Environment(Tuple gravity, Tuple wind) 7 | { 8 | Gravity = gravity; 9 | Wind = wind; 10 | } 11 | 12 | public Tuple Gravity { get; set; } 13 | public Tuple Wind { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter2.DrawProjectile 2 | { 3 | using sys = System; 4 | using codeclimber.raytracer; 5 | 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | var p = new Projectile(Tuple.Point(0, 1, 0), Tuple.Vector(1, 1.8, 0).Normalize().Multiply(11.3)); 11 | var e = new Environment(Tuple.Vector(0, -0.1, 0), Tuple.Vector(-0.01, 0, 0)); 12 | var c = new Canvas(900, 550); 13 | while (p.Position.Y >= 0) 14 | { 15 | Draw(c, p.Position); 16 | p = Tick(p, e); 17 | } 18 | c.Save("file.ppm"); 19 | } 20 | 21 | private static Projectile Tick(Projectile p, Environment e) 22 | { 23 | return new Projectile( 24 | p.Position.Add(p.Velocity), 25 | p.Velocity.Add(e.Gravity).Add(e.Wind) 26 | ); 27 | } 28 | 29 | private static void Draw(Canvas canvas, Tuple position) 30 | { 31 | var x = (int)sys.Math.Round(position.X); 32 | var y = canvas.Height - (int)sys.Math.Round(position.Y) - 1; 33 | //sys.Console.WriteLine($" {x},{y} - {p}"); 34 | if (x >= 0 && x <= canvas.Width - 1 && y >= 0 && y <= canvas.Height - 1) 35 | { 36 | canvas[x, y] = new Color(1, 0, 0); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Exercises/Chapter2/DrawProjectile/Projectile.cs: -------------------------------------------------------------------------------- 1 | namespace Chapter2.DrawProjectile 2 | { 3 | using codeclimber.raytracer; 4 | public class Projectile 5 | { 6 | public Projectile(Tuple position, Tuple velocity) 7 | { 8 | Position = position; 9 | Velocity = velocity; 10 | } 11 | 12 | public Tuple Position { get; set; } 13 | public Tuple Velocity { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | return $"{Position} (v={Velocity})"; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Exercises/Chapter2/Results/file10x10.ppm: -------------------------------------------------------------------------------- 1 | P3 2 | 10 10 3 | 255 4 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 5 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 8 | 0 0 0 0 0 0 0 0 0 255 0 0 255 0 0 255 0 0 255 0 0 0 0 0 0 0 0 0 0 0 9 | 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 10 | 0 0 0 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 11 | 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 0 0 0 0 0 0 0 0 12 | 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 255 0 0 255 0 0 0 0 0 13 | 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Simone Chiaretta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Ray Tracer Challenge... in .NET Core 2 | 3 | This repo holds the code for a ray tracer following this book: [The Ray Tracer Challenge - A Test-Driven Guide to Your First 3D Renderer](https://amzn.to/2Elaxkr). 4 | 5 | I'm blogging my adventures implementing this ray tracer on my blog: [codeclimber.net.nz](https://codeclimber.net.nz). 6 | 7 | ## Technologies used 8 | 9 | The project will be implemented with .NET Core 2.2, with no external libraries, to adhere the most to the concept of the book, which is to build everything from scratch, even features that might exist already as part of .NET Standard or in other packages, like `Color`, `Point`, image rendering and serializing. 10 | 11 | My objective is also to build everything using a Mac, the `dotnet` CLI and VS Code, to prove (mostly to myself) that this is a feasible approach, and that you can do everything you need without the full-blown Visual Studio. 12 | 13 | I'll also try to use the latest features of the C# language, whenever makes sense. 14 | 15 | The book recommends the usage of Cucumber for the testing, and even provides the full set of features in Gherkin. SpecFlow is the only Gherkin implementation for .NET, but seems like the support for .NET Core is not very advanced, and still relies on their Visual Studio plugin for generating the test files. So I'll start with plain old unit testing with xUnit, and will move to SpecFlow later. 16 | 17 | ## What I'm trying to achieve 18 | 19 | Nowadays I spend most of my time building CMS-based projects, using APIs provided by vendors, and aggregating results coming from Cloud-based services. So, mostly I want to re-discover the fun of developing something from scratch. 20 | 21 | I also want to learn new things, improve the knowledge I have and, since there is a lot math involved, re-learn things I've learned at the university. 22 | 23 | Specifically, things I hope to learn better are: 24 | 25 | * Obviously, how raytracing works; 26 | * Math, Algebra, Matrix, Vectors and similar stuff; 27 | * usage of the `dotnet` CLI; 28 | * a more advanced usa of VS Code (now I mostly use it "just" as text editor); 29 | * New features of C# (with new I mean from v6 onwards); 30 | * xUnit; 31 | * BDD with Gherkin and SpecFlow; 32 | * performance testing once everything is done. 33 | 34 | ## Repository structure 35 | 36 | This `master` branch contains the latest state of the ray tracer library and tests. Additionally, some chapters have some self-contained applications, so there will be some additional projects as well. 37 | 38 | * `Exercises`: Contains the implementation of the "Putting It Together" section at the end of each chapter. 39 | * `Chapter1`: Basic physics exercise of lauching a bullet at a given angle with gravity and wind. 40 | * `src`: Implementation of the raytracer library and the tests. 41 | * `blog-posts`: I'm documenting my learning process on my blog, and the posts are stored here. 42 | * `resources` : Collection of other material useful for the projects, like links I found useful, articles or anything else really. 43 | 44 | 45 | ## Evolution of the library 46 | 47 | To look at how the main library and test suite evolved through the book, each chapter has a branch, sometimes multiple when it makes sense. It starts with Chapter 0, which is the preparation of environment. 48 | 49 | * [Chapter 0.1](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-0.1) - Just a script that uses the `dotnet` CLI to create the project structure and makes sure everything is working fine. 50 | * [Chapter 0.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-0.2) - The basic projects (lib and xUnit test lib) with simple code to make sure testing works fine. 51 | * [Chapter 1.1](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-1.1) - Implemented basic algebra of Tuples, Vectors and Points (Equality, Sum, Subtraction, Negative). 52 | * [Chapter 1.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-1.2) - Implemented vector-specific operations, like Normalization, Magnitude, Dot and Cross Product. 53 | * [Chapter 1 End](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter1-End) - Refactored previous implementation and implemented working Exercise 1 54 | * [Chapter 2.1](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-2.1) - Implemented Color class and its operations. 55 | * [Chapter 2.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-2.2) - Implemented Canvas and serilization of images as PPM file 56 | * [Chapter 2 End](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter2-End) - Implemented Exercise 2 -------------------------------------------------------------------------------- /blog-posts/1-intro.md: -------------------------------------------------------------------------------- 1 | # The Ray Tracer Challenge... in .NET Core 2 | 3 | I just bought book [The Ray Tracer Challenge - A Test-Driven Guide to Your First 3D Renderer](https://amzn.to/2Elaxkr) and in the upcoming months I'll be developing my own ray tracer, in .NET Core. 4 | 5 | I will also document my learning experince on the blog, for me, to keep track of my progress, and maybe discuss some implementation decision with you, my readers, but also to share what I learn in the process. 6 | 7 | ## Why am I doing it 8 | 9 | Nowadays I spend most of my time building CMS-based projects, using APIs provided by vendors, and aggregating results coming from Cloud-based services. And I stopped enjoying development. 10 | 11 | I needed a change, I needed to find back my love for developing, and I needed to challenge myself to learn something new. The book teaches the theory of a ray tracing, and, using a TDD approach, makes you implement, from scratch, all the pieces needed: starting from basic vector and matrix computations, to image processing and storing, and finally going into the real-deal of rendering 3D scenes from scratch. 12 | 13 | Rebuilding all the basic primitives might sound silly at first, in the current development world were developers reuse code and packages even for simple tasks as adding padding to strings. But the goal of the book is to help you redescover the fun of developing from scrath, doing things yourself, without relying on stuff done by others. 14 | 15 | ## Technologies used 16 | 17 | The project will be implemented with .NET Core 2.2, with no external libraries, to adhere the most to the concept of the book, which is to build everything from scratch, even features that might exist already as part of .NET Standard or in other packages, like `Color`, `Point`, image rendering and serializing. 18 | 19 | My objective is also to build everything using a Mac, the `dotnet` CLI and VS Code, to prove (mostly to myself) that this is a feasible approach, and that you can do everything you need without the full-blown Visual Studio. 20 | 21 | I'll also try to use the latest features of the C# language, whenever makes sense. 22 | 23 | The book recommends the usage of Cucumber for the testing, and even provides the full set of features in Gherkin. SpecFlow is the only Gherkin implementation for .NET, but seems like the support for .NET Core is not very advanced, and still relies on their Visual Studio plugin for generating the test files. So I'll start with plain old unit testing with xUnit, and will move to SpecFlow later. 24 | 25 | ## What I'll be learning 26 | 27 | Such a project will involve a lot of learning: I will learn new things, improve the knowledge I have and, since there is a lot of math involved, I will re-learn things I've learned at the university. 28 | 29 | Specifically, things I hope to learn better are: 30 | 31 | * Obviously, how raytracing works; 32 | * Math, Algebra, Matrix, Vectors and similar stuff; 33 | * usage of the `dotnet` CLI; 34 | * a more advanced usa of VS Code (now I mostly use it "just" as text editor); 35 | * New features of C# (with new I mean from v6 onwards); 36 | * xUnit; 37 | * BDD with Gherkin and SpecFlow. 38 | 39 | ## Show me the code 40 | 41 | All code is hosted on Github: https://github.com/simonech/ray-tracer-challenge-netcore. 42 | 43 | The repository will also include the posts of the series. To familiarize yourself with the structure of the repository, please have a look at the [README](https://github.com/simonech/ray-tracer-challenge-netcore/blob/master/README.md) file. 44 | 45 | ## Table of Content 46 | 47 | I'll try to write a post at least once per chapter of the book, and document how things are going, the challenges I faced and what I learend. I will update this table of content with every post published 48 | 49 | * The Ray Tracer Challenge... in .NET Core - Introduction (this post) 50 | * [The Ray Tracer Challenge - Setting up the project]() 51 | 52 | I hope you will subscribe to my blog (if you don't have already) and will learn together with my how implementing ray tracing in .NET Core. -------------------------------------------------------------------------------- /blog-posts/2-Setting-up-the-environment.md: -------------------------------------------------------------------------------- 1 | # The Ray Tracer Challenge - Setting up the project 2 | 3 | Here it comes, the first post of my journey developing a Ray Tracer using .NET Core on a Mac, using only VS Code. If you didn't, I recommend you read my [introductory post](http://codeclimber.net.nz/archive/2019/05/22/raytracer-challenge-netcore-intro/) which explains what I'm trying to achieve and why. 4 | 5 | In this first post I'll talk about the libraries and tools I decided to use, and how I setup the project structure using the `dotnet` CLI. 6 | 7 | ## Tools used 8 | 9 | The only two tools I'm using are VS Code and the `dotnet` CLI. Let's see how I configured the two. 10 | 11 | ### VS Code 12 | 13 | As I mentioned already, I'm using VS Code, with the basic [C#/Omnisharp extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) that comes when you download VS Code. In addition, I'm using two other extensions: 14 | 15 | * the [C# Extensions](https://marketplace.visualstudio.com/items?itemName=jchannon.csharpextensions), which adds some nice contextual menu and some additional refactoring; 16 | * the [.NET Core Test Explorer](https://marketplace.visualstudio.com/items?itemName=formulahendry.dotnet-test-explorer), which adds a tree-view UI for all the tests in the workspace. 17 | 18 | ### dotnet CLI 19 | 20 | The other tool I use, which comes with .NET Core, is the `dotnet` CLI. One thing I learned while going through the documentation is that it supports TAB completion, but, in order to have it, it must be configured. You can follow the steps on the page "[How to enable TAB completion for .NET Core CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/enable-tab-autocomplete)" in .NET Core doc site. 21 | 22 | But, in short, add the following code to the `~/.bashrc` file. 23 | 24 | ``` 25 | # bash parameter completion for the dotnet CLI 26 | 27 | _dotnet_bash_complete() 28 | { 29 | local word=${COMP_WORDS[COMP_CWORD]} 30 | 31 | local completions 32 | completions="$(dotnet complete --position "${COMP_POINT}" "${COMP_LINE}")" 33 | 34 | COMPREPLY=( $(compgen -W "$completions" -- "$word") ) 35 | } 36 | 37 | complete -f -F _dotnet_bash_complete dotnet 38 | ``` 39 | 40 | Normally Linux-based systems will run this script every time the terminal is opened. But default MacOS Terminal app runs another file, the `~/.bash_profile` (which is normally executed only at login), but other terminal GUIs might behave differently, so it's better to add the script in both places. You either copy the same code also in this file or just include the following lines in the `.bash_profile` file, so that it calls automatically the `.bashrc` one. 41 | 42 | ``` 43 | if [ -f ~/.bashrc ]; then 44 | source ~/.bashrc 45 | fi 46 | ``` 47 | 48 | ## Libraries used: xUnit 49 | 50 | I'm pretty sure some NuGet packages already exist to compute vector and matrix computations, and for saving images. But that would defeat the point of the whole exercise. The only external library I'm referencing is the testing framework. 51 | 52 | The book already provides the full test suite in Gherkin format to be used with Cucumber, so the obvious choice, to save time, would be to use the .NET counterpart of Cucumber: [SpecFlow](https://specflow.org/). But despite [supporting .NET Core](https://specflow.org/2019/specflow-3-is-here/), it seems that they rely heavily on their Visual Studio plugin in order to parse the feature files and generating the executable tests. I found some posts explaining how to do it without, but I'd rather start coding instead of spending time working around limitations of one specific testing framework. So I'll just start with xUnit, and after the first few chapters are completed, go back and re-implement the tests. 53 | 54 | ## Creating the project structure using the dotnet CLI 55 | 56 | The project at the moment is going to be pretty simple: just two projects (one .NET Standard class lib and one test project) and a solution to link them together. 57 | 58 | The first bit I learned in this process: the `dotnet` CLI is awesome: you can create solution files, projects and also adding projects to solutions and adding references from one project to another. All without any manual editing of the files. 59 | 60 | ``` 61 | dotnet new sln -o src -n ray-tracer-challenge 62 | cd src 63 | dotnet new classlib -o raytracer -n codeclimber.raytracer 64 | dotnet new xunit -o xUnit -n codeclimber.raytracer.xUnit 65 | dotnet sln add **/*.csproj 66 | cd xUnit/ 67 | dotnet add reference ../raytracer/codeclimber.raytracer.csproj 68 | cd .. 69 | dotnet build 70 | dotnet test 71 | ``` 72 | 73 | This list of commands creates a solution inside a folder called `src` and the 2 projects in their own sub-folders. Then they add them to the solution, add to the testing project a reference to the class library, and finally, build and run the tests. 74 | 75 | You can see the code at this point in time by going on the Github repo of this project and switching to the two branches: 76 | 77 | * [Chapter 0.1](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-0.1) - Just a script that uses the `dotnet` CLI to create the project structure and makes sure everything is working fine. 78 | * [Chapter 0.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-0.2) - The basic projects (lib and xUnit test lib) with simple code to make sure testing works fine. 79 | 80 | ## Conclusion 81 | 82 | Now that the environment is setup correctly, in the next post I'll start implementing the exercises of the first chapter of the book [The Ray Tracer Challenge - A Test-Driven Guide to Your First 3D Renderer](https://amzn.to/2Elaxkr). 83 | 84 | You can see the full list of posts in the [introduction to the series](/archive/2019/05/22/raytracer-challenge-netcore-intro/). -------------------------------------------------------------------------------- /blog-posts/3-Implementing-primitives-math.md: -------------------------------------------------------------------------------- 1 | # The Ray Tracer Challenge - Implementing primitives and vector algebra operations 2 | 3 | After setting up the project, it's time to start implementing the code for the first chapter of the book, which focuses mostly on defining primitives (Point and Vectors), and vector algebra operations like addition, subtraction and also dot and cross products. 4 | 5 | ## Challenges encountered 6 | 7 | At first, it looked a simple task: how hard could it be to write some methods to perform simple addition, subtractions, and some other basic formulas? 8 | 9 | But it was not. 10 | 11 | ### "Re-learning" vector algebra 12 | 13 | The first main challenge was "re-learning" vector algebra, and the meaning of the various operations. The book explained well enough for the purpose of the implementation, but I wanted to understand a bit more, and Wikipedia helped a lot. Here some of the links I found: 14 | 15 | * [Euclidean vector](https://en.wikipedia.org/wiki/Euclidean_vector) 16 | * [Dot product](https://en.wikipedia.org/wiki/Dot_product) 17 | * [Cross product](https://en.wikipedia.org/wiki/Cross_product) 18 | 19 | ### Modeling the primitives 20 | 21 | The main primitive in the 3D space is a Tuple (of 4 elements), with the 3 coordinates (`x`, `y`, `z`) and a value (`w`) to identify it is either a vector (`w=0`) or a point (`w=1`). I don't know if this is a standard approach or just something the author of the book came up with. Additionally, in chapter 1 the reasoning is not explained but refers to a later chapter to better understand the meaning of this abstraction. 22 | 23 | In order to simplify the creation points and vectors, as tuples with the correct value for `w`, it suggests creating factory methods. 24 | 25 | But the book uses pseudocode so that the ray tracer can be implemented in any language, both procedural and object-oriented. For this reason, no additional advice is given on how to actually implement it. 26 | 27 | I tried a few approaches. 28 | 29 | #### Inheritance 30 | 31 | Instead of creating factory methods, since C# is an object-oriented language, I implemented the primitives as a `Tuple` base class, that implements all common operations, and two subclasses, `Point` and `Vector`, whose constructor sets the correct value for the `w` parameter. 32 | 33 | ``` 34 | public class Tuple 35 | { 36 | public Tuple(double x, double y, double z, double w) 37 | { 38 | X = x; 39 | Y = y; 40 | Z = z; 41 | W = w; 42 | } 43 | 44 | public double X { get; set; } 45 | public double Y { get; set; } 46 | public double Z { get; set; } 47 | public double W { get; set; } 48 | 49 | public Tuple Add(Tuple other) 50 | { 51 | return new Tuple( 52 | X + other.X, 53 | Y + other.Y, 54 | Z + other.Z, 55 | W + other.W 56 | ); 57 | } 58 | 59 | // All other operations 60 | ... 61 | } 62 | 63 | public class Point : Tuple 64 | { 65 | public Point(double x, double y, double z) : base(x, y, z, 1.0f) { } 66 | } 67 | 68 | public class Vector : Tuple 69 | { 70 | public Vector(double x, double y, double z) : base(x, y, z, 0) { } 71 | } 72 | 73 | ``` 74 | 75 | It all went fine in the implementation of the library, but things started to fail in the first exercise. The reason is that adding a `Point` to a `Vector` should produce a `Point`. But with this inheritance scheme, that addition will create a `Tuple` that cannot be cast to a `Point`. It took me a while to understand that there is no way around this problem (it's actually how inheritance works). 76 | 77 | A `Projectile` (the exercise is a simple ballistic trajectory calculator) is defined as: 78 | 79 | ``` 80 | public Projectile(Point position, Vector velocity) 81 | { 82 | Position = position; 83 | Velocity = velocity; 84 | } 85 | ``` 86 | 87 | When updating the position of the projectile after one *tick* you have to add the current position to the current velocity. The result is logically another point (the new `w` is `1+0=1`), but the `Add` method returns a new `Tuple`, and this cannot be cast to a `Point` later on. 88 | 89 | ``` 90 | public static Projectile Update(Projectile p, Environment e) 91 | { 92 | return new Projectile( 93 | p.Position.Add(p.Velocity), 94 | p.Velocity.Add(e.Gravity).Add(e.Wind) 95 | ); 96 | } 97 | ``` 98 | 99 | You can see far I went before getting stuck, on my repo, on branch [Chapter 1.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-1.2). 100 | 101 | It's a pity this doesn't work because that would have allowed the most code re-use and keeping point and vector as two separate entities. 102 | 103 | #### Inner value 104 | 105 | I then started looking for another solution, to be able to keep the two separate objects, and I implemented them using the "inner value" approach (not sure it's the right name): both `Point` and `Vector` have an "innerValue" object of type `Tuple` on which all operations are done. 106 | 107 | ``` 108 | public class Tuple 109 | { 110 | public Tuple(double x, double y, double z, double w) 111 | { 112 | X = x; 113 | Y = y; 114 | Z = z; 115 | W = w; 116 | } 117 | 118 | public double X { get; set; } 119 | public double Y { get; set; } 120 | public double Z { get; set; } 121 | public double W { get; set; } 122 | 123 | public Tuple Add(Tuple other) 124 | { 125 | return new Tuple( 126 | X + other.X, 127 | Y + other.Y, 128 | Z + other.Z, 129 | W + other.W 130 | ); 131 | } 132 | 133 | // All other operations 134 | ... 135 | } 136 | 137 | public class Point 138 | { 139 | private Tuple _innerValue; 140 | 141 | public Tuple InnerValue { get => _innerValue; set => _innerValue = value; } 142 | 143 | public Point(double x, double y, double z) 144 | { 145 | InnerValue = new Tuple(x, y, z, 1); 146 | } 147 | 148 | public Point(Tuple tuple) 149 | { 150 | InnerValue = tuple; 151 | } 152 | 153 | public double X { get => _innerValue.X; set => _innerValue.X = value; } 154 | public double Y { get => _innerValue.Y; set => _innerValue.Y = value; } 155 | public double Z { get => _innerValue.Z; set => _innerValue.Z = value; } 156 | 157 | public Point Add(Vector other) 158 | { 159 | return new Point(InnerValue.Add(other.InnerValue)); 160 | } 161 | // All other operations 162 | } 163 | 164 | public class Vector 165 | { 166 | private Tuple _innerValue; 167 | 168 | public Tuple InnerValue { get => _innerValue; set => _innerValue = value; } 169 | 170 | public Vector(double x, double y, double z) 171 | { 172 | InnerValue = new Tuple(x, y, z, 0); 173 | } 174 | 175 | public Vector(Tuple tuple) 176 | { 177 | InnerValue = tuple; 178 | } 179 | 180 | public double X { get => _innerValue.X; set => _innerValue.X = value; } 181 | public double Y { get => _innerValue.Y; set => _innerValue.Y = value; } 182 | public double Z { get => _innerValue.Z; set => _innerValue.Z = value; } 183 | 184 | public Vector Add(Vector other) 185 | { 186 | return new Vector(InnerValue.Add(other.InnerValue)); 187 | } 188 | 189 | public Vector Add(Point other) 190 | { 191 | return new Vector(InnerValue.Add(other.InnerValue)); 192 | } 193 | 194 | //All other operations 195 | 196 | } 197 | 198 | ``` 199 | 200 | This approach works, but, unlike the previous approach, I had to reimplement all methods, with the logic of the operation, was delegated to the `Tuple`. The advantage of this approach is that it is possible to limit the operations (for example, you cannot add 2 points, or subtract a point from a vector) and specify the correct return type (the sum of two vectors is a vector, but the sum of a point and a vector is a point). 201 | 202 | This approach works, but it doesn't feel right. Too much code duplication. 203 | 204 | You can see how this was implemented on the branch [inner-element](https://github.com/simonech/ray-tracer-challenge-netcore/tree/inner-element). 205 | 206 | #### Using factory methods 207 | 208 | Finally, I tried using to the suggestion of the author, and I deleted the two classes, only keeping the `Tuple`, to which I added the 2 static factory methods to set the `w` parameter for points (`w=1`) and vectors (`w=0`). The disadvantage is that I lose the possibility of limiting some operations to either point or vectors and that I cannot easily understand if a tuple is a point or a vector just by reading the code. 209 | 210 | ``` 211 | public class Tuple 212 | { 213 | public static Tuple Point(double x, double y, double z) 214 | { 215 | return new Tuple(x, y, z, 1); 216 | } 217 | 218 | public static Tuple Vector(double x, double y, double z) 219 | { 220 | return new Tuple(x, y, z, 0); 221 | } 222 | 223 | public Tuple(double x, double y, double z, double w) 224 | { 225 | X = x; 226 | Y = y; 227 | Z = z; 228 | W = w; 229 | } 230 | 231 | public double X { get; set; } 232 | public double Y { get; set; } 233 | public double Z { get; set; } 234 | public double W { get; set; } 235 | 236 | public bool IsPoint { get => W == 1; } 237 | public bool IsVector { get => W == 0; } 238 | 239 | public Tuple Add(Tuple other) 240 | { 241 | return new Tuple( 242 | X + other.X, 243 | Y + other.Y, 244 | Z + other.Z, 245 | W + other.W 246 | ); 247 | } 248 | 249 | //All other operations 250 | 251 | } 252 | ``` 253 | 254 | This book adopts a TTD approach, so a data structure that works now might need some refactoring to support new features. For this reason, I finally decided to use the approach suggested by the author, of using factory methods. I will possibly come back on this decision later in the book when concepts become more clear. 255 | 256 | You can see how this was implemented on the branch [factory-methods](https://github.com/simonech/ray-tracer-challenge-netcore/tree/factory-methods). 257 | 258 | ### Which data type to use 259 | 260 | The book suggests using a "native" floating point data type since these values will be used a lot. I decided to use a `double`, even if probably a `float` would be enough, to avoid lots of casting given that all the `System.Math` methods work with `Double` types. And also I'm lazy and I don't want to add `f` every time I want to specify a value as literal (just `1.7` vs `1.7f`). 261 | 262 | ### Performances 263 | 264 | I already see some performance issues in this code: creating a new object every time an operation is done will create zillions of objects, and memory will increase. 265 | 266 | Also using fields instead of properties, or using stuct instead of classes might bring some performance improvement. 267 | 268 | ## Putting it all together 269 | 270 | As already mentioned above, the final exercise was about the "Hello World" exercise of vector and physics: the trajectory of a bullet shot with an initial velocity and subject to gravity and wind. 271 | 272 | ``` 273 | using codeclimber.raytracer; 274 | public class Environment 275 | { 276 | public Environment(Tuple gravity, Tuple wind) 277 | { 278 | Gravity = gravity; 279 | Wind = wind; 280 | } 281 | 282 | public Tuple Gravity { get; set; } 283 | public Tuple Wind { get; set; } 284 | } 285 | 286 | public class Projectile 287 | { 288 | public Projectile(Tuple position, Tuple velocity) 289 | { 290 | Position = position; 291 | Velocity = velocity; 292 | } 293 | 294 | public Tuple Position { get; set; } 295 | public Tuple Velocity { get; set; } 296 | 297 | public override string ToString() 298 | { 299 | return $"{Position} (v={Velocity})"; 300 | } 301 | } 302 | 303 | class Program 304 | { 305 | static void Main(string[] args) 306 | { 307 | var p = new Projectile(Tuple.Point(0, 0, 0), Tuple.Vector(1, 1, 0).Normalize()); 308 | var e = new Environment(Tuple.Vector(0, -0.1, 0), Tuple.Vector(-0.01, 0, 0)); 309 | int i = 0; 310 | while (p.Position.Y >= 0) 311 | { 312 | i++; 313 | sys.Console.WriteLine($"{i} - {p}"); 314 | p = Update(p, e); 315 | } 316 | } 317 | 318 | public static Projectile Update(Projectile p, Environment e) 319 | { 320 | return new Projectile( 321 | p.Position.Add(p.Velocity), 322 | p.Velocity.Add(e.Gravity).Add(e.Wind) 323 | ); 324 | } 325 | } 326 | 327 | ``` 328 | 329 | The code is that simple that it just prints out the list of coordinates. 330 | 331 | ![Program output](images/3-text-output.png) 332 | 333 | To see I got it right, I imported the result in excel and drawn a chart. 334 | 335 | ![Program output](images/3-ballistic-trajectory.png) 336 | 337 | On my github repository you can have a look at the [code of the exercise](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter1-End/Exercises/Chapter%201), and the output, both [text](https://github.com/simonech/ray-tracer-challenge-netcore/blob/Chapter1-End/Exercises/Chapter%201/Results/trajectory.txt) and [Excel](https://github.com/simonech/ray-tracer-challenge-netcore/blob/Chapter1-End/Exercises/Chapter%201/Results/Projectile-trajectory.xlsx). 338 | 339 | ## Conclusions 340 | 341 | The basic operations are now implemented. The next chapter in the book is about implementing the code to write images on disk, in [PPM format](https://en.wikipedia.org/wiki/Netpbm_format) (I suppose the easiest format to implement). 342 | 343 | What do you think? Do you think there is a more elegant way to deal with `Point` and `Vector` other than using the two static factory methods? Anything else? 344 | 345 | I've received already some comments from some fellow developer which is also doing this challenge: he suggested that I could use the new `System.MathF` class introduced in .NET Core 2.1, to use floats instead of the double (but I'd still have to type the `f` in literals). He also suggested I use operator overload instead of the methods `Add`, `Subtract`, and so on. 346 | 347 | I'll address these two comments and I'll do a bit of performance optimization before starting with calling these simple operation zillion times. -------------------------------------------------------------------------------- /blog-posts/4-writing-images.md: -------------------------------------------------------------------------------- 1 | # The Ray Tracer Challenge - Drawing to a canvas and saving the image to a file 2 | 3 | In the previous post, I've shown how I implemented chapter 1 of the Ray Tracing Challenge book, coding the basic primitives and their operations. And also implemented a ballistic trajectory calculator. In chapter 2, the goal is to be able to draw an image on a canvas and then saving it to a file. 4 | 5 | ## Challenges encountered 6 | 7 | This was a more straightforward exercise than the one in the previous chapter, but still, not everything was simple. 8 | 9 | ### Modeling the Color class 10 | 11 | The concept behind drawing images is very simple. An image is a matrix of pixels, and the value of each pixel is a color. And a color is a "tuple" of three values (the Red, Green, Blue components). 12 | 13 | For this reason, the author suggests refactoring the implementation of the `Tuple` so that there is no need to reimplement the basic operations (add, subtract and multiplication by a scalar). I thought of creating a `TupleBase` class, which extends `Array` and implement those 3 operations on the array. Unfortunately, in C#, the `Array` is a sealed class, and cannot be used as base class. I thought of using a `List`, but it would probably with a very high-performance cost. I finally decided to just duplicate the code for the 3 operations, and come back to it after this chapter is done. 14 | 15 | ``` 16 | public class Color 17 | { 18 | public static readonly Color Black = new Color(0, 0, 0); 19 | 20 | public Color(double r, double g, double b) 21 | { 22 | Red = r; 23 | Green = g; 24 | Blue = b; 25 | } 26 | 27 | public double Red { get; set; } 28 | public double Green { get; set; } 29 | public double Blue { get; set; } 30 | 31 | //All operations 32 | } 33 | ``` 34 | 35 | On my GitHub repository, you see the implementation till this point going the tag [Chapter 2.1](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-2.1). 36 | 37 | ### Modeling the Canvas class 38 | 39 | The `Canvas` class was pretty straightforward to implement. I used a bi-dimensional array to store the values and implemented an index accessor for setting and reading the values. 40 | 41 | ``` 42 | public class Canvas 43 | { 44 | private Color[,] canvas; 45 | 46 | public Canvas(int width, int height) : this(width, height, Color.Black) 47 | { 48 | 49 | } 50 | public Canvas(int width, int height, Color background) 51 | { 52 | Width = width; 53 | Height = height; 54 | canvas = new Color[width, height]; 55 | Initialize(background); 56 | } 57 | 58 | public int Width { get; set; } 59 | public int Height { get; set; } 60 | 61 | public Color this[int x, int y] 62 | { 63 | get 64 | { 65 | return canvas[x, y]; 66 | } 67 | 68 | set 69 | { 70 | canvas[x, y] = value; 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | This indexer works fine, but intellisense doesn't show the name of the variables, and while writing the exercise I wish I wrote an explicit method to set the color of a pixel, like `Canvas.SetPixelAt(int x, int y, Color color)`. 77 | 78 | On my GitHub repository, you see the implementation of the canvas on the tag [Chapter 2.2](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter-2.2). 79 | 80 | Finally, while writing this post I realized I could have used the same approach also for the base tuple class. I guess now I know how to refactor the implementation. 81 | 82 | ### Implementing the PPM writer 83 | 84 | The PPM file format is the most basic file format existing. Each pixel is represented with a string with the three components. If a pixel is white, it will be represented as `255 255 255 `, 12 bytes per pixel. While a black pixel will be 6 bytes. This makes it probably the most inefficient image format existing. I guess in my "refactoring sprint", I'll try to implement a more efficient format. 85 | 86 | Each row of pixels is represented as a series of lines, of max 70 chars, so the code for rendering must also take care of this limit. 87 | 88 | ``` 89 | public string GetPPMContent() 90 | { 91 | var builder = new StringBuilder(); 92 | builder.AppendLine("P3"); 93 | builder.AppendLine($"{Width} {Height}"); 94 | builder.AppendLine("255"); 95 | for (int y = 0; y < Height; y++) 96 | { 97 | int lineLength = 0; 98 | for (int x = 0; x < Width; x++) 99 | { 100 | string[] colorA = canvas[x, y].ToRGBA(); 101 | foreach (var color in colorA) 102 | { 103 | if (lineLength + 1 + color.Length > 70) 104 | { 105 | builder.AppendLine(); 106 | lineLength = 0; 107 | } 108 | if(lineLength!=0) 109 | { 110 | builder.Append(" "); 111 | lineLength++; 112 | } 113 | builder.Append(color); 114 | lineLength += color.Length; 115 | } 116 | } 117 | builder.AppendLine(); 118 | } 119 | return builder.ToString(); 120 | } 121 | ``` 122 | 123 | This algorithm works but doesn't feel right. Probably there is a more elegant way of doing it. More food for my refactoring sprint. 124 | 125 | ## Putting it all together 126 | 127 | With those basic image drawing methods implemented, the exercise for chapter 2 was drawing the trajectory implemented in the previous chapter. This was a pretty simple exercise. Only needed to make sure the trajectory felt inside the canvas. 128 | 129 | Here is the code for the method that plots the position of the projectile on the canvas, in red. 130 | 131 | ``` 132 | private static void Draw(Canvas canvas, Tuple position) 133 | { 134 | var x = (int)sys.Math.Round(position.X); 135 | var y = canvas.Height - (int)sys.Math.Round(position.Y) - 1; 136 | //sys.Console.WriteLine($" {x},{y} - {position}"); 137 | if (x >= 0 && x <= canvas.Width - 1 && y >= 0 && y <= canvas.Height - 1) 138 | { 139 | canvas[x, y] = new Color(1, 0, 0); 140 | } 141 | } 142 | ``` 143 | 144 | The complex part of the exercise was finding some initial values that would produce something visible. The values used in chapter 1 would have fit in a 10 by 10 image. 145 | 146 | ``` 147 | var p = new Projectile(Tuple.Point(0, 0, 0), Tuple.Vector(1, 1, 0).Normalize()); 148 | var e = new Environment(Tuple.Vector(0, -0.1, 0), Tuple.Vector(-0.01, 0, 0)); 149 | var c = new Canvas(10, 10); 150 | ``` 151 | 152 | ![10 by 10](images/4-trajectory-10x10.png) 153 | 154 | To generate something more visible I had to increase the velocity of the projectile by 11 (while keeping gravity and wind the same) and launched it at a steeper angle. And with these values, the chart fits into an image which is 900 per 550. 155 | 156 | ``` 157 | var p = new Projectile(Tuple.Point(0, 1, 0), Tuple.Vector(1, 1.8, 0).Normalize().Multiply(11.3)); 158 | var e = new Environment(Tuple.Vector(0, -0.1, 0), Tuple.Vector(-0.01, 0, 0)); 159 | var c = new Canvas(900, 550); 160 | ``` 161 | 162 | ![900 by 550](images/4-trajectory-900x500.png) 163 | 164 | The trajectory is not very visible 165 | 166 | On my GitHub repository, you can have a look at the [code of the exercise](https://github.com/simonech/ray-tracer-challenge-netcore/tree/Chapter2-End/Exercises/Chapter%202), and the output, both images, the [10x10](https://github.com/simonech/ray-tracer-challenge-netcore/blob/Chapter2-End/Exercises/Chapter%202/Results/file-10x10.ppm) and [900x550](https://github.com/simonech/ray-tracer-challenge-netcore/blob/Chapter2-End/Exercises/Chapter%202/Results/file-900x500.ppm) sizes. 167 | 168 | ## Conclusions 169 | 170 | This chapter was easier to implement than the previous one, but there is still something I'd love to improve. Specifically, I'd like to refactor the `Color` implementation as child class of a `TupleBase` shared with the other `Tuple` class for point and vectors. Also, see if I can make the PPM serialization code a bit nicer and find a more storage efficient file format. 171 | 172 | Together with these tasks, I also want to go back to the basic primitives and implement math as operations overloads and do some performance testing to find the most performing data storage solution. 173 | 174 | If you like this series of posts and don't want to miss my next episode, please consider subscribing. -------------------------------------------------------------------------------- /blog-posts/images/1-raytracer-challenge-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/1-raytracer-challenge-cover.jpg -------------------------------------------------------------------------------- /blog-posts/images/2-initial-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/2-initial-structure.png -------------------------------------------------------------------------------- /blog-posts/images/3-ballistic-trajectory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/3-ballistic-trajectory.png -------------------------------------------------------------------------------- /blog-posts/images/3-text-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/3-text-output.png -------------------------------------------------------------------------------- /blog-posts/images/4-trajectory-10x10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/4-trajectory-10x10.png -------------------------------------------------------------------------------- /blog-posts/images/4-trajectory-900x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simonech/ray-tracer-challenge-netcore/f29062efd50c6da783f2f245b01d61fb7491c78d/blog-posts/images/4-trajectory-900x500.png -------------------------------------------------------------------------------- /create-structure.sh: -------------------------------------------------------------------------------- 1 | dotnet new sln -o src -n ray-tracer-challenge 2 | cd src 3 | dotnet new classlib -o raytracer -n codeclimber.raytracer 4 | dotnet new xunit -o xUnit -n codeclimber.raytracer.xUnit 5 | dotnet sln add **/*.csproj 6 | cd xUnit/ 7 | dotnet add reference ../raytracer/codeclimber.raytracer.csproj 8 | cd .. 9 | dotnet build 10 | dotnet test -------------------------------------------------------------------------------- /resources/useful-links.md: -------------------------------------------------------------------------------- 1 | 2 | Implementation of simple raytracer in .NET Core (Pixar simple raytracer) 3 | Explanation http://fabiensanglard.net/postcard_pathtracer/index.html 4 | Code https://gist.github.com/mattwarren/d17a0c356bd6fdb9f596bee6b9a5e63c 5 | Optimization: https://mattwarren.org/2019/03/01/Is-CSharp-a-low-level-language/ -------------------------------------------------------------------------------- /src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/xUnit/bin/Debug/netcoreapp2.2/codeclimber.raytracer.xUnit.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/xUnit", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /src/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/xUnit/codeclimber.raytracer.xUnit.csproj" 11 | ], 12 | "problemMatcher": "$tsc" 13 | }, 14 | { 15 | "label": "publish", 16 | "command": "dotnet", 17 | "type": "process", 18 | "args": [ 19 | "publish", 20 | "${workspaceFolder}/xUnit/codeclimber.raytracer.xUnit.csproj" 21 | ], 22 | "problemMatcher": "$tsc" 23 | }, 24 | { 25 | "label": "watch", 26 | "command": "dotnet", 27 | "type": "process", 28 | "args": [ 29 | "watch", 30 | "run", 31 | "${workspaceFolder}/xUnit/codeclimber.raytracer.xUnit.csproj" 32 | ], 33 | "problemMatcher": "$tsc" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /src/ray-tracer-challenge.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "codeclimber.raytracer", "raytracer\codeclimber.raytracer.csproj", "{05B48B88-1E9C-45B2-AC43-1DAEE33B263B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "codeclimber.raytracer.xUnit", "xUnit\codeclimber.raytracer.xUnit.csproj", "{2CDB73F4-8ECD-4B78-A08E-95D416C569B2}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Debug|x64 = Debug|x64 14 | Debug|x86 = Debug|x86 15 | Release|Any CPU = Release|Any CPU 16 | Release|x64 = Release|x64 17 | Release|x86 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|x64.ActiveCfg = Debug|Any CPU 26 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|x64.Build.0 = Debug|Any CPU 27 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|x86.ActiveCfg = Debug|Any CPU 28 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Debug|x86.Build.0 = Debug|Any CPU 29 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|x64.ActiveCfg = Release|Any CPU 32 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|x64.Build.0 = Release|Any CPU 33 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|x86.ActiveCfg = Release|Any CPU 34 | {05B48B88-1E9C-45B2-AC43-1DAEE33B263B}.Release|x86.Build.0 = Release|Any CPU 35 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|x64.ActiveCfg = Debug|Any CPU 38 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|x64.Build.0 = Debug|Any CPU 39 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|x86.ActiveCfg = Debug|Any CPU 40 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Debug|x86.Build.0 = Debug|Any CPU 41 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|x64.ActiveCfg = Release|Any CPU 44 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|x64.Build.0 = Release|Any CPU 45 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|x86.ActiveCfg = Release|Any CPU 46 | {2CDB73F4-8ECD-4B78-A08E-95D416C569B2}.Release|x86.Build.0 = Release|Any CPU 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /src/raytracer/Canvas.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer 2 | { 3 | using System.Text; 4 | using System.IO; 5 | 6 | public class Canvas 7 | { 8 | private Color[,] canvas; 9 | 10 | public Canvas(int width, int height) : this(width, height, Color.Black) 11 | { 12 | 13 | } 14 | public Canvas(int width, int height, Color background) 15 | { 16 | Width = width; 17 | Height = height; 18 | canvas = new Color[width, height]; 19 | Initialize(background); 20 | } 21 | 22 | public int Width { get; set; } 23 | public int Height { get; set; } 24 | 25 | public Color this[int x, int y] 26 | { 27 | get 28 | { 29 | return canvas[x, y]; 30 | } 31 | 32 | set 33 | { 34 | canvas[x, y] = value; 35 | } 36 | } 37 | 38 | public string GetPPMContent() 39 | { 40 | var builder = new StringBuilder(); 41 | builder.AppendLine("P3"); 42 | builder.AppendLine($"{Width} {Height}"); 43 | builder.AppendLine("255"); 44 | for (int y = 0; y < Height; y++) 45 | { 46 | int lineLength = 0; 47 | for (int x = 0; x < Width; x++) 48 | { 49 | string[] colorA = canvas[x, y].ToRGBA(); 50 | foreach (var color in colorA) 51 | { 52 | if (lineLength + 1 + color.Length > 70) 53 | { 54 | builder.AppendLine(); 55 | lineLength = 0; 56 | } 57 | if(lineLength!=0) 58 | { 59 | builder.Append(" "); 60 | lineLength++; 61 | } 62 | builder.Append(color); 63 | lineLength += color.Length; 64 | } 65 | } 66 | builder.AppendLine(); 67 | } 68 | return builder.ToString(); 69 | } 70 | 71 | public void Save(string filename) 72 | { 73 | File.WriteAllText(filename,GetPPMContent()); 74 | } 75 | 76 | private void Initialize(Color color) 77 | { 78 | for (int x = 0; x < Width; x++) 79 | { 80 | for (int y = 0; y < Height; y++) 81 | { 82 | canvas[x, y] = color; 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/raytracer/Color.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer 2 | { 3 | using System; 4 | public class Color 5 | { 6 | public static readonly Color Black = new Color(0, 0, 0); 7 | 8 | public Color(double r, double g, double b) 9 | { 10 | Red = r; 11 | Green = g; 12 | Blue = b; 13 | } 14 | 15 | public double Red { get; set; } 16 | public double Green { get; set; } 17 | public double Blue { get; set; } 18 | 19 | public Color Add(Color other) 20 | { 21 | return new Color( 22 | Red + other.Red, 23 | Green + other.Green, 24 | Blue + other.Blue 25 | ); 26 | } 27 | 28 | public Color Subtract(Color other) 29 | { 30 | return new Color( 31 | Red - other.Red, 32 | Green - other.Green, 33 | Blue - other.Blue 34 | ); 35 | } 36 | 37 | public Color Multiply(double multiplier) 38 | { 39 | return new Color( 40 | Red * multiplier, 41 | Green * multiplier, 42 | Blue * multiplier 43 | ); 44 | } 45 | 46 | public Color Multiply(Color other) 47 | { 48 | return new Color( 49 | Red * other.Red, 50 | Green * other.Green, 51 | Blue * other.Blue 52 | ); 53 | } 54 | 55 | public string ToRGB() 56 | { 57 | return $"{Normalize(Red)} {Normalize(Green)} {Normalize(Blue)}"; 58 | } 59 | 60 | public string[] ToRGBA() 61 | { 62 | return new string[] {Normalize(Red).ToString(), Normalize(Green).ToString(), Normalize(Blue).ToString()}; 63 | } 64 | 65 | public override bool Equals(object obj) 66 | { 67 | Color t = obj as Color; 68 | if (t == null) 69 | { 70 | return false; 71 | } 72 | else 73 | { 74 | return (t.Red.EqualsD(Red) 75 | && t.Green.EqualsD(Green) 76 | && t.Blue.EqualsD(Blue)); 77 | } 78 | } 79 | 80 | private int Normalize(double comp) 81 | { 82 | if (comp < 0) comp = 0; 83 | if (comp > 1) comp = 1; 84 | 85 | return (int)Math.Round(255*comp); 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/raytracer/DoubleExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer 2 | { 3 | using System; 4 | public static class DoubleExtensions 5 | { 6 | private const double EPSILON = 0.00001; 7 | 8 | public static bool EqualsD(this double a, double b) 9 | { 10 | if((Math.Abs(a - b))<=EPSILON) 11 | { 12 | return true; 13 | } 14 | else 15 | { 16 | return false; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/raytracer/Tuple.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer 2 | { 3 | using System; 4 | 5 | public class Tuple 6 | { 7 | public static readonly Tuple Zero = new Tuple(0, 0, 0, 0); 8 | 9 | public static Tuple Point(double x, double y, double z) 10 | { 11 | return new Tuple(x, y, z, 1); 12 | } 13 | 14 | public static Tuple Vector(double x, double y, double z) 15 | { 16 | return new Tuple(x, y, z, 0); 17 | } 18 | 19 | public Tuple(double x, double y, double z, double w) 20 | { 21 | X = x; 22 | Y = y; 23 | Z = z; 24 | W = w; 25 | } 26 | 27 | public double X { get; set; } 28 | public double Y { get; set; } 29 | public double Z { get; set; } 30 | public double W { get; set; } 31 | 32 | public bool IsPoint { get => W == 1; } 33 | public bool IsVector { get => W == 0; } 34 | 35 | 36 | public Tuple Add(Tuple other) 37 | { 38 | return new Tuple( 39 | X + other.X, 40 | Y + other.Y, 41 | Z + other.Z, 42 | W + other.W 43 | ); 44 | } 45 | 46 | public Tuple Subtract(Tuple other) 47 | { 48 | return new Tuple( 49 | X - other.X, 50 | Y - other.Y, 51 | Z - other.Z, 52 | W - other.W 53 | ); 54 | } 55 | 56 | public Tuple Negate() 57 | { 58 | return new Tuple( 59 | -X, 60 | -Y, 61 | -Z, 62 | -W 63 | ); 64 | } 65 | 66 | public Tuple Multiply(double multiplier) 67 | { 68 | return new Tuple( 69 | X * multiplier, 70 | Y * multiplier, 71 | Z * multiplier, 72 | W * multiplier 73 | ); 74 | } 75 | 76 | public Tuple Divide(double divisor) 77 | { 78 | return new Tuple( 79 | X / divisor, 80 | Y / divisor, 81 | Z / divisor, 82 | W / divisor 83 | ); 84 | } 85 | 86 | public double Magnitude() 87 | { 88 | return Math.Sqrt(Math.Pow(X, 2) + Math.Pow(Y, 2) + Math.Pow(Z, 2) + Math.Pow(W, 2)); 89 | } 90 | 91 | public Tuple Normalize() 92 | { 93 | var m = this.Magnitude(); 94 | return this.Divide(m); 95 | } 96 | 97 | public double Dot(Tuple other) 98 | { 99 | return 100 | this.X * other.X + 101 | this.Y * other.Y + 102 | this.Z * other.Z + 103 | this.W * other.W; 104 | } 105 | 106 | public Tuple Cross(Tuple v) 107 | { 108 | return new Tuple( 109 | this.Y * v.Z - this.Z * v.Y, 110 | this.Z * v.X - this.X * v.Z, 111 | this.X * v.Y - this.Y * v.X, 112 | 0 113 | ); 114 | } 115 | 116 | public override string ToString() 117 | { 118 | return $"[{X}, {Y}, {Z}],({W})"; 119 | } 120 | 121 | 122 | public override bool Equals(object obj) 123 | { 124 | Tuple t = obj as Tuple; 125 | Tuple self = this as Tuple; 126 | if (t == null || self == null) 127 | { 128 | return false; 129 | } 130 | else 131 | { 132 | return (t.X.EqualsD(self.X) 133 | && t.Y.EqualsD(self.Y) 134 | && t.Z.EqualsD(self.Z) 135 | && t.W.EqualsD(self.W)); 136 | } 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /src/raytracer/codeclimber.raytracer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/xUnit/CanvasTest.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer.xUnit 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.IO; 6 | using Xunit; 7 | public class CanvasTest 8 | { 9 | [Fact] 10 | public void CanvasSetsWidthAndHeight() 11 | { 12 | //Given 13 | var c = new Canvas(10, 20); 14 | 15 | //When 16 | 17 | //Then 18 | Assert.Equal(10, c.Width); 19 | Assert.Equal(20, c.Height); 20 | } 21 | 22 | [Fact] 23 | public void EmptyCanvasIsAllBlack() 24 | { 25 | //Given 26 | var c = new Canvas(10, 20); 27 | 28 | //When 29 | 30 | //Then 31 | for (int x = 0; x < c.Width; x++) 32 | { 33 | for (int y = 0; y < c.Height; y++) 34 | { 35 | Assert.Equal(Color.Black, c[x, y]); 36 | } 37 | } 38 | } 39 | 40 | [Fact] 41 | public void CanSetPixelColor() 42 | { 43 | //Given 44 | var c = new Canvas(10, 20); 45 | var red = new Color(1, 0, 0); 46 | 47 | //When 48 | c[2, 3] = red; 49 | //Then 50 | Assert.Equal(red, c[2, 3]); 51 | } 52 | 53 | [Fact] 54 | public void CanWritePPMHeader() 55 | { 56 | //Given 57 | var c = new Canvas(5, 3); 58 | //When 59 | var ppm = c.GetPPMContent(); 60 | //Then 61 | var ppmLines = GetLines(ppm); 62 | Assert.Equal("P3", ppmLines[0]); 63 | Assert.Equal("5 3", ppmLines[1]); 64 | Assert.Equal("255", ppmLines[2]); 65 | } 66 | 67 | [Fact] 68 | public void CanWritePixelData() 69 | { 70 | //Given 71 | var c = new Canvas(5, 3); 72 | var c1 = new Color(1.5, 0, 0); 73 | var c2 = new Color(0, 0.5, 0); 74 | var c3 = new Color(-0.5, 0, 1); 75 | //When 76 | c[0, 0] = c1; 77 | c[2, 1] = c2; 78 | c[4, 2] = c3; 79 | var ppm = c.GetPPMContent(); 80 | //Then 81 | var ppmLines = GetLines(ppm); 82 | Assert.Equal("255 0 0 0 0 0 0 0 0 0 0 0 0 0 0", ppmLines[3]); 83 | Assert.Equal("0 0 0 0 0 0 0 128 0 0 0 0 0 0 0", ppmLines[4]); 84 | Assert.Equal("0 0 0 0 0 0 0 0 0 0 0 0 0 0 255", ppmLines[5]); 85 | } 86 | 87 | [Fact] 88 | public void CanWritePixelLineLessThan70() 89 | { 90 | //Given 91 | var c = new Canvas(10, 20, new Color(1, 0.8, 0.6)); 92 | var ppm = c.GetPPMContent(); 93 | //Then 94 | var ppmLines = GetLines(ppm); 95 | Assert.Equal("255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204", ppmLines[3]); 96 | Assert.Equal("153 255 204 153 255 204 153 255 204 153 255 204 153", ppmLines[4]); 97 | Assert.Equal("255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204", ppmLines[5]); 98 | Assert.Equal("153 255 204 153 255 204 153 255 204 153 255 204 153", ppmLines[6]); 99 | } 100 | 101 | [Fact] 102 | public void FileEndsWithNewLine() 103 | { 104 | //Given 105 | var c = new Canvas(5, 3); 106 | //When 107 | var ppm = c.GetPPMContent(); 108 | //Then 109 | 110 | Assert.True(ppm.EndsWith(Environment.NewLine)); 111 | } 112 | 113 | [Fact] 114 | public void CanSaveFile() 115 | { 116 | //Given 117 | var c = new Canvas(10, 20, new Color(1, 0.8, 0.6)); 118 | //When 119 | var c1 = new Color(1.5, 0, 0); 120 | var c2 = new Color(0, 0.5, 0); 121 | var c3 = new Color(-0.5, 0, 1); 122 | //When 123 | c[0, 0] = c1; 124 | c[2, 1] = c2; 125 | c[4, 2] = c3; 126 | c.Save("file.ppm"); 127 | //Then 128 | Assert.True(File.Exists("file.ppm")); 129 | } 130 | 131 | private List GetLines(string text) 132 | { 133 | var lines = new List(); 134 | using (StringReader reader = new StringReader(text)) 135 | { 136 | while (true) 137 | { 138 | var line = reader.ReadLine(); 139 | if (line != null) 140 | { 141 | lines.Add(line); 142 | } 143 | else 144 | { 145 | break; 146 | } 147 | } 148 | } 149 | return lines; 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /src/xUnit/ColorTest.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer.xUnit 2 | { 3 | using Xunit; 4 | 5 | public class ColorTest 6 | { 7 | [Fact] 8 | public void ColorSetsComponents() 9 | { 10 | //Given 11 | var c = new Color(-0.5, 0.4, 1.7); 12 | //When 13 | 14 | //Then 15 | Assert.Equal(-0.5, c.Red); 16 | Assert.Equal(0.4, c.Green); 17 | Assert.Equal(1.7, c.Blue); 18 | } 19 | 20 | [Fact] 21 | public void CanAddColors() 22 | { 23 | //Given 24 | var a1 = new Color(3, 4, 5); 25 | var a2 = new Color(2, 3, 1); 26 | var expectedResult = new Color(5, 7, 6); 27 | //When 28 | 29 | //Then 30 | var result = a1.Add(a2); 31 | Assert.Equal(expectedResult, result); 32 | } 33 | 34 | [Fact] 35 | public void CanSubtractColors() 36 | { 37 | //Given 38 | var a1 = new Color(0.9, 0.6, 0.75); 39 | var a2 = new Color(0.7, 0.1, 0.25); 40 | var expectedResult = new Color(0.2, 0.5, 0.5); 41 | //When 42 | 43 | //Then 44 | var result = a1.Subtract(a2); 45 | Assert.Equal(expectedResult, result); 46 | } 47 | 48 | [Fact] 49 | public void CanMultiplyTimesScalar() 50 | { 51 | //Given 52 | var t = new Color(0.2, 0.3, 0.4); 53 | var expectedResult = new Color(0.4, 0.6, 0.8); 54 | //When 55 | 56 | //Then 57 | var result = t.Multiply(2); 58 | Assert.Equal(expectedResult, result); 59 | } 60 | 61 | [Fact] 62 | public void CanMultiplyTwoColors() 63 | { 64 | //Given 65 | var a = new Color(1, 0.2, 0.4); 66 | var b = new Color(0.9, 1, 0.1); 67 | var expectedResult = new Color(0.9, 0.2, 0.04); 68 | //When 69 | 70 | //Then 71 | var result = a.Multiply(b); 72 | Assert.Equal(expectedResult, result); 73 | } 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /src/xUnit/PointTest.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer.xUnit 2 | { 3 | using Xunit; 4 | 5 | public class PointTest 6 | { 7 | [Fact] 8 | public void PointIsTupleWithW1() 9 | { 10 | var p = Tuple.Point(4f, -4f, 3f); 11 | var t = new Tuple(4f, -4f, 3f, 1f); 12 | Assert.Equal(t, p); 13 | Assert.Equal(p, t); 14 | } 15 | 16 | 17 | [Fact] 18 | public void PointIsDifferentFromVector() 19 | { 20 | var p = Tuple.Point(4.3f, -4.1f, 3.1f); 21 | var v = Tuple.Vector(4.3f, -4.2f, 3.1f); 22 | 23 | Assert.NotEqual(p, v); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/xUnit/TupleTest.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer.xUnit 2 | { 3 | using Xunit; 4 | using s = System; 5 | 6 | public class TupleTest 7 | { 8 | [Fact] 9 | public void TupleWithW1IsAPoint() 10 | { 11 | //Given 12 | var a = new Tuple(4.3, -4.2, 3.1, 1); 13 | 14 | //When 15 | 16 | //Then 17 | Assert.Equal(4.3, a.X); 18 | Assert.Equal(-4.2, a.Y); 19 | Assert.Equal(3.1, a.Z); 20 | Assert.Equal(1, a.W); 21 | Assert.True(a.IsPoint); 22 | Assert.False(a.IsVector); 23 | 24 | } 25 | [Fact] 26 | public void TupleWithW0IsAVector() 27 | { 28 | //Given 29 | var a = new Tuple(4.3, -4.2, 3.1, 0); 30 | 31 | //When 32 | 33 | //Then 34 | Assert.Equal(4.3, a.X); 35 | Assert.Equal(-4.2, a.Y); 36 | Assert.Equal(3.1, a.Z); 37 | Assert.Equal(0, a.W); 38 | Assert.False(a.IsPoint); 39 | Assert.True(a.IsVector); 40 | 41 | } 42 | 43 | [Fact] 44 | public void EqualityOperatorReturnsTrueWhenEqual() 45 | { 46 | var a = new Tuple(4.3, -4.2, 3.1, 0); 47 | var b = new Tuple(4.3, -4.2, 3.1, 0); 48 | 49 | 50 | var ab = a.Equals(b); 51 | var ba = b.Equals(a); 52 | 53 | Assert.True(ab); 54 | Assert.True(ba); 55 | } 56 | 57 | [Fact] 58 | public void EqualityOperatorReturnsFalseWhenNotEqual() 59 | { 60 | var a = new Tuple(4.3, -4.1, 3.1, 0); 61 | var b = new Tuple(4.3, -4.2, 3.1, 0); 62 | 63 | 64 | var ab = a.Equals(b); 65 | var ba = b.Equals(a); 66 | 67 | Assert.False(ab); 68 | Assert.False(ba); 69 | } 70 | 71 | [Fact] 72 | public void CanAddTuples() 73 | { 74 | //Given 75 | var a1 = new Tuple(3, -2, 5, 1); 76 | var a2 = new Tuple(-2, 3, 1, 0); 77 | var expectedResult = new Tuple(1, 1, 6, 1); 78 | //When 79 | 80 | //Then 81 | var result = a1.Add(a2); 82 | Assert.Equal(expectedResult, result); 83 | } 84 | 85 | [Fact] 86 | public void AddingVectorToPointGivesAPoint() 87 | { 88 | //Given 89 | var p = Tuple.Point(3, 2, 1); 90 | var v = Tuple.Vector(5, 6, 7); 91 | var expectedResult = Tuple.Point(8, 8, 8); 92 | //When 93 | 94 | //Then 95 | var result = p.Add(v); 96 | Assert.Equal(expectedResult, result); 97 | Assert.True(result.IsPoint); 98 | } 99 | 100 | [Fact] 101 | public void SubtractingPointsGivesAVector() 102 | { 103 | //Given 104 | var a1 = Tuple.Point(3, 2, 1); 105 | var a2 = Tuple.Point(5, 6, 7); 106 | var expectedResult = Tuple.Vector(-2, -4, -6); 107 | //When 108 | 109 | //Then 110 | var result = a1.Subtract(a2); 111 | Assert.Equal(expectedResult, result); 112 | Assert.True(result.IsVector); 113 | } 114 | 115 | [Fact] 116 | public void SubtractingVectorFromPointGivesAPoint() 117 | { 118 | //Given 119 | var p = Tuple.Point(3, 2, 1); 120 | var v = Tuple.Vector(5, 6, 7); 121 | var expectedResult = Tuple.Point(-2, -4, -6); 122 | //When 123 | 124 | //Then 125 | var result = p.Subtract(v); 126 | Assert.Equal(expectedResult, result); 127 | Assert.True(result.IsPoint); 128 | } 129 | 130 | [Fact] 131 | public void SubtractingTwoVectorsGivesAVector() 132 | { 133 | //Given 134 | var v1 = Tuple.Vector(3, 2, 1); 135 | var v2 = Tuple.Vector(5, 6, 7); 136 | var expectedResult = Tuple.Vector(-2, -4, -6); 137 | //When 138 | 139 | //Then 140 | var result = v1.Subtract(v2); 141 | Assert.Equal(expectedResult, result); 142 | Assert.True(result.IsVector); 143 | } 144 | 145 | [Fact] 146 | public void ZeroVectorIsTuple0000() 147 | { 148 | //Given 149 | var zero = Tuple.Zero; 150 | var zeroTuple = new Tuple(0, 0, 0, 0); 151 | //When 152 | 153 | //Then 154 | Assert.Equal(zeroTuple, zero); 155 | } 156 | 157 | [Fact] 158 | public void SubtractingFromZeroInvertsTheVector() 159 | { 160 | //Given 161 | var zero = Tuple.Zero; 162 | var v = Tuple.Vector(1, -2, 3); 163 | var expectedResult = Tuple.Vector(-1, 2, -3); 164 | //When 165 | 166 | //Then 167 | var result = zero.Subtract(v); 168 | Assert.Equal(expectedResult, result); 169 | } 170 | 171 | [Fact] 172 | public void CanNegateTuple() 173 | { 174 | //Given 175 | var t = new Tuple(1, -2, 3, -4); 176 | var expectedResult = new Tuple(-1, 2, -3, 4); 177 | //When 178 | 179 | //Then 180 | var result = t.Negate(); 181 | Assert.Equal(expectedResult, result); 182 | } 183 | 184 | [Fact] 185 | public void CanMultiplyTimesScalarTuple() 186 | { 187 | //Given 188 | var t = new Tuple(1, -2, 3, -4); 189 | var expectedResult = new Tuple(3.5f, -7, 10.5f, -14); 190 | //When 191 | 192 | //Then 193 | var result = t.Multiply(3.5f); 194 | Assert.Equal(expectedResult, result); 195 | } 196 | 197 | [Fact] 198 | public void CanMultiplyTimesFractionTuple() 199 | { 200 | //Given 201 | var t = new Tuple(1, -2, 3, -4); 202 | var expectedResult = new Tuple(0.5f, -1, 1.5f, -2); 203 | //When 204 | 205 | //Then 206 | var result = t.Multiply(0.5f); 207 | Assert.Equal(expectedResult, result); 208 | } 209 | 210 | [Fact] 211 | public void CanDivideTuple() 212 | { 213 | //Given 214 | var t = new Tuple(1, -2, 3, -4); 215 | var expectedResult = new Tuple(0.5, -1, 1.5, -2); 216 | //When 217 | 218 | //Then 219 | var result = t.Divide(2); 220 | Assert.Equal(expectedResult, result); 221 | } 222 | 223 | [Theory] 224 | [InlineData(1, 0, 0, 1)] 225 | [InlineData(0, 1, 0, 1)] 226 | [InlineData(0, 0, 1, 1)] 227 | public void CanComputeMagnitudeUnitVector(double x, double y, double z, double magnitude) 228 | { 229 | var value = Tuple.Vector(x, y, z); 230 | Assert.Equal(magnitude, value.Magnitude()); 231 | } 232 | 233 | [Fact] 234 | public void CanComputeMagnitudeVector123() 235 | { 236 | //Given 237 | var value = Tuple.Vector(1, 2, 3); 238 | var expectedResult = s.Math.Sqrt(14); 239 | //When 240 | 241 | //Then 242 | Assert.Equal(expectedResult, value.Magnitude()); 243 | } 244 | 245 | [Fact] 246 | public void CanComputeMagnitudeVectorNeg123() 247 | { 248 | //Given 249 | var value = Tuple.Vector(-1, -2, -3); 250 | var expectedResult = s.Math.Sqrt(14); 251 | //When 252 | 253 | //Then 254 | Assert.Equal(expectedResult, value.Magnitude()); 255 | } 256 | 257 | [Fact] 258 | public void CanNormalizeUnidimensionalVector() 259 | { 260 | //Given 261 | var value = Tuple.Vector(4, 0, 0); 262 | var expectedResult = Tuple.Vector(1, 0, 0); 263 | //When 264 | 265 | //Then 266 | Assert.Equal(expectedResult, value.Normalize()); 267 | } 268 | 269 | [Fact] 270 | public void CanNormalizeBidimensionalVector() 271 | { 272 | //Given 273 | var value = Tuple.Vector(1, 1, 0); 274 | var sq = s.Math.Sqrt(2); 275 | var expectedResult = Tuple.Vector(1 / sq, 1 / sq, 0); 276 | //When 277 | 278 | //Then 279 | Assert.Equal(expectedResult, value.Normalize()); 280 | } 281 | 282 | [Fact] 283 | public void CanNormalize3DVector() 284 | { 285 | //Given 286 | var value = Tuple.Vector(1, 2, 3); 287 | var sq = s.Math.Sqrt(14); 288 | var expectedResult = Tuple.Vector(1 / sq, 2 / sq, 3 / sq); 289 | //When 290 | 291 | //Then 292 | Assert.Equal(expectedResult, value.Normalize()); 293 | } 294 | 295 | [Fact] 296 | public void MagnitudeOfNormalizedVectorIs1() 297 | { 298 | //Given 299 | var value = Tuple.Vector(1, 2, 3); 300 | //When 301 | var norm = value.Normalize(); 302 | //Then 303 | Assert.Equal(1, norm.Magnitude()); 304 | } 305 | 306 | [Fact] 307 | public void CanComputeDOTProduct() 308 | { 309 | //Given 310 | var a = Tuple.Vector(1, 2, 3); 311 | var b = Tuple.Vector(2, 3, 4); 312 | //When 313 | 314 | //Then 315 | Assert.Equal(20, a.Dot(b)); 316 | } 317 | 318 | [Fact] 319 | public void IdenticalUnitVectorsHaveDotProductEqualTo1() 320 | { 321 | //Given 322 | var a = Tuple.Vector(1, 0, 0); 323 | var b = Tuple.Vector(1, 0, 0); 324 | //When 325 | 326 | //Then 327 | Assert.Equal(1, a.Dot(b)); 328 | } 329 | 330 | [Fact] 331 | public void OppositeUnitVectorsHaveDotProductEqualTo1() 332 | { 333 | //Given 334 | var a = Tuple.Vector(1, 0, 0); 335 | var b = Tuple.Vector(-1, 0, 0); 336 | //When 337 | 338 | //Then 339 | Assert.Equal(-1, a.Dot(b)); 340 | } 341 | 342 | [Fact] 343 | public void CanWriteToString() 344 | { 345 | //Given 346 | var t = new Tuple(4.3, -4.1, 3.1, 0); 347 | var expectedResult = "[4.3, -4.1, 3.1],(0)"; 348 | //When 349 | 350 | //Then 351 | Assert.Equal(expectedResult,t.ToString()); 352 | } 353 | 354 | } 355 | } -------------------------------------------------------------------------------- /src/xUnit/VectorTest.cs: -------------------------------------------------------------------------------- 1 | namespace codeclimber.raytracer.xUnit 2 | { 3 | using Xunit; 4 | 5 | public class VectorTest 6 | { 7 | [Fact] 8 | public void VectorIsTupleWithW0() 9 | { 10 | var v = Tuple.Vector(4f, -4f, 3f); 11 | var t = new Tuple(4f, -4f, 3f, 0); 12 | Assert.Equal(t, v); 13 | Assert.Equal(v, t); 14 | } 15 | 16 | 17 | [Fact] 18 | public void CanComputeCROSSProduct() 19 | { 20 | //Given 21 | var a = Tuple.Vector(1, 2, 3); 22 | var b = Tuple.Vector(2, 3, 4); 23 | //When 24 | 25 | //Then 26 | Assert.Equal(Tuple.Vector(-1, 2, -1), a.Cross(b)); 27 | Assert.Equal(Tuple.Vector(1, -2, 1), b.Cross(a)); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/xUnit/codeclimber.raytracer.xUnit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------