hotline-rs 0.3.2

A high-performance, hot-reload graphics engine.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
// returns constant pi
float pi() {
    return 3.141592653589793238462643;
}

// returns constant inverse pi (1.0/pi)
float inv_pi() {
    return 0.318309886;
}

// returns constant phi (golden ratio)
float phi() {
    return 1.61803399;
}

// returns constant tau which is the ratio of the circumference to the radius of a circle
float tau() {
    return 6.283185307179586;
}

// returns a vector normalized / projected to a square
float3 chebyshev_normalize(float3 v) {
    return (v.xyz / max(max(abs(v.x), abs(v.y)), abs(v.z)));
}

// performs signed distance union of `d1` and `d2`
float op_union(float d1, float d2) {
    return min(d1,d2);
}

// performs subtract operation from 2 distance fields `d1` and `d2`
float op_subtract(float d1, float d2) {
    return max(-d1,d2);
}

// intersects 2 distance fields `d1` and `d2`
float op_intersect(float d1, float d2) {
    return max(d1,d2);
}

// signed distance from point `p` to a to sphere centred at 0 with radius `s`
float sd_sphere(float3 p, float s) {
    return length(p)-s;
}

// signed distance from point `p` to a box centred at 0 with half extents `b`
float sd_box(float3 p, float3 b) {
    float3 d = abs(p) - b;
    return min(max(d.x,max(d.y,d.z)),0.0) + length(max(d,0.0));
}

// signed distance from point `p` to an octahedron centred at 0 with half extents `b`
float sd_octahedron(float3 p, float s) {
    p = abs(p);
    return (p.x + p.y + p.z - s) * 0.57735027;
}

// unsigned distance from point `p` to a box centred at 0 with half extents `b`
float ud_box(float3 p, float3 b) {
    return length(max(abs(p) - b, 0.0));
}

// unsigned distance from point `p` to rounded box centred at 0 with extents `b` and roundness `r`
float ud_round_box(float3 p, float3 b, float r) {
    return length(max(abs(p) - b, 0.0)) - r;
}

// signed distance from point `p` to a cross with size `s`
float sd_cross(float3 p, float2 s) {
    float da = sd_box(p.xyz, float3(s.y, s.x, s.x));
    float db = sd_box(p.yzx, float3(s.x, s.y, s.x));
    float dc = sd_box(p.zxy, float3(s.x, s.x, s.y));
    return op_union(da, op_union(db, dc));
}

// signed distance from point `p` to a tourus with size `t`
float sd_torus(float3 p, float2 t) {
    float2 q = float2(length(p.xy) - t.x,p.z);
    return length(q)-t.y;
}

// signed distance from point `p` to a cylinder with size `c`
float sd_cylinder(float3 p, float3 c) {
    return length(p.xz - c.xy) - c.z;
}

// signed distance from point `p` to a cone with size `c` where `c` must be normalized
float sd_cone(float3 p, float2 c) {
    float q = length(p.xy);
    return dot(c, float2(q, p.z));
}

// signed distance from point `p` to a plane with equation `n` where `n` must be normalized
// and n.xyz = plane normal, n.w = plane constant
float sd_plane(float3 p, float4 n) {
  return dot(p, n.xyz) + n.w;
}

// hash3 is used for voronoise
float3 hash3(float2 p) {
    float3 q = float3(
        dot(p, float2(127.1,311.7)), 
        dot(p, float2(269.5,183.3)), 
        dot(p, float2(419.2,371.9))
    );
	return frac(sin(q)*43758.5453);
}

// supply a 2d coordniate to sample the noise (p)
// control the noise type with u and v:
// cell noise (blocky squares): u = 0, v = 0
// voronoi (voronoi like): u = 1, v = 0
// voronoi noise (voronoi like but soomth): u = 1, v = 1
// noise (perlin like): u = 0, v = 1
float voronoise(float2 p, float u, float v) {
	float k = 1.0 + 63.0 * pow(1.0-v, 6.0);
    float2 i = floor(p);
    float2 f = frac(p);
	float2 a = float2(0.0,0.0);
    for(int y = -2; y <= 2; y++) {
        for(int x = -2; x <= 2; x++) {
            float2 g = float2(x, y);
            float3 o = hash3(i + g) * float3(u, u, 1.0);
            float2 d = g - f + o.xy;
            float w = pow(1.0 - smoothstep( 0.0, 1.414, length(d)), k);
            a += float2(o.z * w, w);
        }
    }
    return a.x/a.y;
}

// basic rng float for given 
float random(float2 uv) {
    return frac(sin(dot(uv.xy, float2(12.9898, 78.233))) * 43758.5453123);
}

// 2d noise for uv coordinate
// https://www.shadertoy.com/view/4dS3Wd
float noise(float2 uv) {
    float2 i = floor(uv);
    float2 f = frac(uv);

    // four corners in 2D of a tile
    float a = random(i);
    float b = random(i + float2(1.0, 0.0));
    float c = random(i + float2(0.0, 1.0));
    float d = random(i + float2(1.0, 1.0));

    float2 u = f * f * (3.0 - 2.0 * f);

    return lerp(a, b, u.x) + (c - a)* u.y * (1.0 - u.x) + (d - b) * u.x * u.y;
}

// fractal brownian motion for uv coordinate with n `octaves`
// https://www.shadertoy.com/view/4dS3Wd
float fbm(float2 uv, int octaves) {
    float value = 0.0;
    float amplitude = 0.3;
    float frequency = 0.0;
    for (int i = 0; i < octaves; i++) {
        value += amplitude * noise(uv);
        uv *= 3.0;
        amplitude *= 0.8;
    }
    return value;
}

// returns an rgb colour gradient for the value of x into 3 bands (like a heatmap)
float3 uv_gradient(float x) {
    float3 rgb_uv = float3(0.0, 0.0, 0.0);
    float grad = x % 1.0;
    if (grad < 0.333) {
        rgb_uv = lerp(float3(1.0, 0, 0.0), float3(0.0, 1.0, 0.0), grad * 3.333);
    }
    else if (grad < 0.666) {
        rgb_uv = lerp(float3(0.0, 1.0, 0.0), float3(0.0, 0.0, 1.0), (grad - 0.333) * 3.333);
    }
    else {
        rgb_uv = lerp(float3(0.0, 0.0, 1.0), float3(1.0, 0.0, 0.0), (grad - 0.666) * 3.333);
    }
    return rgb_uv;
}

// constructs ortho basis using hughes_moeller selecting from iput vector v, returning orthogonal vectors in bt and t
void construct_orthonormal_basis_hughes_moeller(float3 v, float3 out bt, float3 out t) {
    // choose a vector orthogonal to cv as the direction of b2.
    b = float3(0.0, -cv.z, cv.y);
    if(abs(n.x) > abs(n.z))
    {
        b = float3(-cv.y, cv.x, 0.0);
    }

    // normalise b2 and construct t
    b = b * rsqrt(dot(b, b));
    float3 t = cross(B, n);
}

// lambertian diffuse, where `l` is the direction from the light to the surface and `n` is the surface normal
float lambert(float3 l, float3 n) {
    return saturate(1.0 - dot(n, l));
}

// phong specular term, where `l` is the direction from the light to the surface, `n` is the surface normal
// `v` is the direction from the camera to the surface, `ks` is the specular coefficient useful in range 0-1
// `shininess controls the size of the highlight and useful ranges from 0-1000
float phong(
    float3 l,
    float3 n,
    float3 v,
    float ks,
    float shininess
) {
    return saturate(ks * pow(max(dot(reflect(-l, n), v), 0.0), shininess));
}

// blinn specular term, where `l` is the direction from surface to the light and `n` is the surface normal
// `v` is the direction from the camera to the surface, `ks` is the specular coefficient useful in range 0-1
// `shininess controls the size of the highlight and useful ranges from 0-1000
float blinn(
    float3 l,
    float3 n,
    float3 v,
    float ks,
    float shininess
) {
    float3 half_vector = -normalize(l + v);
    return saturate(ks * pow(saturate(dot(half_vector, n)), shininess));
}

// cook_torrance specular term with microfacet distribution
// where `l` is the direction from the light to the surface, `n` is the surface normal and `v` is the direction from the
// camera to the surface, `roughness` is the surface roughness in 0-1 range and `k` is the reflectivity coefficient
float cook_torrance(
    float3 l,
    float3 n,
    float3 v,
    float roughness,
    float k
) {
    l = -l;
    float n_dot_l = dot(n, l);
    if( n_dot_l > 0.0f ) {
        float roughness_sq = roughness * roughness;
        float3 hv = normalize(-v + l);
        
        float n_dot_v = dot(n, -v);
        float n_dot_h = dot(n, hv);
        float v_dot_h = dot(-v, hv);
        
        // geometric attenuation
        float n_dot_h_2 = 2.0f * n_dot_h;
        float g1 = (n_dot_h_2 * n_dot_v) / v_dot_h;
        float g2 = (n_dot_h_2 * n_dot_l) / v_dot_h;
        float geom_atten = min(1.0, min(g1, g2));
        
        // microfacet distribution function: beckmann
        float r1 = 1.0f / ( 4.0f * roughness_sq * pow(n_dot_h, 4.0f));
        float r2 = (n_dot_h * n_dot_h - 1.0) / (roughness_sq * n_dot_h * n_dot_h);
        float roughness_atten = r1 * exp(r2);
        
        // fresnel: schlick approximation
        float fresnel = pow(1.0 - v_dot_h, 5.0);
        fresnel *= roughness;
        fresnel += k;
                
        // specular
        float specular = (fresnel * geom_atten * roughness_atten) / (n_dot_v * n_dot_l * pi());
        return saturate(n_dot_l * (k + specular * (1.0 - k)));
    }   
    return 0.0;
}

// oren nayar diffuse to model reflection from rough surfaces
// where `l` is the direction from the light to the surface, `n` is the surface normal 
// `v` is the direction from the camera to the surface, `lum` represents the surface luminosity
// and `roughness` controls surface roughness in 0-1 range
float oren_nayar(
    float3 l,
    float3 n,
    float3 v,
    float lum,
    float roughness
) {
    l = -l;
    float l_dot_v = dot(l, v);
    float n_dot_l = dot(n, l);
    float n_dot_v = dot(n, v);

    float s = l_dot_v - n_dot_l * n_dot_v;
    float t = lerp(1.0, max(n_dot_l, n_dot_v), step(0.0, s));

    float sigma2 = roughness * roughness;
    float A = 1.0 + sigma2 * (lum / (sigma2 + 0.13) + 0.5 / (sigma2 + 0.33));
    float B = 0.45 * sigma2 / (sigma2 + 0.09);

    return max(n_dot_l, 0.0) * (A + B * s / t) / pi();
}

// calculates point light attenuation in respect to radius, where the light pos and world pos
// are both in world space, the atteniuation has infinite fall off
float point_light_attenuation(float3 light_pos, float radius, float3 world_pos) {
    float d = length(world_pos.xyz - light_pos.xyz);
    float r = radius;    
    float denom = d/r + 1.0;
    return 1.0 / (denom*denom);
}

// calculates a point light attentuation falloff such that the returned value reaches 0.0
// at the radius of the light and 1.0 when the distance to light is 0
float point_light_attenuation_cutoff(float3 light_pos, float radius, float3 world_pos) {
    float r = radius;
    float d = length(world_pos.xyz - light_pos.xyz);
    d = max(d - r, 0.0);
    float denom = d/r + 1.0;
    float attenuation = 1.0 / (denom*denom);
    float cutoff = 0.2;
    attenuation = (attenuation - cutoff) / (1.0 - cutoff);
    return max(attenuation, 0.0);
}

// returns a scalar attenuation coefficient for spot light, where `l` is the direction of the light to the surafce 
// cutoff is in radians (a wider or tighter cone) and the falloff ranges 0-1 (giving softer or harder edges)
float spot_light_attenuation(
    float3 l,
    float3 spot_dir,
    float  cutoff,
    float  falloff
) {
    float dp = (1.0 - dot(l, spot_dir));
    return smoothstep(cutoff, cutoff - falloff, dp);
}

// creates a crt scaline effect, returning src modulated, `tc` defines 0-1 uv space and tscale defines
// the scale of the crt texel size, use 1.0/image_size for 1:1 mapping, but you can tweak that for different effects
float3 crt_c(float3 src, float2 tc, float2 tscale) {
    float2 ca = float2(tscale.x * 2.0, 0.0);
    src.rgb *= saturate(abs(sin(tc.y / tscale.y/2.0)) + 0.5);
    return src;
}

// bends texcoords to create a crt monitor like curvature for the given `uv` in 0-1 range
float2 bend_tc(float2 uv){
    float2 tc = uv;
    float2 cc = tc - 0.5;
    float dist = dot(cc, cc) * 0.07;
    tc = tc * (tc + cc * (1.0 + dist) * dist) / tc;
    return tc;
}

// returns true if the sphere at `pos` with `radius` is inside or intersects the frustum
// defined by 6 planes (.xyz=normal, .w=constant (distance from origin))
bool sphere_vs_frustum(float3 pos, float radius, float4 planes[6]) {
    for (uint p = 0; p < 6; ++p) {
        float d = dot(pos, planes[p].xyz) + planes[p].w;
        if (d > radius) {
            return false;
        }
    }
    return true;
}

// returns true if an aabb defined by aabb_pos (centre) and aabb_extent (half extent) is inside or intersecting the frustum
// defined by 6 planes (.xyz=plane normal, .w=constant (distance from origin))
// implemented via info detailed in this insightful blog post: https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling
bool aabb_vs_frustum(float3 aabb_pos, float3 aabb_extent, float4 planes[6]) {
    bool inside = true;
    for (int p = 0; p < 6; ++p) {
        float3 sign_flip = sign(planes[p].xyz) * -1.0f;
        float pd = planes[p].w;
        float d2 = dot(aabb_pos + aabb_extent * sign_flip, planes[p].xyz);
        if (d2 > -pd) {
            return false;
        }
    }
    return true;
}

// returns the fresnel factor for specular reflectance where `cos_theta` is h.v and `f0` is the reflectance at normal incidence
float3 fresnel_schlick(float cos_theta, float3 f0)
{
    return f0 + (1.0 - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
}

// returns the fresnel factor for specular reflectance accounting for material roughness where
// `cos_theta` is h.v and `f0` is the reflectance at normal incidence
// `roughness` is the material roughness in 0-1 range   
float3 fresnel_schlick_roughness(float cos_theta, float3 f0, float roughness)
{
    float invr = 1.0 - roughness;
    return f0 + (max(float3(invr, invr, invr), f0) - f0) * pow(clamp(1.0 - cos_theta, 0.0, 1.0), 5.0);
}

// normal distribution function (ndf) which returns the probability density of sufrace normals where
// `n` is the surface normal, `n_dot_h` is the dot product of surface normal and the half angle h
// and `roughness` is the material roughness in 0-1 range 
float distribution_beckmann(float3 n, float n_dot_h, float roughness)
{
    // microfacet distribution function: beckmann
    /*
    float r2 = roughness * roughness;
    float nh2 = n_dot_h * n_dot_h;
    float r1 = 1.0f / ( 4.0f * r2 * pow(n_dot_h, 4.0f));
    float r2 = (nh2 * nh2 - 1.0) / (r2 * nh2);
    return r1 * exp(r2);
    */

    float nh2 = n_dot_h * n_dot_h;
    float tanh = (1.0 - nh2) / nh2;
    float tanh2 = tanh*tanh;
    float r2 = roughness * roughness;
    float denom = (pi() * r2) * pow(n_dot_h, 4.0);
    return exp(-tanh2 / r2) / denom;
}

// normal distribution function (ndf) which returns the probability density of sufrace normals where
// `n` is the surface normal, `h` is the halfway vector between the surface normal and view direction
// and `roughness` is the material roughness in 0-1 range 
float distribution_ggx(float3 n, float3 h, float roughness)
{
    float a = roughness * roughness;
    float a2 = a * a;
    float nh = max(dot(h, n), 0.0);
    float nh2 = nh * nh;
    float nom = a2;
    float denom = (nh2 * (a2 - 1.0) + 1.0);
    denom = pi() * denom * denom;

    return nom / denom;
}

// geometry term which returns the probability that light will reflect light toward the viewer or be occluded
// where `n_dot_v` is the dot product of n (the surface normal) and v (the view direction)
// and `roughness` is the material roughness in 0-1 range 
float geometry_schlick_ggx(float n_dot_v, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r * r) / 8.0;
    float nom = n_dot_v;
    float denom = n_dot_v * (1.0 - k) + k;

    return nom / denom;
}

// geometry term which returns the probability that light will reflect light toward the viewer or be occluded
// where `n` is the surface normal, `v` is the view direction, `l` is the light direction
// and `roughness` is the material roughness in 0-1 range 
float geometry_smith(float3 n, float3 v, float3 l, float roughness)
{
    float n_dot_v = max(dot(n, v), 0.0);
    float n_dot_l = max(dot(n, l), 0.0);
    float ggx2 = geometry_schlick_ggx(n_dot_v, roughness);
    float ggx1 = geometry_schlick_ggx(n_dot_l, roughness);

    return ggx1 * ggx2;
}

// probability density function which returns the density of scattered light for the light direction vector `l`
// viewing direction `v` and `g` is the assymetry factor (anisotropy) that describes the degree of forward or back scattering 
float phase_henyey_greenstein(float3 l, float3 v, float g)
{
    float cost = dot(l, -v);
    return (1.0 - g * g) / (4.0 * pi() * pow(1.0 + g * g - 2.0 * g * cost, 3.0 / 2.0));
}