|
39 | 39 | ;; |
40 | 40 | ;; [Worley noise](https://en.wikipedia.org/wiki/Worley_noise) is a type of structured noise which is defined for each pixel using the distance to the nearest seed point. |
41 | 41 | ;; |
| 42 | +;; ### Noise parameters |
| 43 | +;; |
42 | 44 | ;; First we define a function to create parameters of the noise. |
43 | 45 | ;; |
44 | 46 | ;; * **size** is the size of each dimension of the noise array |
45 | 47 | ;; * **divisions** is the number of subdividing cells in each dimension |
46 | 48 | ;; * **dimensions** is the number of dimensions |
47 | | - |
48 | 49 | (defn make-noise-params |
49 | 50 | [size divisions dimensions] |
50 | 51 | {:size size :divisions divisions :cellsize (/ size divisions) :dimensions dimensions}) |
51 | 52 |
|
52 | | - |
| 53 | +;; Here is a corresponding Midje test. |
| 54 | +;; Note that ideally you practise [Test Driven Development (TDD)](https://martinfowler.com/bliki/TestDrivenDevelopment.html), i.e. you start with writing one failing test. |
| 55 | +;; Because this is a Clojure notebook, the unit tests are displayed after the implementation. |
53 | 56 | (fact "Noise parameter initialisation" |
54 | 57 | (make-noise-params 256 8 2) => {:size 256 :divisions 8 :cellsize 32 :dimensions 2}) |
55 | 58 |
|
56 | 59 |
|
| 60 | +;; ### 2D and 3D vectors |
| 61 | +;; |
| 62 | +;; Next we need a function which allows us to create 2D or 3D vectors depending on the number of input parameters. |
57 | 63 | (defn vec-n |
58 | 64 | ([x y] (vec2 x y)) |
59 | 65 | ([x y z] (vec3 x y z))) |
60 | 66 |
|
61 | | - |
62 | 67 | (facts "Generic vector function for creating 2D and 3D vectors" |
63 | 68 | (vec-n 2 3) => (vec2 2 3) |
64 | 69 | (vec-n 2 3 1) => (vec3 2 3 1)) |
65 | 70 |
|
66 | 71 |
|
| 72 | +;; ### Random points |
| 73 | +;; |
| 74 | +;; The following method generates a random point in a cell specified by the cell indices. |
67 | 75 | (defn random-point-in-cell |
68 | | - [{:keys [cellsize]} & args] |
| 76 | + [{:keys [cellsize]} & indices] |
69 | 77 | (let [random-seq (repeatedly #(rand cellsize)) |
70 | | - dimensions (count args)] |
71 | | - (add (mult (apply vec-n (reverse args)) cellsize) |
| 78 | + dimensions (count indices)] |
| 79 | + (add (mult (apply vec-n (reverse indices)) cellsize) |
72 | 80 | (apply vec-n (take dimensions random-seq))))) |
73 | 81 |
|
74 | | - |
| 82 | +;; We test the method by replacing the random function with a deterministic function. |
75 | 83 | (facts "Place random point in a cell" |
76 | 84 | (with-redefs [rand (fn [s] (* 0.5 s))] |
77 | 85 | (random-point-in-cell {:cellsize 1} 0 0) => (vec2 0.5 0.5) |
|
81 | 89 | (random-point-in-cell {:cellsize 2} 2 3 5) => (vec3 11.0 7.0 5.0))) |
82 | 90 |
|
83 | 91 |
|
| 92 | +;; We can now use the `random-point` method to generate a grid of random points. |
84 | 93 | (defn random-points |
85 | 94 | [{:keys [divisions dimensions] :as params}] |
86 | 95 | (tensor/clone |
87 | 96 | (tensor/compute-tensor (repeat dimensions divisions) |
88 | 97 | (partial random-point-in-cell params)))) |
89 | 98 |
|
90 | | - |
91 | 99 | (facts "Greate grid of random points" |
92 | 100 | (let [params-2d (make-noise-params 32 8 2) |
93 | 101 | params-3d (make-noise-params 32 8 3)] |
|
99 | 107 | (dtype/shape (random-points params-3d)) => [8 8 8] |
100 | 108 | ((random-points params-3d) 2 3 5) => (vec3 22.0 14.0 10.0)))) |
101 | 109 |
|
102 | | - |
| 110 | +;; Here is a scatter plot showing one random point placed in each cell. |
103 | 111 | (let [points (tensor/reshape (random-points (make-noise-params 256 8 2)) [(* 8 8)]) |
104 | 112 | scatter (tc/dataset {:x (map first points) :y (map second points)})] |
105 | 113 | (-> scatter |
106 | 114 | (plotly/base {:=title "Random points"}) |
107 | 115 | (plotly/layer-point {:=x :x :=y :y}))) |
108 | 116 |
|
109 | 117 |
|
| 118 | +;; ### Modular distance |
| 119 | +;; |
| 120 | +;; In order to get a periodic noise array, we need to component-wise wrap around distance vectors. |
110 | 121 | (defn mod-vec |
111 | 122 | [{:keys [size]} v] |
112 | 123 | (let [size2 (/ size 2) |
113 | 124 | wrap (fn [x] (-> x (+ size2) (mod size) (- size2)))] |
114 | 125 | (apply vec-n (map wrap v)))) |
115 | 126 |
|
116 | | - |
117 | 127 | (facts "Wrap around components of vector to be within -size/2..size/2" |
118 | 128 | (mod-vec {:size 8} (vec2 2 3)) => (vec2 2 3) |
119 | 129 | (mod-vec {:size 8} (vec2 5 2)) => (vec2 -3 2) |
|
128 | 138 | (mod-vec {:size 8} (vec3 2 -5 1)) => (vec3 2 3 1) |
129 | 139 | (mod-vec {:size 8} (vec3 2 3 -5)) => (vec3 2 3 3)) |
130 | 140 |
|
131 | | - |
| 141 | +;; |
| 142 | +;; Using the `mod-dist` function we can calculate the distance between two points in the periodic noise array. |
132 | 143 | (defn mod-dist |
133 | 144 | [params a b] |
134 | 145 | (mag (mod-vec params (sub b a)))) |
135 | 146 |
|
136 | | - |
137 | 147 | (tabular "Wrapped distance of two points" |
138 | 148 | (fact (mod-dist {:size 8} (vec2 ?ax ?ay) (vec2 ?bx ?by)) => ?result) |
139 | 149 | ?ax ?ay ?bx ?by ?result |
|
148 | 158 | 0 5 0 0 3.0) |
149 | 159 |
|
150 | 160 |
|
| 161 | +;; ### Modular lookup |
| 162 | +;; |
| 163 | +;; We also need to lookup elements with wrap around. |
| 164 | +;; We recursively use `tensor/select` and then finally the tensor as a function to lookup along each axis. |
151 | 165 | (defn wrap-get |
152 | 166 | [t & args] |
153 | 167 | (if (> (count (dtype/shape t)) (count args)) |
154 | 168 | (apply tensor/select t (map mod args (dtype/shape t))) |
155 | 169 | (apply t (map mod args (dtype/shape t))))) |
156 | 170 |
|
157 | | - |
| 171 | +;; A tensor with index vectors is used to test the lookup. |
158 | 172 | (facts "Wrapped lookup of tensor values" |
159 | 173 | (let [t (tensor/compute-tensor [4 6] vec2)] |
160 | 174 | (wrap-get t 2 3) => (vec2 2 3) |
|
0 commit comments