p5.env 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/p5.env.js +440 -0
- package/package.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Generative lighting
|
|
2
|
+
|
|
3
|
+
This is an experiment around replacing image lighting in p5 with something that is lighter computationally and easier to code by hand for non-photoreal workflows.
|
|
4
|
+
|
|
5
|
+
A representation for this has to be both expressive, and also cheaply blurrable for different levels of roughness. Since this is intended for generative art, is is not essential that it perfectly sum the energy coming in from different angles; it just has to have that general look. The Phong lighting model in p5.js can cheaply be evaluated at different roughness levels, but isn't quite expressive enough to create scenes with. Image lighting is more expressive -- you can create an image and draw to it with whatever you want -- but all the convolutions required to make the different roughness levels are prohibitively expensive to animate environment lighting.
|
|
6
|
+
|
|
7
|
+
The solution I'm working with here is based on angular signed distance functions, creating 2D shapes that are mapped onto an infinitely large sphere around a subject.
|
|
8
|
+
|
|
9
|
+
## Angular SDFs
|
|
10
|
+
|
|
11
|
+
The angular SDFs here take in a surface normal and return two things:
|
|
12
|
+
- **distance**: the distance in radians to the edge of a shape
|
|
13
|
+
- **thickness**: the radius of the largest circle that can be inscribed within the shape
|
|
14
|
+
|
|
15
|
+
## Builder API
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
myShader = buildEnvLightShader(() => {
|
|
19
|
+
envColor.begin()
|
|
20
|
+
const l = envLight(baseColor, envColor.dir, envColor.blur)
|
|
21
|
+
l.mix(l.envCircle(...), lightColor)
|
|
22
|
+
envColor.set(l.get())
|
|
23
|
+
envColor.end()
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`envLight(baseColor, dir, blur)` creates a builder. `dir` and `blur` are captured once and used by all subsequent method calls.
|
|
28
|
+
|
|
29
|
+
**Shape methods** (return an SDF result to pass to `mix`):
|
|
30
|
+
- `l.envCircle(center, radius)` - spherical cap; `center` is a unit vec3, `radius` in radians
|
|
31
|
+
- `l.envCapsule(a, b, radius)` - capsule between two unit vec3 endpoints, `radius` in radians
|
|
32
|
+
- `l.envStar(center, n, innerRadius, outerRadius, rotation?)` - n-pointed star; radii in radians, `n` is a plain JS integer
|
|
33
|
+
- `l.envRect(center, size, rotation?)` - rectangle; `size` is `[halfWidth, halfHeight]` as chord lengths (sine of angle, not radians), `rotation` in radians
|
|
34
|
+
- `l.envWindow(center, size, panes, barWidth)` - rectangle subdivided into panes; `panes` is `[nx, ny]`; `size` and `barWidth` are chord lengths (sine of angle, not radians — for small shapes the difference is negligible)
|
|
35
|
+
|
|
36
|
+
**Color methods** (return a scalar/vec3 usable in expressions or as a base color):
|
|
37
|
+
- `l.envGradient(center, ...stops)` - radial gradient; each stop is `{ t, color }` where `t` is angle from `center` in radians and `color` is a vec3; spread stops as individual arguments
|
|
38
|
+
- `l.envNoise(size)` - blur-aware fractal noise value; `size` is the angular scale of the largest octave
|
|
39
|
+
- `l.envNoisePlane(planeNormal, h, size, { rotation?, offset? })` - projects a planar noise field onto the sphere; `h` is the plane's height, `size` is the noise scale; `offset` is a vec2 that shifts the noise coordinate (use for animation)
|
|
40
|
+
|
|
41
|
+
**Builder methods**:
|
|
42
|
+
- `l.mix(shape, color)` - blends `color` into the accumulated result using the shape's SDF; returns `l` for chaining
|
|
43
|
+
- `l.get()` - returns the final accumulated color
|
|
44
|
+
|
|
45
|
+
## Panorama
|
|
46
|
+
|
|
47
|
+
The same `envColor` hook can also be used to create a version of `panorama()` but using your generative environment, `panoramaEnv`.
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
function envHooks() {
|
|
51
|
+
envColor.begin()
|
|
52
|
+
// Draw something here!
|
|
53
|
+
envColor.end()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let envShader, panoramaShader
|
|
57
|
+
|
|
58
|
+
function setup() {
|
|
59
|
+
envShader = buildEnvLightShader(envHooks)
|
|
60
|
+
panoramaShader = buildEnvLightPanorama(envHooks)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function draw() {
|
|
64
|
+
clear()
|
|
65
|
+
panoramaEnv(panoramaShader)
|
|
66
|
+
shader(envShader)
|
|
67
|
+
sphere(150)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The API is `panoramaEnv(shader, blur = 0)`. Pass a nonzero `blur` (in radians) to draw a blurry background instead of a default clear one.
|
package/p5.env.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
function envLight(p5, fn) {
|
|
2
|
+
fn.baseEnvLightShader = function() {
|
|
3
|
+
if (!this._baseEnvLightShader) {
|
|
4
|
+
this._baseEnvLightShader = new p5.Shader(
|
|
5
|
+
this.baseMaterialShader()._renderer,
|
|
6
|
+
this.baseMaterialShader()._vertSrc,
|
|
7
|
+
this.baseMaterialShader()._fragSrc,
|
|
8
|
+
{
|
|
9
|
+
declarations: 'vec3 n; vec3 r;',
|
|
10
|
+
vertex: {
|
|
11
|
+
...this.baseMaterialShader().hooks.vertex,
|
|
12
|
+
},
|
|
13
|
+
fragment: {
|
|
14
|
+
'vec3 envColor': `(vec3 dir, float blur) { return vec3(0.); }`,
|
|
15
|
+
...this.baseMaterialShader().hooks.fragment,
|
|
16
|
+
'Inputs getPixelInputs': `(Inputs inputs) {
|
|
17
|
+
n = inputs.normal * uCameraNormalMatrix;
|
|
18
|
+
vec3 lightDir = normalize(vViewPosition);
|
|
19
|
+
r = reflect(lightDir, inputs.normal) * uCameraNormalMatrix;
|
|
20
|
+
return inputs;
|
|
21
|
+
}`,
|
|
22
|
+
'vec4 combineColors': `(ColorComponents components) {
|
|
23
|
+
components.diffuse = HOOK_envColor(n, ${PI/2});
|
|
24
|
+
components.specular = HOOK_envColor(r, ${PI/2}/(1. + 0.25 * uShininess));
|
|
25
|
+
vec4 color = vec4(0.);
|
|
26
|
+
color.rgb += components.diffuse * components.baseColor;
|
|
27
|
+
color.rgb += components.ambient * components.ambientColor;
|
|
28
|
+
color.rgb += components.specular * components.specularColor;
|
|
29
|
+
color.rgb += components.emissive;
|
|
30
|
+
color.a = components.opacity;
|
|
31
|
+
return color;
|
|
32
|
+
}`,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return this._baseEnvLightShader
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn.buildEnvLightShader = function(...args) {
|
|
41
|
+
return this.baseEnvLightShader().modify(...args)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fn.baseEnvLightPanoramaShader = function() {
|
|
45
|
+
if (!this._baseEnvLightPanoramaShader) {
|
|
46
|
+
this._baseEnvLightPanoramaShader = new p5.Shader(
|
|
47
|
+
this.baseFilterShader()._renderer,
|
|
48
|
+
this.baseFilterShader()._vertSrc,
|
|
49
|
+
this.baseFilterShader()._fragSrc,
|
|
50
|
+
{
|
|
51
|
+
declarations: `
|
|
52
|
+
uniform float uFovY;
|
|
53
|
+
uniform float uAspect;
|
|
54
|
+
uniform mat3 uCameraRotation;
|
|
55
|
+
uniform float uBlur;
|
|
56
|
+
`,
|
|
57
|
+
fragment: {
|
|
58
|
+
'vec3 envColor': '(vec3 dir, float blur) { return vec3(0.); }',
|
|
59
|
+
...this.baseFilterShader().hooks.fragment,
|
|
60
|
+
'vec4 getColor': `(FilterInputs inputs, sampler2D tex0) {
|
|
61
|
+
float fovX = uFovY * uAspect;
|
|
62
|
+
float angleY = mix(uFovY/2.0, -uFovY/2.0, inputs.texCoord.y);
|
|
63
|
+
float angleX = mix(fovX/2.0, -fovX/2.0, inputs.texCoord.x);
|
|
64
|
+
vec3 dir = uCameraRotation * normalize(vec3(angleX, angleY, 1.0));
|
|
65
|
+
return vec4(HOOK_envColor(-dir, uBlur), 1.0);
|
|
66
|
+
}`,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return this._baseEnvLightPanoramaShader
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn.buildEnvLightPanorama = function(...args) {
|
|
75
|
+
return this.baseEnvLightPanoramaShader().modify(...args)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn.panoramaEnv = function(panoramaShader, blur = 0) {
|
|
79
|
+
const renderer = this._renderer
|
|
80
|
+
renderer.scratchMat3.inverseTranspose4x4(renderer.states.uViewMatrix)
|
|
81
|
+
renderer.scratchMat3.invert(renderer.scratchMat3)
|
|
82
|
+
panoramaShader.setUniform('uFovY', renderer.states.curCamera.cameraFOV)
|
|
83
|
+
panoramaShader.setUniform('uAspect', renderer.states.curCamera.aspectRatio)
|
|
84
|
+
panoramaShader.setUniform('uCameraRotation', renderer.scratchMat3.mat3)
|
|
85
|
+
panoramaShader.setUniform('uBlur', Math.min(blur, Math.PI / 2))
|
|
86
|
+
this.filter(panoramaShader)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Color helpers
|
|
90
|
+
|
|
91
|
+
fn.envGradient = function(dir, center, blur, ...stops) {
|
|
92
|
+
dir = p5.strandsNode(dir)
|
|
93
|
+
center = p5.strandsNode(center)
|
|
94
|
+
blur = p5.strandsNode(blur)
|
|
95
|
+
|
|
96
|
+
for (const stop of stops) {
|
|
97
|
+
stop.t = p5.strandsNode(stop.t)
|
|
98
|
+
stop.color = p5.strandsNode(stop.color)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let r = this.acos(this.clamp(this.dot(dir, center), -1, 1))
|
|
102
|
+
|
|
103
|
+
// Clamp window to [0, PI] and ensure nonzero width so blur=0 stays safe
|
|
104
|
+
let safeBlur = this.max(blur, 0.0001)
|
|
105
|
+
let a = this.max(0, r.sub(safeBlur))
|
|
106
|
+
let b = this.min(Math.PI, r.add(safeBlur))
|
|
107
|
+
|
|
108
|
+
let totalWeight = p5.strandsNode(0)
|
|
109
|
+
let weightedColor = this.vec3(0, 0, 0)
|
|
110
|
+
|
|
111
|
+
// Region before first stop: extend with its color
|
|
112
|
+
let beforeOverlap = this.max(0, this.min(b, stops[0].t).sub(a))
|
|
113
|
+
weightedColor = weightedColor.add(p5.strandsNode(stops[0].color).mult(beforeOverlap))
|
|
114
|
+
totalWeight = totalWeight.add(beforeOverlap)
|
|
115
|
+
|
|
116
|
+
// Each linear segment: integrate over the blur window overlap
|
|
117
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
118
|
+
const t0 = stops[i].t
|
|
119
|
+
const t1 = stops[i + 1].t
|
|
120
|
+
let segLo = this.max(a, t0)
|
|
121
|
+
let segHi = this.min(b, t1)
|
|
122
|
+
let segOverlap = this.max(0, segHi.sub(segLo))
|
|
123
|
+
// Average lerp factor across the overlap is (f_lo + f_hi) / 2
|
|
124
|
+
let segF0 = this.clamp(segLo.sub(t0).div(t1.sub(t0)), 0, 1)
|
|
125
|
+
let segF1 = this.clamp(segHi.sub(t0).div(t1.sub(t0)), 0, 1)
|
|
126
|
+
let segColor = this.mix(
|
|
127
|
+
stops[i].color,
|
|
128
|
+
stops[i + 1].color,
|
|
129
|
+
segF0.add(segF1).div(2)
|
|
130
|
+
)
|
|
131
|
+
weightedColor = weightedColor.add(segColor.mult(segOverlap))
|
|
132
|
+
totalWeight = totalWeight.add(segOverlap)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Region after last stop: extend with its color
|
|
136
|
+
let afterOverlap = this.max(0, b.sub(this.max(a, stops[stops.length - 1].t)))
|
|
137
|
+
weightedColor = weightedColor.add(p5.strandsNode(stops[stops.length - 1].color).mult(afterOverlap))
|
|
138
|
+
totalWeight = totalWeight.add(afterOverlap)
|
|
139
|
+
|
|
140
|
+
return weightedColor.div(totalWeight)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Shape helpers
|
|
144
|
+
|
|
145
|
+
fn.envCircle = function(dir, center, radius) {
|
|
146
|
+
dir = p5.strandsNode(dir)
|
|
147
|
+
center = p5.strandsNode(center)
|
|
148
|
+
radius = p5.strandsNode(radius)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
distance: this.acos(this.dot(dir, center)).sub(radius),
|
|
152
|
+
thickness: radius,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fn.envCapsule = function(dir, a, b, radius) {
|
|
157
|
+
dir = p5.strandsNode(dir)
|
|
158
|
+
a = p5.strandsNode(a)
|
|
159
|
+
b = p5.strandsNode(b)
|
|
160
|
+
radius = p5.strandsNode(radius)
|
|
161
|
+
|
|
162
|
+
let ba = b.sub(a)
|
|
163
|
+
let h = this.clamp(this.dot(dir.sub(a), ba).div(this.dot(ba, ba)), 0, 1)
|
|
164
|
+
let closest = this.normalize(a.add(ba.mult(h)))
|
|
165
|
+
return {
|
|
166
|
+
distance: this.acos(this.dot(dir, closest)).sub(radius),
|
|
167
|
+
thickness: radius,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn.envNoise = function(dir, size, blur, offset = [0, 0]) {
|
|
172
|
+
// Adjust if p5's noise output is not centered exactly here
|
|
173
|
+
const noiseMean = 0.5
|
|
174
|
+
|
|
175
|
+
dir = p5.strandsNode(dir)
|
|
176
|
+
size = p5.strandsNode(size)
|
|
177
|
+
blur = p5.strandsNode(blur).mult(2)
|
|
178
|
+
offset = p5.strandsNode(offset)
|
|
179
|
+
|
|
180
|
+
// Single-octave raw noise so we can stack octaves ourselves
|
|
181
|
+
this.noiseDetail(1, 0.5)
|
|
182
|
+
|
|
183
|
+
// Adding offset before dividing by size means all octaves drift at the
|
|
184
|
+
// same world-space rate (higher-frequency octaves get a proportionally
|
|
185
|
+
// larger offset in their normalized coordinate space).
|
|
186
|
+
let p = dir.add(offset).div(size)
|
|
187
|
+
let blurRatio = blur.div(size)
|
|
188
|
+
|
|
189
|
+
// Each octave fades out when blur exceeds that octave's angular size
|
|
190
|
+
let f1 = this.max(0, p5.strandsNode(1).sub(blurRatio))
|
|
191
|
+
let f2 = this.max(0, p5.strandsNode(1).sub(blurRatio.mult(2)))
|
|
192
|
+
let f3 = this.max(0, p5.strandsNode(1).sub(blurRatio.mult(4)))
|
|
193
|
+
let f4 = this.max(0, p5.strandsNode(1).sub(blurRatio.mult(8)))
|
|
194
|
+
|
|
195
|
+
// Subtract the mean before weighting so blur only attenuates variation,
|
|
196
|
+
// not the average. Add the mean back once at the end.
|
|
197
|
+
return p5.strandsNode(noiseMean)
|
|
198
|
+
.add(f1.mult(this.noise(p).sub(noiseMean)))
|
|
199
|
+
.add(f2.mult(this.noise(p.mult(2)).sub(noiseMean)).mult(0.5))
|
|
200
|
+
.add(f3.mult(this.noise(p.mult(4)).sub(noiseMean)).mult(0.25))
|
|
201
|
+
.add(f4.mult(this.noise(p.mult(8)).sub(noiseMean)).mult(0.125))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn.envNoisePlane = function(dir, planeNormal, h, size, blur, { rotation = 0, offset = [0, 0] } = {}) {
|
|
205
|
+
dir = p5.strandsNode(dir)
|
|
206
|
+
planeNormal = p5.strandsNode(planeNormal)
|
|
207
|
+
rotation = p5.strandsNode(rotation)
|
|
208
|
+
h = p5.strandsNode(h)
|
|
209
|
+
|
|
210
|
+
let up = p5.strandsTernary(this.abs(planeNormal.y).lt(0.99), this.vec3(0, 1, 0), this.vec3(1, 0, 0))
|
|
211
|
+
let xLocal = this.normalize(this.cross(up, planeNormal))
|
|
212
|
+
let yLocal = this.cross(planeNormal, xLocal)
|
|
213
|
+
|
|
214
|
+
let pn = this.dot(dir, planeNormal)
|
|
215
|
+
let px = this.dot(dir, xLocal)
|
|
216
|
+
let py = this.dot(dir, yLocal)
|
|
217
|
+
|
|
218
|
+
// Rotate in chord space, then perspective-project onto the plane (x/n).
|
|
219
|
+
let cosR = this.cos(rotation)
|
|
220
|
+
let sinR = this.sin(rotation)
|
|
221
|
+
let rpx = cosR.mult(px).add(sinR.mult(py))
|
|
222
|
+
let rpy = cosR.mult(py).sub(sinR.mult(px))
|
|
223
|
+
let invPn = h.div(pn.add(0.001))
|
|
224
|
+
let coord = this.vec2(rpx.mult(invPn), rpy.mult(invPn))
|
|
225
|
+
|
|
226
|
+
// let blurScale = h.div(this.pow(pn, 2).add(0.001))
|
|
227
|
+
// let blurScale = h.div(this.abs(pn).add(0.001))
|
|
228
|
+
let blurScale = h.div(this.pow(this.abs(pn), 1.5).add(0.001))
|
|
229
|
+
return this.mix(
|
|
230
|
+
0.5,
|
|
231
|
+
this.envNoise(coord.add(1000), size, blur.mult(blurScale), offset),
|
|
232
|
+
this.abs(pn) // Hack for smoother transition
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn.envStar = function(dir, center, n, innerRadius, outerRadius, rotation = 0) {
|
|
237
|
+
dir = p5.strandsNode(dir)
|
|
238
|
+
center = p5.strandsNode(center)
|
|
239
|
+
innerRadius = p5.strandsNode(innerRadius)
|
|
240
|
+
outerRadius = p5.strandsNode(outerRadius)
|
|
241
|
+
rotation = p5.strandsNode(rotation)
|
|
242
|
+
|
|
243
|
+
let up = p5.strandsTernary(this.abs(center.y).lt(0.99), this.vec3(0, 1, 0), this.vec3(1, 0, 0))
|
|
244
|
+
let xLocal = this.normalize(this.cross(up, center))
|
|
245
|
+
let yLocal = this.cross(center, xLocal)
|
|
246
|
+
|
|
247
|
+
const an = Math.PI / n
|
|
248
|
+
|
|
249
|
+
// True angular distance from center -- same units as innerRadius/outerRadius
|
|
250
|
+
let r = this.acos(this.clamp(this.dot(dir, center), -1, 1))
|
|
251
|
+
|
|
252
|
+
// Azimuthal angle via a single atan on raw dot products.
|
|
253
|
+
// The atan discontinuity at +-PI is harmless: mod(PI, 2an) == mod(-PI, 2an)
|
|
254
|
+
let a = this.mod(
|
|
255
|
+
this.atan(this.dot(dir, yLocal), this.dot(dir, xLocal)).sub(rotation),
|
|
256
|
+
2 * an
|
|
257
|
+
)
|
|
258
|
+
let aFolded = this.min(a, p5.strandsNode(2 * an).sub(a))
|
|
259
|
+
let q = r.mult(this.vec2(this.cos(aFolded), this.sin(aFolded)))
|
|
260
|
+
|
|
261
|
+
// Edge from outer tip to inner valley
|
|
262
|
+
let tip = this.vec2(outerRadius, 0)
|
|
263
|
+
let valley = this.vec2(innerRadius.mult(Math.cos(an)), innerRadius.mult(Math.sin(an)))
|
|
264
|
+
let edge = valley.sub(tip)
|
|
265
|
+
|
|
266
|
+
// Signed distance: negative inside the star
|
|
267
|
+
let h = this.clamp(this.dot(q.sub(tip), edge).div(this.dot(edge, edge)), 0, 1)
|
|
268
|
+
let d = this.length(q.sub(tip.add(edge.mult(h))))
|
|
269
|
+
let cross2D = edge.x.mult(q.y).sub(edge.y.mult(q.x.sub(outerRadius)))
|
|
270
|
+
d = d.mult(this.sign(cross2D.mult(-1)))
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
distance: d,
|
|
274
|
+
thickness: innerRadius,
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fn.rotate2D = function(p, angle) {
|
|
279
|
+
p = p5.strandsNode(p)
|
|
280
|
+
angle = p5.strandsNode(angle)
|
|
281
|
+
|
|
282
|
+
let c = this.cos(angle)
|
|
283
|
+
let s = this.sin(angle)
|
|
284
|
+
|
|
285
|
+
return this.vec2(
|
|
286
|
+
c.mult(p.x).sub(s.mult(p.y)),
|
|
287
|
+
s.mult(p.x).add(c.mult(p.y))
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn.envRect = function(dir, center, size, rotation = 0) {
|
|
292
|
+
dir = p5.strandsNode(dir)
|
|
293
|
+
center = p5.strandsNode(center)
|
|
294
|
+
size = p5.strandsNode(size)
|
|
295
|
+
rotation = p5.strandsNode(rotation)
|
|
296
|
+
|
|
297
|
+
let up = p5.strandsTernary(this.abs(center.y).lessThan(0.99), this.vec3(0, 1, 0), this.vec3(1, 0, 0))
|
|
298
|
+
let xLocal = this.normalize(this.cross(up, center))
|
|
299
|
+
let yLocal = this.cross(center, xLocal)
|
|
300
|
+
|
|
301
|
+
// Raw dot-product coordinates in chord space (sin of angle, not radians).
|
|
302
|
+
let px = this.dot(dir, xLocal)
|
|
303
|
+
let py = this.dot(dir, yLocal)
|
|
304
|
+
|
|
305
|
+
let cosR = this.cos(rotation)
|
|
306
|
+
let sinR = this.sin(rotation)
|
|
307
|
+
let rpx = cosR.mult(px).add(sinR.mult(py))
|
|
308
|
+
let rpy = cosR.mult(py).sub(sinR.mult(px))
|
|
309
|
+
|
|
310
|
+
let half = size.mult(0.5)
|
|
311
|
+
let q = this.abs(this.vec2(rpx, rpy)).sub(half)
|
|
312
|
+
let d = this.length(this.max(q, 0)).add(this.min(this.max(q.x, q.y), 0))
|
|
313
|
+
|
|
314
|
+
// Clip to front hemisphere to prevent a false rect at the antipodal point.
|
|
315
|
+
let hemiD = this.acos(this.clamp(this.dot(dir, center), -1, 1)).sub(Math.PI / 2)
|
|
316
|
+
d = this.max(d, hemiD)
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
distance: d,
|
|
320
|
+
thickness: this.min(half.x, half.y),
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
fn.envWindow = function(dir, center, size, panes, barWidth) {
|
|
325
|
+
dir = p5.strandsNode(dir)
|
|
326
|
+
center = p5.strandsNode(center)
|
|
327
|
+
size = p5.strandsNode(size)
|
|
328
|
+
panes = p5.strandsNode(panes)
|
|
329
|
+
barWidth = p5.strandsNode(barWidth)
|
|
330
|
+
|
|
331
|
+
let up = p5.strandsTernary(this.abs(center.y).lessThan(0.99), this.vec3(0, 1, 0), this.vec3(1, 0, 0))
|
|
332
|
+
let xLocal = this.normalize(this.cross(up, center))
|
|
333
|
+
let yLocal = this.cross(center, xLocal)
|
|
334
|
+
|
|
335
|
+
// Raw dot-product coordinates in chord space (sin of angle, not radians).
|
|
336
|
+
let p = this.vec2(this.dot(dir, xLocal), this.dot(dir, yLocal))
|
|
337
|
+
|
|
338
|
+
let half = size.mult(0.5)
|
|
339
|
+
|
|
340
|
+
let q = this.abs(p).sub(half)
|
|
341
|
+
let outerD = this.length(this.max(q, 0)).add(this.min(this.max(q.x, q.y), 0))
|
|
342
|
+
|
|
343
|
+
let cell = this.vec2(
|
|
344
|
+
size.x.div(panes.x.sub(1)),
|
|
345
|
+
size.y.div(panes.y.sub(1))
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
// position in grid space
|
|
349
|
+
let g = p.add(half)
|
|
350
|
+
|
|
351
|
+
// local position within a cell
|
|
352
|
+
let cellP = this.mod(g, cell).sub(cell.mult(0.5))
|
|
353
|
+
|
|
354
|
+
// distance to nearest vertical/horizontal bar centerlines
|
|
355
|
+
let barLocal = this.min(
|
|
356
|
+
this.abs(cellP.x),
|
|
357
|
+
this.abs(cellP.y)
|
|
358
|
+
).sub(barWidth.mult(0.5))
|
|
359
|
+
|
|
360
|
+
// final SDF: window clipped, then subtract bars
|
|
361
|
+
let d = this.max(outerD, barLocal.mult(-1))
|
|
362
|
+
|
|
363
|
+
// Clip to front hemisphere. Back-hemisphere directions also produce small
|
|
364
|
+
// chord coordinates and would create a false window at the antipodal point
|
|
365
|
+
// without this
|
|
366
|
+
let hemiD = this.acos(this.clamp(this.dot(dir, center), -1, 1)).sub(Math.PI / 2)
|
|
367
|
+
d = this.max(d, hemiD)
|
|
368
|
+
|
|
369
|
+
// thickness = size of a single pane (not full window!)
|
|
370
|
+
let paneSize = this.vec2(
|
|
371
|
+
cell.x.sub(barWidth),
|
|
372
|
+
cell.y.sub(barWidth)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
distance: d,
|
|
377
|
+
thickness: this.min(paneSize.x, paneSize.y).mult(0.5),
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
fn.mixEnv = function(result, materialColor, c, blur) {
|
|
382
|
+
c = p5.strandsNode(c)
|
|
383
|
+
materialColor = p5.strandsNode(materialColor)
|
|
384
|
+
blur = p5.strandsNode(blur)
|
|
385
|
+
|
|
386
|
+
let d = result.distance
|
|
387
|
+
let thickness = result.thickness
|
|
388
|
+
let mixAmt = p5.strandsTernary(
|
|
389
|
+
blur.equalTo(0),
|
|
390
|
+
this.step(0, d.mult(-1)),
|
|
391
|
+
this.map(d.sub(blur.div(2)), blur.mult(-1), blur, 1, 0, true)
|
|
392
|
+
)
|
|
393
|
+
let fade = this.min(1, thickness.div(blur))
|
|
394
|
+
mixAmt = mixAmt.mult(fade)
|
|
395
|
+
return this.mix(c, materialColor, mixAmt)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn.envLight = function(baseColor, dir, blur) {
|
|
399
|
+
const sketch = this
|
|
400
|
+
let c = p5.strandsNode(baseColor)
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
envCircle(center, radius) {
|
|
404
|
+
return sketch.envCircle(dir, center, radius)
|
|
405
|
+
},
|
|
406
|
+
envCapsule(a, b, radius) {
|
|
407
|
+
return sketch.envCapsule(dir, a, b, radius)
|
|
408
|
+
},
|
|
409
|
+
envStar(center, n, innerRadius, outerRadius, rotation) {
|
|
410
|
+
return sketch.envStar(dir, center, n, innerRadius, outerRadius, rotation)
|
|
411
|
+
},
|
|
412
|
+
envRect(center, size, rotation) {
|
|
413
|
+
return sketch.envRect(dir, center, size, rotation)
|
|
414
|
+
},
|
|
415
|
+
envWindow(center, size, panes, barWidth) {
|
|
416
|
+
return sketch.envWindow(dir, center, size, panes, barWidth)
|
|
417
|
+
},
|
|
418
|
+
envNoise(size) {
|
|
419
|
+
return sketch.envNoise(dir, size, blur)
|
|
420
|
+
},
|
|
421
|
+
envGradient(center, ...stops) {
|
|
422
|
+
return sketch.envGradient(dir, center, blur, ...stops)
|
|
423
|
+
},
|
|
424
|
+
envNoisePlane(planeNormal, h, size, { rotation = 0, offset = [0, 0] } = {}) {
|
|
425
|
+
return sketch.envNoisePlane(dir, planeNormal, h, size, blur, { rotation, offset })
|
|
426
|
+
},
|
|
427
|
+
mix(shape, materialColor) {
|
|
428
|
+
c = sketch.mixEnv(shape, materialColor, c, blur)
|
|
429
|
+
return this
|
|
430
|
+
},
|
|
431
|
+
get() {
|
|
432
|
+
return c
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (typeof p5 !== 'undefined') {
|
|
439
|
+
p5.registerAddon(envLight)
|
|
440
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "p5.env",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Generative environment light for p5.strands",
|
|
5
|
+
"main": "p5.env.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"author": "Dave Pagurek",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"files": [
|
|
12
|
+
"p5.env.js"
|
|
13
|
+
]
|
|
14
|
+
}
|