├── .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 | 
332 |
333 | To see I got it right, I imported the result in excel and drawn a chart.
334 |
335 | 
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 | 
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 | 
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