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
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
|
/**
* @file class1\deferred\pbrterrainUtilF.glsl
*
* $LicenseInfo:firstyear=2023&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2023, Linden Research, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
* $/LicenseInfo$
*/
/*[EXTRA_CODE_HERE]*/
/**
* Triplanar mapping implementation adapted from Inigo Quilez' example shader,
* MIT license.
* https://www.shadertoy.com/view/MtsGWH
* Copyright © 2015 Inigo Quilez
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions: The above copyright
* notice and this permission notice shall be included in all copies or
* substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS",
* WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#define TERRAIN_PBR_DETAIL_EMISSIVE 0
#define TERRAIN_PBR_DETAIL_OCCLUSION -1
#define TERRAIN_PBR_DETAIL_NORMAL -2
#define TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS -3
#define TERRAIN_PAINT_TYPE_HEIGHTMAP_WITH_NOISE 0
#define TERRAIN_PAINT_TYPE_PBR_PAINTMAP 1
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
in vec3 vary_vertex_normal;
#endif
vec3 srgb_to_linear(vec3 c);
// A relatively agressive threshold for terrain material mixing sampling
// cutoff. This ensures that only one or two materials are used in most places,
// making PBR terrain blending more performant. Should be greater than 0 to work.
#define TERRAIN_RAMP_MIX_THRESHOLD 0.1
// A small threshold for triplanar mapping sampling cutoff. This and
// TERRAIN_TRIPLANAR_BLEND_FACTOR together ensures that only one or two samples
// per texture are used in most places, making triplanar mapping more
// performant. Should be greater than 0 to work.
// There's also an artistic design choice in the use of these factors, and the
// use of triplanar generally. Don't take these triplanar constants for granted.
#define TERRAIN_TRIPLANAR_MIX_THRESHOLD 0.01
#define SAMPLE_X 1 << 0
#define SAMPLE_Y 1 << 1
#define SAMPLE_Z 1 << 2
#define MIX_X 1 << 3
#define MIX_Y 1 << 4
#define MIX_Z 1 << 5
#define MIX_W 1 << 6
struct PBRMix
{
vec4 col; // RGB color with alpha, linear space
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
vec3 orm; // Occlusion, roughness, metallic
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
vec2 rm; // Roughness, metallic
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
vec3 vNt; // Unpacked normal texture sample, vector
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
vec3 emissive; // RGB emissive color, linear space
#endif
};
PBRMix init_pbr_mix()
{
PBRMix mix;
mix.col = vec4(0);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
mix.orm = vec3(0);
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
mix.rm = vec2(0);
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
mix.vNt = vec3(0);
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
mix.emissive = vec3(0);
#endif
return mix;
}
// Usage example, for two weights:
// vec2 weights = ... // Weights must add up to 1
// PBRMix mix = init_pbr_mix();
// PBRMix mix1 = ...
// mix = mix_pbr(mix, mix1, weights.x);
// PBRMix mix2 = ...
// mix = mix_pbr(mix, mix2, weights.y);
PBRMix mix_pbr(PBRMix mix1, PBRMix mix2, float mix2_weight)
{
PBRMix mix;
mix.col = mix1.col + (mix2.col * mix2_weight);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
mix.orm = mix1.orm + (mix2.orm * mix2_weight);
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
mix.rm = mix1.rm + (mix2.rm * mix2_weight);
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
mix.vNt = mix1.vNt + (mix2.vNt * mix2_weight);
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
mix.emissive = mix1.emissive + (mix2.emissive * mix2_weight);
#endif
return mix;
}
PBRMix sample_pbr(
vec2 uv
, sampler2D tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, sampler2D tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, sampler2D tex_vNt
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, sampler2D tex_emissive
#endif
)
{
PBRMix mix;
mix.col = texture(tex_col, uv);
mix.col.rgb = srgb_to_linear(mix.col.rgb);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
mix.orm = texture(tex_orm, uv).xyz;
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
mix.rm = texture(tex_orm, uv).yz;
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
mix.vNt = texture(tex_vNt, uv).xyz*2.0-1.0;
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
mix.emissive = srgb_to_linear(texture(tex_emissive, uv).xyz);
#endif
return mix;
}
struct TerrainTriplanar
{
vec3 weight;
int type;
};
struct TerrainMix
{
vec4 weight;
int type;
};
#define TerrainMixSample vec4[4]
#define TerrainMixSample3 vec3[4]
TerrainMix get_terrain_mix_weights(float alpha1, float alpha2, float alphaFinal)
{
TerrainMix tm;
vec4 sample_x = vec4(1,0,0,0);
vec4 sample_y = vec4(0,1,0,0);
vec4 sample_z = vec4(0,0,1,0);
vec4 sample_w = vec4(0,0,0,1);
tm.weight = mix( mix(sample_w, sample_z, alpha2), mix(sample_y, sample_x, alpha1), alphaFinal );
tm.weight -= TERRAIN_RAMP_MIX_THRESHOLD;
ivec4 usage = max(ivec4(0), ivec4(ceil(tm.weight)));
// Prevent negative weights and keep weights balanced
tm.weight = tm.weight*vec4(usage);
tm.weight /= (tm.weight.x + tm.weight.y + tm.weight.z + tm.weight.w);
tm.type = (usage.x * MIX_X) |
(usage.y * MIX_Y) |
(usage.z * MIX_Z) |
(usage.w * MIX_W);
return tm;
}
// A paintmap weight applier for 4 swatches. The input saves a channel by not
// storing swatch 1, and assuming the weights of the 4 swatches add to 1.
// The components of weight3 should be between 0 and 1
// The sum of the components of weight3 should be between 0 and 1
TerrainMix get_terrain_usage_from_weight3(vec3 weight3)
{
// These steps ensure the output weights add to between 0 and 1
weight3.xyz = max(vec3(0.0), weight3.xyz);
weight3.xyz /= max(1.0, weight3.x + weight3.y + weight3.z);
TerrainMix tm;
// Extract the first weight from the other weights
tm.weight.x = 1.0 - (weight3.x + weight3.y + weight3.z);
tm.weight.yzw = weight3.xyz;
ivec4 usage = max(ivec4(0), ivec4(ceil(tm.weight)));
tm.type = (usage.x * MIX_X) |
(usage.y * MIX_Y) |
(usage.z * MIX_Z) |
(usage.w * MIX_W);
return tm;
}
// Inverse of get_terrain_usage_from_weight3, excluding usage flags
// The components of weight should be between 0 and 1
// The sum of the components of weight should be 1
vec3 get_weight3_from_terrain_weight(vec4 weight)
{
// These steps ensure the input weights add to 1
weight = max(vec4(0.0), weight);
weight.x += 1.0 - sign(weight.x + weight.y + weight.z + weight.w);
weight /= weight.x + weight.y + weight.z + weight.w;
// Then return the input weights with the first weight truncated
return weight.yzw;
}
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
TerrainTriplanar _t_triplanar()
{
float sharpness = TERRAIN_TRIPLANAR_BLEND_FACTOR;
float threshold = TERRAIN_TRIPLANAR_MIX_THRESHOLD;
vec3 weight_signed = pow(abs(vary_vertex_normal), vec3(sharpness));
weight_signed /= (weight_signed.x + weight_signed.y + weight_signed.z);
weight_signed -= vec3(threshold);
TerrainTriplanar tw;
// *NOTE: Make sure the threshold doesn't affect the materials
tw.weight = max(vec3(0), weight_signed);
tw.weight /= (tw.weight.x + tw.weight.y + tw.weight.z);
ivec3 usage = ivec3(round(max(vec3(0), sign(weight_signed))));
tw.type = ((usage.x) * SAMPLE_X) |
((usage.y) * SAMPLE_Y) |
((usage.z) * SAMPLE_Z);
return tw;
}
#endif
// Assume weights add to 1
float terrain_mix(TerrainMix tm, vec4 tms4)
{
return (tm.weight.x * tms4[0]) +
(tm.weight.y * tms4[1]) +
(tm.weight.z * tms4[2]) +
(tm.weight.w * tms4[3]);
}
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
// Triplanar mapping
// Pre-transformed texture coordinates for each axial uv slice (Packing: xy, yz, (-x)z, unused)
#define TerrainCoord vec4[3]
// If sign_or_zero is positive, use uv_unflippped, otherwise use uv_flipped
vec2 _t_uv(vec2 uv_unflipped, vec2 uv_flipped, float sign_or_zero)
{
return mix(uv_flipped, uv_unflipped, max(0.0, sign_or_zero));
}
vec3 _t_normal_post_1(vec3 vNt0, float sign_or_zero)
{
// Assume normal is unpacked
vec3 vNt1 = vNt0;
// Get sign
float sign = sign_or_zero;
// Handle case where sign is 0
sign = (2.0*sign) + 1.0;
sign /= abs(sign);
// If the sign is negative, rotate normal by 180 degrees
vNt1.xy = (min(0, sign) * vNt1.xy) + (min(0, -sign) * -vNt1.xy);
return vNt1;
}
// Triplanar-specific normal texture fixes
vec3 _t_normal_post_x(vec3 vNt0, float tangent_sign)
{
vec3 vNt_x = _t_normal_post_1(vNt0, sign(vary_vertex_normal.x));
// *HACK: Transform normals according to orientation of the UVs
vNt_x.xy = vec2(-vNt_x.y, vNt_x.x);
vNt_x.xy *= tangent_sign;
return vNt_x;
}
vec3 _t_normal_post_y(vec3 vNt0)
{
vec3 vNt_y = _t_normal_post_1(vNt0, sign(vary_vertex_normal.y));
// *HACK: Transform normals according to orientation of the UVs
vNt_y.xy = -vNt_y.xy;
return vNt_y;
}
vec3 _t_normal_post_z(vec3 vNt0)
{
vec3 vNt_z = _t_normal_post_1(vNt0, sign(vary_vertex_normal.z));
return vNt_z;
}
PBRMix terrain_sample_pbr(
TerrainCoord terrain_coord
, TerrainTriplanar tw
, sampler2D tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, sampler2D tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, sampler2D tex_vNt
, float tangent_sign
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, sampler2D tex_emissive
#endif
)
{
PBRMix mix = init_pbr_mix();
#define get_uv_x() _t_uv(terrain_coord[0].zw, terrain_coord[1].zw, sign(vary_vertex_normal.x))
#define get_uv_y() _t_uv(terrain_coord[1].xy, terrain_coord[2].xy, sign(vary_vertex_normal.y))
#define get_uv_z() _t_uv(terrain_coord[0].xy, vec2(0), sign(vary_vertex_normal.z))
switch (tw.type & SAMPLE_X)
{
case SAMPLE_X:
PBRMix mix_x = sample_pbr(
get_uv_x()
, tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, tex_vNt
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, tex_emissive
#endif
);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
// Triplanar-specific normal texture fix
mix_x.vNt = _t_normal_post_x(mix_x.vNt, tangent_sign);
#endif
mix = mix_pbr(mix, mix_x, tw.weight.x);
break;
default:
break;
}
switch (tw.type & SAMPLE_Y)
{
case SAMPLE_Y:
PBRMix mix_y = sample_pbr(
get_uv_y()
, tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, tex_vNt
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, tex_emissive
#endif
);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
// Triplanar-specific normal texture fix
mix_y.vNt = _t_normal_post_y(mix_y.vNt);
#endif
mix = mix_pbr(mix, mix_y, tw.weight.y);
break;
default:
break;
}
switch (tw.type & SAMPLE_Z)
{
case SAMPLE_Z:
PBRMix mix_z = sample_pbr(
get_uv_z()
, tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, tex_vNt
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, tex_emissive
#endif
);
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
// Triplanar-specific normal texture fix
// *NOTE: Bottom face has not been tested
mix_z.vNt = _t_normal_post_z(mix_z.vNt);
#endif
mix = mix_pbr(mix, mix_z, tw.weight.z);
break;
default:
break;
}
return mix;
}
#elif TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 1
#define TerrainCoord vec2
#define terrain_sample_pbr sample_pbr
#endif
PBRMix multiply_factors_pbr(
PBRMix mix_in
, vec4 factor_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
, vec3 factor_orm
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, vec2 factor_rm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, vec3 factor_emissive
#endif
)
{
PBRMix mix = mix_in;
mix.col *= factor_col;
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
mix.orm *= factor_orm;
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
mix.rm *= factor_rm;
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
mix.emissive *= factor_emissive;
#endif
return mix;
}
PBRMix terrain_sample_and_multiply_pbr(
TerrainCoord terrain_coord
, sampler2D tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, sampler2D tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, sampler2D tex_vNt
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
, float tangent_sign
#endif
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, sampler2D tex_emissive
#endif
, vec4 factor_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
, vec3 factor_orm
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, vec2 factor_rm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, vec3 factor_emissive
#endif
)
{
PBRMix mix = terrain_sample_pbr(
terrain_coord
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
, _t_triplanar()
#endif
, tex_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, tex_orm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_NORMAL)
, tex_vNt
#if TERRAIN_PLANAR_TEXTURE_SAMPLE_COUNT == 3
, tangent_sign
#endif
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, tex_emissive
#endif
);
mix = multiply_factors_pbr(mix
, factor_col
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_OCCLUSION)
, factor_orm
#elif (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_METALLIC_ROUGHNESS)
, factor_rm
#endif
#if (TERRAIN_PBR_DETAIL >= TERRAIN_PBR_DETAIL_EMISSIVE)
, factor_emissive
#endif
);
return mix;
}
|