|
1 | 1 | (ns eido.ir.material |
2 | | - "Material descriptors for 3D rendering. |
| 2 | + "Material descriptors and multi-light shading for 3D rendering. |
3 | 3 |
|
4 | | - Materials describe how surfaces respond to light — going beyond |
5 | | - the existing diffuse-only shading in scene3d. |
| 4 | + Materials describe how surfaces respond to light using Blinn-Phong. |
6 | 5 |
|
7 | | - Material types: |
8 | | - :material/phong — Blinn-Phong shading with ambient, diffuse, and specular" |
| 6 | + Light types: |
| 7 | + :directional — parallel rays from a direction (default, existing) |
| 8 | + :omni — radiates in all directions from a position |
| 9 | + :spot — cone of light from a position with hotspot/falloff |
| 10 | + :hemisphere — sky/ground ambient blend by normal direction |
| 11 | +
|
| 12 | + Inspired by 3ds Max Standard Lights." |
9 | 13 | (:require |
| 14 | + [eido.color :as color] |
10 | 15 | [eido.math3d :as m])) |
11 | 16 |
|
12 | 17 | ;; --- material constructors --- |
|
22 | 27 | :material/shininess shininess |
23 | 28 | :material/color color}) |
24 | 29 |
|
25 | | -;; --- shading --- |
| 30 | +;; --- light constructors --- |
| 31 | + |
| 32 | +(defn directional |
| 33 | + "Creates a directional light (parallel rays)." |
| 34 | + [direction & {:keys [color multiplier ambient] |
| 35 | + :or {multiplier 1.0 ambient 0.0}}] |
| 36 | + (cond-> {:light/type :directional |
| 37 | + :light/direction direction |
| 38 | + :light/multiplier multiplier} |
| 39 | + color (assoc :light/color color) |
| 40 | + (pos? ambient) (assoc :light/ambient ambient))) |
| 41 | + |
| 42 | +(defn omni |
| 43 | + "Creates an omni (point) light that radiates in all directions." |
| 44 | + [position & {:keys [color multiplier decay decay-start] |
| 45 | + :or {multiplier 1.0 decay :none decay-start 0.0}}] |
| 46 | + (cond-> {:light/type :omni |
| 47 | + :light/position position |
| 48 | + :light/multiplier multiplier |
| 49 | + :light/decay decay |
| 50 | + :light/decay-start decay-start} |
| 51 | + color (assoc :light/color color))) |
| 52 | + |
| 53 | +(defn spot |
| 54 | + "Creates a spot light with a cone defined by hotspot and falloff angles." |
| 55 | + [position direction & {:keys [color multiplier hotspot falloff decay decay-start] |
| 56 | + :or {multiplier 1.0 hotspot 43.0 falloff 45.0 |
| 57 | + decay :none decay-start 0.0}}] |
| 58 | + (cond-> {:light/type :spot |
| 59 | + :light/position position |
| 60 | + :light/direction direction |
| 61 | + :light/multiplier multiplier |
| 62 | + :light/hotspot hotspot |
| 63 | + :light/falloff falloff |
| 64 | + :light/decay decay |
| 65 | + :light/decay-start decay-start} |
| 66 | + color (assoc :light/color color))) |
| 67 | + |
| 68 | +(defn hemisphere |
| 69 | + "Creates a hemisphere (sky) light with sky and ground colors." |
| 70 | + [sky-color ground-color & {:keys [up multiplier] |
| 71 | + :or {up [0 1 0] multiplier 0.3}}] |
| 72 | + {:light/type :hemisphere |
| 73 | + :light/sky-color sky-color |
| 74 | + :light/ground-color ground-color |
| 75 | + :light/up up |
| 76 | + :light/multiplier multiplier}) |
| 77 | + |
| 78 | +;; --- decay --- |
| 79 | + |
| 80 | +(defn- apply-decay |
| 81 | + "Applies distance decay to light intensity." |
| 82 | + ^double [^double intensity light ^double distance] |
| 83 | + (let [decay (get light :light/decay :none) |
| 84 | + decay-start (double (get light :light/decay-start 0.0)) |
| 85 | + effective-dist (max 0.0 (- distance decay-start))] |
| 86 | + (if (or (= :none decay) (<= effective-dist 0.0)) |
| 87 | + intensity |
| 88 | + (let [factor (case decay |
| 89 | + :inverse (/ 1.0 (+ 1.0 effective-dist)) |
| 90 | + :inverse-square (/ 1.0 (+ 1.0 (* effective-dist effective-dist))) |
| 91 | + 1.0)] |
| 92 | + (* intensity factor))))) |
| 93 | + |
| 94 | +;; --- per-light-type contribution --- |
26 | 95 |
|
27 | 96 | (defn- clamp-byte ^long [^double v] |
28 | 97 | (long (Math/max 0.0 (Math/min 255.0 v)))) |
29 | 98 |
|
30 | | -(defn shade-phong |
31 | | - "Computes Blinn-Phong shading for a face. |
32 | | - Returns a shaded color vector [:color/rgb r g b]. |
33 | | -
|
34 | | - normal: normalized face normal (toward camera) |
35 | | - light-dir: normalized light direction (toward light) |
36 | | - cam-dir: normalized camera direction (toward camera) |
37 | | - light: light map with :light/ambient, :light/intensity |
38 | | - material: material descriptor with :material/* keys" |
39 | | - [normal light-dir cam-dir light material] |
40 | | - (let [base-color (:material/color material) |
41 | | - [_ br bg bb] base-color |
42 | | - ambient (double (:material/ambient material)) |
| 99 | +(defn- get-multiplier ^double [light] |
| 100 | + (double (or (:light/multiplier light) 1.0))) |
| 101 | + |
| 102 | +(defn- directional-contribution |
| 103 | + "Computes diffuse + specular contribution from a directional light." |
| 104 | + [normal cam-dir material light] |
| 105 | + (let [light-dir (m/normalize (:light/direction light)) |
| 106 | + multiplier (get-multiplier light) |
| 107 | + mat-diffuse (double (:material/diffuse material)) |
| 108 | + mat-spec (double (:material/specular material)) |
| 109 | + shininess (double (:material/shininess material)) |
| 110 | + cos-angle (m/dot normal light-dir) |
| 111 | + diffuse (* mat-diffuse multiplier (max 0.0 cos-angle)) |
| 112 | + half-vec (m/normalize (m/v+ light-dir cam-dir)) |
| 113 | + n-dot-h (max 0.0 (m/dot normal half-vec)) |
| 114 | + specular (if (pos? cos-angle) |
| 115 | + (* mat-spec multiplier (Math/pow n-dot-h shininess)) |
| 116 | + 0.0)] |
| 117 | + {:diffuse diffuse :specular specular})) |
| 118 | + |
| 119 | +(defn- omni-contribution |
| 120 | + "Computes diffuse + specular contribution from an omni (point) light." |
| 121 | + [normal cam-dir material light face-centroid] |
| 122 | + (let [light-pos (:light/position light) |
| 123 | + to-light (m/v- light-pos face-centroid) |
| 124 | + distance (m/magnitude to-light) |
| 125 | + light-dir (if (pos? distance) (m/v* to-light (/ 1.0 distance)) [0 0 0]) |
| 126 | + multiplier (apply-decay (get-multiplier light) light distance) |
43 | 127 | mat-diffuse (double (:material/diffuse material)) |
44 | 128 | mat-spec (double (:material/specular material)) |
45 | 129 | shininess (double (:material/shininess material)) |
46 | | - intensity (double (get light :light/intensity 0.7)) |
47 | | - ;; Diffuse (Lambert) |
48 | 130 | cos-angle (m/dot normal light-dir) |
49 | | - diffuse (* mat-diffuse intensity (max 0.0 cos-angle)) |
50 | | - ;; Specular (Blinn-Phong) |
| 131 | + diffuse (* mat-diffuse multiplier (max 0.0 cos-angle)) |
51 | 132 | half-vec (m/normalize (m/v+ light-dir cam-dir)) |
52 | 133 | n-dot-h (max 0.0 (m/dot normal half-vec)) |
53 | 134 | specular (if (pos? cos-angle) |
54 | | - (* mat-spec intensity (Math/pow n-dot-h shininess)) |
55 | | - 0.0) |
56 | | - ;; Combined brightness |
57 | | - brightness (min 1.0 (+ ambient diffuse)) |
58 | | - ;; Apply to color channels, add specular as white highlight |
59 | | - r (clamp-byte (+ (* (double br) brightness) (* 255.0 specular))) |
60 | | - g (clamp-byte (+ (* (double bg) brightness) (* 255.0 specular))) |
61 | | - b (clamp-byte (+ (* (double bb) brightness) (* 255.0 specular)))] |
| 135 | + (* mat-spec multiplier (Math/pow n-dot-h shininess)) |
| 136 | + 0.0)] |
| 137 | + {:diffuse diffuse :specular specular})) |
| 138 | + |
| 139 | +(defn- spot-contribution |
| 140 | + "Computes diffuse + specular contribution from a spot light." |
| 141 | + [normal cam-dir material light face-centroid] |
| 142 | + (let [light-pos (:light/position light) |
| 143 | + to-light (m/v- light-pos face-centroid) |
| 144 | + distance (m/magnitude to-light) |
| 145 | + light-dir (if (pos? distance) (m/v* to-light (/ 1.0 distance)) [0 0 0]) |
| 146 | + ;; Spot cone factor |
| 147 | + spot-dir (m/normalize (:light/direction light)) |
| 148 | + cos-theta (m/dot (m/v* light-dir -1.0) spot-dir) |
| 149 | + hotspot-rad (* (/ (double (:light/hotspot light)) 2.0) (/ Math/PI 180.0)) |
| 150 | + falloff-rad (* (/ (double (:light/falloff light)) 2.0) (/ Math/PI 180.0)) |
| 151 | + cos-hotspot (Math/cos hotspot-rad) |
| 152 | + cos-falloff (Math/cos falloff-rad) |
| 153 | + spot-factor (m/smoothstep cos-falloff cos-hotspot cos-theta)] |
| 154 | + (if (zero? spot-factor) |
| 155 | + {:diffuse 0.0 :specular 0.0} |
| 156 | + (let [multiplier (* (apply-decay (get-multiplier light) light distance) spot-factor) |
| 157 | + mat-diffuse (double (:material/diffuse material)) |
| 158 | + mat-spec (double (:material/specular material)) |
| 159 | + shininess (double (:material/shininess material)) |
| 160 | + cos-angle (m/dot normal light-dir) |
| 161 | + diffuse (* mat-diffuse multiplier (max 0.0 cos-angle)) |
| 162 | + half-vec (m/normalize (m/v+ light-dir cam-dir)) |
| 163 | + n-dot-h (max 0.0 (m/dot normal half-vec)) |
| 164 | + specular (if (pos? cos-angle) |
| 165 | + (* mat-spec multiplier (Math/pow n-dot-h shininess)) |
| 166 | + 0.0)] |
| 167 | + {:diffuse diffuse :specular specular})))) |
| 168 | + |
| 169 | +(defn- hemisphere-contribution |
| 170 | + "Computes ambient contribution from a hemisphere light. |
| 171 | + Returns color contribution as [r g b] scaled 0-1." |
| 172 | + [normal light] |
| 173 | + (let [up (m/normalize (or (:light/up light) [0 1 0])) |
| 174 | + multiplier (get-multiplier light) |
| 175 | + factor (+ 0.5 (* 0.5 (m/dot normal up))) |
| 176 | + sky (color/resolve-color (:light/sky-color light)) |
| 177 | + ground (color/resolve-color (:light/ground-color light)) |
| 178 | + r (/ (+ (* factor (:r sky)) (* (- 1.0 factor) (:r ground))) 255.0) |
| 179 | + g (/ (+ (* factor (:g sky)) (* (- 1.0 factor) (:g ground))) 255.0) |
| 180 | + b (/ (+ (* factor (:b sky)) (* (- 1.0 factor) (:b ground))) 255.0)] |
| 181 | + {:ambient-r (* r multiplier) |
| 182 | + :ambient-g (* g multiplier) |
| 183 | + :ambient-b (* b multiplier)})) |
| 184 | + |
| 185 | +;; --- multi-light shading --- |
| 186 | + |
| 187 | +(defn- resolve-lights |
| 188 | + "Normalizes light input to a vector of light maps. |
| 189 | + Supports single :light map or :lights vector." |
| 190 | + [light lights] |
| 191 | + (cond |
| 192 | + (seq lights) lights |
| 193 | + light [light] |
| 194 | + :else [])) |
| 195 | + |
| 196 | +(defn shade-multi-light |
| 197 | + "Shades a face using multiple lights and a Blinn-Phong material. |
| 198 | + Returns [:color/rgb r g b]." |
| 199 | + [normal cam-dir face-centroid material lights] |
| 200 | + (let [base-color (or (:material/color material) [:color/rgb 128 128 128]) |
| 201 | + [_ br bg bb] base-color |
| 202 | + mat-ambient (double (:material/ambient material)) |
| 203 | + ;; Accumulate contributions from all lights |
| 204 | + ;; Resolve light color to [0-1] RGB scale factors |
| 205 | + light-rgb (fn [light] |
| 206 | + (if-let [c (:light/color light)] |
| 207 | + (let [resolved (color/resolve-color c)] |
| 208 | + [(/ (double (:r resolved)) 255.0) |
| 209 | + (/ (double (:g resolved)) 255.0) |
| 210 | + (/ (double (:b resolved)) 255.0)]) |
| 211 | + [1.0 1.0 1.0])) |
| 212 | + result (reduce |
| 213 | + (fn [{:keys [diff-r diff-g diff-b spec-r spec-g spec-b |
| 214 | + amb-r amb-g amb-b]} light] |
| 215 | + (let [light-type (get light :light/type :directional) |
| 216 | + [lr lg lb] (light-rgb light)] |
| 217 | + (case light-type |
| 218 | + :directional |
| 219 | + (let [{:keys [diffuse specular]} (directional-contribution normal cam-dir material light)] |
| 220 | + {:diff-r (+ diff-r (* diffuse lr)) :diff-g (+ diff-g (* diffuse lg)) :diff-b (+ diff-b (* diffuse lb)) |
| 221 | + :spec-r (+ spec-r (* specular lr)) :spec-g (+ spec-g (* specular lg)) :spec-b (+ spec-b (* specular lb)) |
| 222 | + :amb-r amb-r :amb-g amb-g :amb-b amb-b}) |
| 223 | + |
| 224 | + :omni |
| 225 | + (let [{:keys [diffuse specular]} (omni-contribution normal cam-dir material light face-centroid)] |
| 226 | + {:diff-r (+ diff-r (* diffuse lr)) :diff-g (+ diff-g (* diffuse lg)) :diff-b (+ diff-b (* diffuse lb)) |
| 227 | + :spec-r (+ spec-r (* specular lr)) :spec-g (+ spec-g (* specular lg)) :spec-b (+ spec-b (* specular lb)) |
| 228 | + :amb-r amb-r :amb-g amb-g :amb-b amb-b}) |
| 229 | + |
| 230 | + :spot |
| 231 | + (let [{:keys [diffuse specular]} (spot-contribution normal cam-dir material light face-centroid)] |
| 232 | + {:diff-r (+ diff-r (* diffuse lr)) :diff-g (+ diff-g (* diffuse lg)) :diff-b (+ diff-b (* diffuse lb)) |
| 233 | + :spec-r (+ spec-r (* specular lr)) :spec-g (+ spec-g (* specular lg)) :spec-b (+ spec-b (* specular lb)) |
| 234 | + :amb-r amb-r :amb-g amb-g :amb-b amb-b}) |
| 235 | + |
| 236 | + :hemisphere |
| 237 | + (let [{:keys [ambient-r ambient-g ambient-b]} (hemisphere-contribution normal light)] |
| 238 | + {:diff-r diff-r :diff-g diff-g :diff-b diff-b |
| 239 | + :spec-r spec-r :spec-g spec-g :spec-b spec-b |
| 240 | + :amb-r (+ amb-r ambient-r) :amb-g (+ amb-g ambient-g) :amb-b (+ amb-b ambient-b)}) |
| 241 | + |
| 242 | + ;; Unknown light type — skip |
| 243 | + {:diff-r diff-r :diff-g diff-g :diff-b diff-b |
| 244 | + :spec-r spec-r :spec-g spec-g :spec-b spec-b |
| 245 | + :amb-r amb-r :amb-g amb-g :amb-b amb-b}))) |
| 246 | + {:diff-r 0.0 :diff-g 0.0 :diff-b 0.0 |
| 247 | + :spec-r 0.0 :spec-g 0.0 :spec-b 0.0 |
| 248 | + :amb-r mat-ambient :amb-g mat-ambient :amb-b mat-ambient} |
| 249 | + lights) |
| 250 | + {:keys [diff-r diff-g diff-b spec-r spec-g spec-b amb-r amb-g amb-b]} result |
| 251 | + r (clamp-byte (+ (* (double br) (min 1.0 (+ amb-r diff-r))) (* 255.0 spec-r))) |
| 252 | + g (clamp-byte (+ (* (double bg) (min 1.0 (+ amb-g diff-g))) (* 255.0 spec-g))) |
| 253 | + b (clamp-byte (+ (* (double bb) (min 1.0 (+ amb-b diff-b))) (* 255.0 spec-b)))] |
62 | 254 | [:color/rgb r g b])) |
63 | 255 |
|
| 256 | +;; --- backward-compatible API --- |
| 257 | + |
| 258 | +(defn shade-phong |
| 259 | + "Computes Blinn-Phong shading for a face with a single directional light." |
| 260 | + [normal light-dir cam-dir light material] |
| 261 | + (shade-multi-light normal cam-dir [0 0 0] material |
| 262 | + [(assoc light :light/type :directional :light/direction light-dir)])) |
| 263 | + |
64 | 264 | (defn shade-face |
65 | | - "Shades a face style using a material descriptor. |
66 | | - Falls back to the material's color if the face has no fill. |
67 | | - Returns updated style map with shaded fill and stroke." |
68 | | - [style normal light-dir cam-dir light material] |
69 | | - (let [mat-color (or (:material/color material) |
70 | | - (:style/fill style)) |
71 | | - material (assoc material :material/color mat-color) |
72 | | - shaded-color (shade-phong normal light-dir cam-dir light material)] |
73 | | - (cond-> (assoc style :style/fill shaded-color) |
74 | | - (:style/stroke style) |
75 | | - (assoc-in [:style/stroke :color] shaded-color)))) |
| 265 | + "Shades a face style using a material and lights. |
| 266 | + Supports single light or lights vector." |
| 267 | + ([style normal light-dir cam-dir light material] |
| 268 | + (shade-face style normal light-dir cam-dir light material nil nil)) |
| 269 | + ([style normal light-dir cam-dir light material lights face-centroid] |
| 270 | + (let [mat-color (or (:material/color material) (:style/fill style)) |
| 271 | + material (assoc material :material/color mat-color) |
| 272 | + all-lights (resolve-lights light lights) |
| 273 | + centroid (or face-centroid [0 0 0]) |
| 274 | + shaded-color (shade-multi-light normal cam-dir centroid material all-lights)] |
| 275 | + (cond-> (assoc style :style/fill shaded-color) |
| 276 | + (:style/stroke style) |
| 277 | + (assoc-in [:style/stroke :color] shaded-color))))) |
0 commit comments