Skip to content

Commit c20c0e8

Browse files
committed
Reviewing article
1 parent 4e80326 commit c20c0e8

1 file changed

Lines changed: 42 additions & 33 deletions

File tree

src/volumetric_clouds/main.clj

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
[size divisions dimensions]
4949
{:size size :divisions divisions :cellsize (/ size divisions) :dimensions dimensions})
5050

51-
;; Here is a corresponding Midje test.
51+
;; Here is a corresponding [Midje](https://github.com/marick/Midje) test.
5252
;; Note that ideally you practise [Test Driven Development (TDD)](https://martinfowler.com/bliki/TestDrivenDevelopment.html), i.e. you start with writing one failing test.
5353
;; Because this is a Clojure notebook, the unit tests are displayed after the implementation.
5454
(fact "Noise parameter initialisation"
@@ -208,7 +208,7 @@
208208
;;
209209
;; Using above functions one can now implement Worley noise.
210210
;; For each pixel the distance to the closest seed point is calculated.
211-
;; The distance to each random point in all neighbouring cells is calculated and the minimum is taken.
211+
;; This is achieved by determining the distance to each random point in all neighbouring cells and then taking the minimum.
212212
(defn worley-noise
213213
[{:keys [size dimensions] :as params}]
214214
(let [random-points (random-points params)]
@@ -275,7 +275,7 @@
275275
[{:keys [divisions dimensions]}]
276276
(tensor/clone (tensor/compute-tensor (repeat dimensions divisions) random-gradient)))
277277

278-
;; The function is tested that it generates 2D and 3D random gradient fields correctly.
278+
;; The function is verified to correctly generate 2D and 3D random gradient fields.
279279
(facts "Random gradients"
280280
(with-redefs [rand (constantly 1.5)]
281281
(dtype/shape (random-gradients {:divisions 8 :dimensions 2}))
@@ -286,7 +286,7 @@
286286
((random-gradients {:divisions 8 :dimensions 3}) 0 0 0)
287287
=> (vec3 (/ 1 (sqrt 3)) (/ 1 (sqrt 3)) (/ 1 (sqrt 3)))))
288288

289-
;; The gradient field can be plotted with Plotly as a scatter plot of disconnected lines/
289+
;; The gradient field can be plotted with Plotly as a scatter plot of disconnected lines.
290290
(let [gradients (tensor/reshape (random-gradients (make-noise-params 256 8 2))
291291
[(* 8 8)])
292292
points (tensor/reshape (tensor/compute-tensor [8 8] (fn [y x] (vec2 x y)))
@@ -311,7 +311,7 @@
311311
;; First we define a function to determine the fractional part of a number.
312312
(defn frac
313313
[x]
314-
(mod x 1.0))
314+
(- x (Math/floor x)))
315315

316316
(facts "Fractional part of floating point number"
317317
(frac 0.25) => 0.25
@@ -329,7 +329,7 @@
329329
(cell-pos {:cellsize 4} (vec2 7 5)) => (vec2 0.75 0.25)
330330
(cell-pos {:cellsize 4} (vec3 7 5 2)) => (vec3 0.75 0.25 0.5))
331331

332-
;; A tensor converting the corner vectors can be computed by subtracting the corner coordinates from the point coordinates.
332+
;; A 2 × 2 tensor of corner vectors can be computed by subtracting the corner coordinates from the point coordinates.
333333
(defn corner-vectors
334334
[{:keys [dimensions] :as params} point]
335335
(let [cell-pos (cell-pos params point)]
@@ -338,17 +338,18 @@
338338
(fn [& args] (sub cell-pos (apply vec-n (reverse args)))))))
339339

340340
(facts "Compute relative vectors from cell corners to point in cell"
341-
(let [v2 (corner-vectors {:cellsize 4 :dimensions 2} (vec2 7 6))
342-
v3 (corner-vectors {:cellsize 4 :dimensions 3} (vec3 7 6 5))]
343-
(v2 0 0) => (vec2 0.75 0.5)
344-
(v2 0 1) => (vec2 -0.25 0.5)
345-
(v2 1 0) => (vec2 0.75 -0.5)
346-
(v2 1 1) => (vec2 -0.25 -0.5)
347-
(v3 0 0 0) => (vec3 0.75 0.5 0.25)))
341+
(let [corners2 (corner-vectors {:cellsize 4 :dimensions 2} (vec2 7 6))
342+
corners3 (corner-vectors {:cellsize 4 :dimensions 3} (vec3 7 6 5))]
343+
(corners2 0 0) => (vec2 0.75 0.5)
344+
(corners2 0 1) => (vec2 -0.25 0.5)
345+
(corners2 1 0) => (vec2 0.75 -0.5)
346+
(corners2 1 1) => (vec2 -0.25 -0.5)
347+
(corners3 0 0 0) => (vec3 0.75 0.5 0.25)))
348348

349349
;; ### Extract gradients of cell corners
350350
;;
351351
;; The function below retrieves the gradient values at a cell's corners, utilizing `wrap-get` for modular access.
352+
;; The result is a 2 × 2 tensor of gradient vectors.
352353
(defn corner-gradients
353354
[{:keys [dimensions] :as params} gradients point]
354355
(let [division (map (partial division-index params) point)]
@@ -480,7 +481,7 @@
480481
;;
481482
;; ### Combination of Worley and Perlin noise
482483
;;
483-
;; One can mix Worley and Perlin noise by simply doing a linear combination of the two.
484+
;; You can blend Worley and Perlin noise by performing a linear combination of both.
484485
(def perlin-worley-norm (dfn/+ (dfn/* 0.3 perlin-norm) (dfn/* 0.7 worley-norm)))
485486

486487
;; Here for example is the average of Perlin and Worley noise.
@@ -567,7 +568,7 @@
567568
1 0 2 0 4 2)
568569

569570

570-
;; The clamp function is used to clamp a value to a range.
571+
;; The clamp function is used to element-wise clamp values to a range.
571572
(defn clamp
572573
[value low high]
573574
(dfn/max low (dfn/min value high)))
@@ -582,7 +583,7 @@
582583

583584
;; ### Generating octaves of noise
584585
;;
585-
;; The octaves function is to create a series of decreasing weights and normalize them so that they add up to 1.
586+
;; The octaves function is used to create a series of decreasing weights and normalize them so that they add up to 1.
586587
(defn octaves
587588
[n decay]
588589
(let [series (take n (iterate #(* % decay) 1.0))
@@ -738,7 +739,7 @@
738739
(GL11/glGetTexImage GL11/GL_TEXTURE_2D 0 GL12/GL_RGBA GL11/GL_FLOAT buffer)
739740
(float-buffer->array buffer)))
740741

741-
;; This method sets up rendering to a specified texture of specified size and then executes the body.
742+
;; This method sets up rendering using a specified texture as a framebuffer and then executes the body.
742743
(defmacro framebuffer-render
743744
[texture width height & body]
744745
`(let [fbo# (GL30/glGenFramebuffers)]
@@ -756,7 +757,7 @@
756757
(GL30/glDeleteFramebuffers fbo#)))))
757758

758759
;; We also create a method to set up the layout of the vertex buffer.
759-
;; Our vertex data is only going to be 3D coordinates of points.
760+
;; Our vertex data is only going to contain 3D coordinates of points.
760761
(defn setup-point-attribute
761762
[program]
762763
(let [point-attribute (GL20/glGetAttribLocation program "point")]
@@ -803,7 +804,7 @@
803804
(GL20/glDeleteProgram program)))))
804805

805806

806-
;; We are going to use this simple vertex shader to simply pass through the points from the vertex buffer without any transformations.
807+
;; We are going to use a simple vertex shader to simply pass through the points from the vertex buffer without any transformations.
807808
(def vertex-passthrough
808809
"#version 130
809810
in vec3 point;
@@ -839,7 +840,7 @@ float noise(vec3 idx)
839840
}")
840841

841842
;; We can test this mock function using the following probing shader.
842-
;; Note that we are using the `template` macro of the `comb` Clojure library to generate the shader code from a template.
843+
;; Note that we are using the `template` macro of the `comb` Clojure library to generate the probing shader code from a template.
843844
(def noise-probe
844845
(template/fn [x y z]
845846
"#version 130
@@ -852,7 +853,8 @@ void main()
852853

853854
;; Here multiple tests are run to test that the mock implements a checkboard pattern correctly.
854855
(tabular "Test noise mock"
855-
(fact (nth (render-pixel [vertex-passthrough] [noise-mock (noise-probe ?x ?y ?z)]) 0)
856+
(fact (nth (render-pixel [vertex-passthrough]
857+
[noise-mock (noise-probe ?x ?y ?z)]) 0)
856858
=> ?result)
857859
?x ?y ?z ?result
858860
0 0 0 0.0
@@ -945,7 +947,7 @@ void main()
945947
fragColor = vec4(ray_box(box_min, box_max, origin, direction), 0, 0);
946948
}"))
947949

948-
;; The shader is tested with different ray origins and directions.
950+
;; The `ray-box` shader is tested with different ray origins and directions.
949951
(tabular "Test intersection of ray with box"
950952
(fact ((juxt first second)
951953
(render-pixel [vertex-passthrough]
@@ -1056,7 +1058,7 @@ void main()
10561058
;;
10571059
;; The following fragment shader is used to render an image of a box filled with fog.
10581060
;;
1059-
;; * The pixel coordinate and the resolution of the image are used to determine a viewing direction which also gets rotated using the rotation matrix.
1061+
;; * The pixel coordinate and the resolution of the image are used to determine a viewing direction which also gets rotated using the rotation matrix and normalized.
10601062
;; * The origin of the camera is set at a specified distance to the center of the box and rotated as well.
10611063
;; * The ray box function is used to determine the near and far intersection points of the ray with the box.
10621064
;; * The cloud transfer function is used to sample the cloud density along the ray and determine the overall opacity and color of the fog box.
@@ -1074,17 +1076,19 @@ vec2 ray_box(vec3 box_min, vec3 box_max, vec3 origin, vec3 direction);
10741076
vec4 cloud_transfer(vec3 origin, vec3 direction, vec2 interval);
10751077
void main()
10761078
{
1077-
vec3 direction = normalize(rotation * vec3(gl_FragCoord.xy - 0.5 * resolution, focal_length));
1079+
vec3 direction =
1080+
normalize(rotation * vec3(gl_FragCoord.xy - 0.5 * resolution, focal_length));
10781081
vec3 origin = rotation * vec3(0, 0, -distance);
10791082
vec2 interval = ray_box(vec3(-0.5, -0.5, -0.5), vec3(0.5, 0.5, 0.5), origin, direction);
10801083
vec4 transfer = cloud_transfer(origin, direction, interval);
1081-
vec3 background = mix(vec3(0.125, 0.125, 0.25), vec3(1, 1, 1), pow(dot(direction, light), 1000.0));
1084+
vec3 background = mix(vec3(0.125, 0.125, 0.25), vec3(1, 1, 1),
1085+
pow(dot(direction, light), 1000.0));
10821086
fragColor = vec4(background * (1.0 - transfer.a) + transfer.rgb, 1.0);
10831087
}")
10841088

10851089
;; Uniform variables are parameters that remain constant throughout the shader execution, unlike vertex input data.
10861090
;; Here we use the following uniform variables:
1087-
;; * **resolution: a 2D vector containing the window pixel width and height
1091+
;; * **resolution**: a 2D vector containing the window pixel width and height
10881092
;; * **light:** a 3D unit vector pointing to the light source
10891093
;; * **rotation:** a 3x3 rotation matrix to rotate the camera around the origin
10901094
;; * **focal_length:** the ratio of camera focal length to pixel size of the virtual camera
@@ -1224,7 +1228,9 @@ out vec4 fragColor;
12241228
float remap_clamp(float value, float low1, float high1, float low2, float high2);
12251229
void main()
12261230
{
1227-
fragColor = vec4(remap_clamp(<%= value %>, <%= low1 %>, <%= high1 %>, <%= low2 %>, <%= high2 %>));
1231+
fragColor = vec4(remap_clamp(<%= value %>,
1232+
<%= low1 %>, <%= high1 %>,
1233+
<%= low2 %>, <%= high2 %>));
12281234
}"))
12291235

12301236
;; `remap_clamp` is tested using a parametrized tests.
@@ -1291,7 +1297,8 @@ float remap_noise(vec3 idx)
12911297
uniform vec3 light;
12921298
float mie(float mu)
12931299
{
1294-
return 3 * (1 - G * G) * (1 + mu * mu) / (8 * M_PI * (2 + G * G) * pow(1 + G * G - 2 * G * mu, 1.5));
1300+
return 3 * (1 - G * G) * (1 + mu * mu) /
1301+
(8 * M_PI * (2 + G * G) * pow(1 + G * G - 2 * G * mu, 1.5));
12951302
}
12961303
float in_scatter(vec3 point, vec3 direction)
12971304
{
@@ -1312,7 +1319,8 @@ void main()
13121319

13131320
;; The shader is tested using a few values.
13141321
(tabular "Shader function for scattering phase function"
1315-
(fact (first (render-pixel [vertex-passthrough] [(mie-scatter ?g) (mie-probe ?mu)]))
1322+
(fact (first (render-pixel [vertex-passthrough]
1323+
[(mie-scatter ?g) (mie-probe ?mu)]))
13161324
=> (roughly ?result 1e-6))
13171325
?g ?mu ?result
13181326
0 0 (/ 3 (* 16 PI))
@@ -1325,18 +1333,18 @@ void main()
13251333
(defn scatter-amount [theta]
13261334
(first (render-pixel [vertex-passthrough] [(mie-scatter 0.76) (mie-probe (cos theta))])))
13271335

1328-
;; We can use this function to plot a mix of isotropic and anisotropic scattering.
1336+
;; We can use this function to plot Mie scattering for different angles.
13291337
(let [scatter
13301338
(tc/dataset {:x (map (fn [theta]
13311339
(* (cos (to-radians theta))
1332-
(+ 0.75 (* 0.25 (scatter-amount (to-radians theta))))))
1340+
(scatter-amount (to-radians theta))))
13331341
(range 361))
13341342
:y (map (fn [theta]
13351343
(* (sin (to-radians theta))
1336-
(+ 0.75 (* 0.25 (scatter-amount (to-radians theta))))))
1344+
(scatter-amount (to-radians theta))))
13371345
(range 361)) })]
13381346
(-> scatter
1339-
(plotly/base {:=title "Mixed Mie and isotropic scattering" :=mode "lines"})
1347+
(plotly/base {:=title "Mie scattering" :=mode "lines"})
13401348
(plotly/layer-point {:=x :x :=y :y})
13411349
plotly/plot
13421350
(assoc-in [:layout :yaxis :scaleanchor] "x")))
@@ -1394,6 +1402,7 @@ float shadow(vec3 point)
13941402

13951403
;; ## Further topics
13961404
;;
1405+
;; I hope you enjoyed this little tour of volumetric clouds.
13971406
;; Here are some references to get from a cloud prototype to more realistic clouds.
13981407
;;
13991408
;; * [Vertical density profile](https://www.wedesoft.de/software/2023/05/03/volumetric-clouds/)

0 commit comments

Comments
 (0)