Skip to content

Commit 430e523

Browse files
leifericfclaude
andcommitted
Add point, spot, hemisphere lights and multi-light shading
Extends eido.ir.material with four light types inspired by 3ds Max: - directional: parallel rays (existing, enhanced with constructors) - omni: point light with position and distance decay - spot: cone light with hotspot/falloff angles and decay - hemisphere: sky/ground ambient blend by surface normal Adds multi-light support: shade-multi-light accumulates per-channel contributions from all lights. Light color tints diffuse and specular. Decay types: :none, :inverse, :inverse-square with configurable start. Updates scene3d render-mesh to pass face centroid to shading and accept :lights vector alongside :light for backward compatibility. Adds smoothstep to math3d. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cc3b3bf commit 430e523

4 files changed

Lines changed: 419 additions & 130 deletions

File tree

src/eido/ir/material.clj

Lines changed: 244 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
(ns eido.ir.material
2-
"Material descriptors for 3D rendering.
2+
"Material descriptors and multi-light shading for 3D rendering.
33
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.
65
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."
913
(:require
14+
[eido.color :as color]
1015
[eido.math3d :as m]))
1116

1217
;; --- material constructors ---
@@ -22,54 +27,251 @@
2227
:material/shininess shininess
2328
:material/color color})
2429

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 ---
2695

2796
(defn- clamp-byte ^long [^double v]
2897
(long (Math/max 0.0 (Math/min 255.0 v))))
2998

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)
43127
mat-diffuse (double (:material/diffuse material))
44128
mat-spec (double (:material/specular material))
45129
shininess (double (:material/shininess material))
46-
intensity (double (get light :light/intensity 0.7))
47-
;; Diffuse (Lambert)
48130
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))
51132
half-vec (m/normalize (m/v+ light-dir cam-dir))
52133
n-dot-h (max 0.0 (m/dot normal half-vec))
53134
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)))]
62254
[:color/rgb r g b]))
63255

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+
64264
(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)))))

src/eido/math3d.clj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
[0.0 0.0 0.0]
4343
(v* v (/ 1.0 m)))))
4444

45+
(defn smoothstep
46+
"Hermite interpolation between edge0 and edge1.
47+
Returns 0 when x <= edge0, 1 when x >= edge1, smooth curve between."
48+
^double [^double edge0 ^double edge1 ^double x]
49+
(let [t (Math/max 0.0 (Math/min 1.0 (/ (- x edge0) (- edge1 edge0))))]
50+
(* t t (- 3.0 (* 2.0 t)))))
51+
4552
;; --- rotations ---
4653

4754
(defn rotate-x

0 commit comments

Comments
 (0)