yum-archive/2ner

A toon shader for Unity's BIRP.

git clone https://git.yummers.dev/yum-archive/2ner

yumupdate lightvolumes0e53f4f

master
39.4 KiB950 linesraw
1#ifndef VRC_LIGHT_VOLUMES_INCLUDED
2#define VRC_LIGHT_VOLUMES_INCLUDED
3#define VRCLV_VERSION 2
4#define VRCLV_MAX_VOLUMES_COUNT 32
5#define VRCLV_MAX_LIGHTS_COUNT 128
6
7
8#ifndef SHADER_TARGET_SURFACE_ANALYSIS
9cbuffer LightVolumeUniforms {
10#endif
11
12// Are Light Volumes enabled on scene? can be 0 or 1
13uniform float _UdonLightVolumeEnabled;
14
15// Rreturns 1, 2 or other number if there are light volumes on the scene. Number represents the light volumes system internal version number.
16uniform float _UdonLightVolumeVersion;
17
18// All volumes count in scene
19uniform float _UdonLightVolumeCount;
20
21// Additive volumes max overdraw count
22uniform float _UdonLightVolumeAdditiveMaxOverdraw;
23
24// Additive volumes count
25uniform float _UdonLightVolumeAdditiveCount;
26
27// Should volumes be blended with lightprobes?
28uniform float _UdonLightVolumeProbesBlend;
29
30// Should volumes be with sharp edges when not blending with each other
31uniform float _UdonLightVolumeSharpBounds;
32
33// World to Local (-0.5, 0.5) UVW Matrix 4x4
34uniform float4x4 _UdonLightVolumeInvWorldMatrix[VRCLV_MAX_VOLUMES_COUNT];
35
36// L1 SH quaternion rotation (relative to baked rotation)
37//uniform float4 _UdonLightVolumeRotationQuaternion[32];
38uniform float4 _UdonLightVolumeRotation[VRCLV_MAX_VOLUMES_COUNT * 2]; // Legacy! Used in this version to have back compatibility with older worlds. Array commented above will be used in future releases! Legacy!
39
40// Value that is needed to smoothly blend volumes ( BoundsScale / edgeSmooth )
41uniform float3 _UdonLightVolumeInvLocalEdgeSmooth[VRCLV_MAX_VOLUMES_COUNT];
42
43// AABB Bounds of islands on the 3D Texture atlas. XYZ: UvwMin, W: Scale per axis
44// uniform float4 _UdonLightVolumeUvwScale[96];
45uniform float3 _UdonLightVolumeUvw[VRCLV_MAX_VOLUMES_COUNT * 6]; // Legacy! AABB Bounds of islands on the 3D Texture atlas. Array commented above will be used in future releases! Legacy!
46
47// XYZ: AABB Bounds of islands on the 3D Texture atlas storing occlusion. W: Scale factor for the occlusion volume UVW
48// This is optional data. If the volume has no occlusion, the value will be (-1, -1, -1, -1).
49uniform float4 _UdonLightVolumeOcclusionUvw[VRCLV_MAX_VOLUMES_COUNT];
50
51// Color multiplier (RGB) | If we actually need to rotate L1 components at all (A)
52uniform float4 _UdonLightVolumeColor[VRCLV_MAX_VOLUMES_COUNT];
53
54// Point Lights count
55uniform float _UdonPointLightVolumeCount;
56
57// Cubemaps count in the custom textures array
58uniform float _UdonPointLightVolumeCubeCount;
59
60// For point light: XYZ = Position, W = Inverse squared range
61// For spot light: XYZ = Position, W = Inverse squared range, negated
62// For area light: XYZ = Position, W = Width
63uniform float4 _UdonPointLightVolumePosition[VRCLV_MAX_LIGHTS_COUNT];
64
65// For point light: XYZ = Color, W = Cos of angle (for LUT)
66// For spot light: XYZ = Color, W = Cos of outer angle if no custom texture, tan of outer angle otherwise
67// For area light: XYZ = Color, W = 2 + Height
68uniform float4 _UdonPointLightVolumeColor[VRCLV_MAX_LIGHTS_COUNT];
69
70// For point light: XYZW = Rotation quaternion
71// For spot light: XYZ = Direction, W = Cone falloff
72// For area light: XYZW = Rotation quaternion
73uniform float4 _UdonPointLightVolumeDirection[VRCLV_MAX_LIGHTS_COUNT];
74
75// X = Custom ID:
76//   If parametric: X stores 0
77//   If uses custom lut: X stores LUT ID with positive sign
78//   If uses custom texture: X stores texture ID with negative sign
79// Y = Shadowmask index. If light doesn't use shadowmask, the index will be negative.
80// Z = Squared Culling Range. Just a precalculated culling range to not recalculate in in shader.
81uniform float3 _UdonPointLightVolumeCustomID[VRCLV_MAX_LIGHTS_COUNT];
82
83// If we are far enough from a light that the irradiance
84// is guaranteed lower than the threshold defined by this value,
85// we cull the light.
86uniform float _UdonLightBrightnessCutoff;
87
88// The number of volumes that provide occlusion data.
89// We use this to take faster paths when no occlusion is needed.
90uniform float _UdonLightVolumeOcclusionCount;
91
92#ifndef SHADER_TARGET_SURFACE_ANALYSIS
93}
94#endif
95
96#ifndef SHADER_TARGET_SURFACE_ANALYSIS
97
98// Main 3D Texture atlas
99uniform Texture3D _UdonLightVolume;
100uniform SamplerState sampler_UdonLightVolume;
101// First elements must be cubemap faces (6 face textures per cubemap). Then goes other textures
102uniform Texture2DArray _UdonPointLightVolumeTexture;
103// Samples a texture using mip 0, and reusing a single sampler
104#define LV_SAMPLE(tex, uvw) tex.SampleLevel(sampler_UdonLightVolume, uvw, 0)
105
106#else
107
108// Dummy macro definition to satisfy MojoShader (surface shaders).
109#define LV_SAMPLE(tex, uvw) float4(0,0,0,0)
110
111#endif
112
113#define LV_PI 3.141592653589793f
114#define LV_PI2 6.283185307179586f
115
116// Smoothstep to 0, 1 but cheaper
117float LV_Smoothstep01(float x) {
118    return x * x * (3 - 2 * x);
119}
120
121// Rotates vector by Quaternion
122float3 LV_MultiplyVectorByQuaternion(float3 v, float4 q) {
123    float3 t = 2.0 * cross(q.xyz, v);
124    return v + q.w * t + cross(q.xyz, t);
125}
126
127// Rotates vector by Matrix 2x3
128float3 LV_MultiplyVectorByMatrix2x3(float3 v, float3 r0, float3 r1) {
129    float3 r2 = cross(r0, r1);
130    return float3(dot(v, r0), dot(v, r1), dot(v, r2));
131}
132
133// Fast approximate inverse cosine. Max absolute error = 0.009.
134// From https://seblagarde.wordpress.com/2014/12/01/inverse-trigonometric-functions-gpu-optimization-for-amd-gcn-architecture/
135float LV_FastAcos(float x) {
136    float absX = abs(x);
137    float res = -0.156583f * absX + LV_PI * 0.5f;
138    res *= sqrt(1.0f - absX);
139    return (x >= 0) ? res : (LV_PI - res);
140}
141
142// Forms specular based on roughness
143float LV_DistributionGGX(float NoH, float roughness) {
144    float f = (roughness - 1) * ((roughness + 1) * (NoH * NoH)) + 1;
145    return (roughness * roughness) / ((float) LV_PI * f * f);
146}
147
148// Checks if local UVW point is in bounds from -0.5 to +0.5
149bool LV_PointLocalAABB(float3 localUVW) {
150    return all(abs(localUVW) <= 0.5);
151}
152
153// Calculates local UVW using volume ID
154float3 LV_LocalFromVolume(uint volumeID, float3 worldPos) {
155    return mul(_UdonLightVolumeInvWorldMatrix[volumeID], float4(worldPos, 1.0)).xyz;
156}
157
158// Linear single SH L1 channel evaluation
159float LV_EvaluateSH(float L0, float3 L1, float3 n) {
160    return L0 + dot(L1, n);
161}
162
163// Samples a cubemap from _UdonPointLightVolumeTexture array
164float4 LV_SampleCubemapArray(uint id, float3 dir) {
165    float3 absDir = abs(dir);
166    float2 uv;
167    uint face;
168    if (absDir.x >= absDir.y && absDir.x >= absDir.z) {
169        face = dir.x > 0 ? 0 : 1;
170        uv = float2((dir.x > 0 ? -dir.z : dir.z), -dir.y) * rcp(absDir.x);
171    } else if (absDir.y >= absDir.z) {
172        face = dir.y > 0 ? 2 : 3;
173        uv = float2(dir.x, (dir.y > 0 ? dir.z : -dir.z)) * rcp(absDir.y);
174    } else {
175        face = dir.z > 0 ? 4 : 5;
176        uv = float2((dir.z > 0 ? dir.x : -dir.x), -dir.y) * rcp(absDir.z);
177    }
178    float3 uvid = float3(uv * 0.5 + 0.5, id * 6 + face);
179    return LV_SAMPLE(_UdonPointLightVolumeTexture, uvid);
180}
181
182// Projects irradiance from a planar quad with uniform radiant exitance into L1 spherical harmonics.
183// Based on "Analytic Spherical Harmonic Coefficients for Polygonal Area Lights" by Wang and Ramamoorthi.
184// https://cseweb.ucsd.edu/~ravir/ash.pdf. Assumes that shadingPosition is not behind the quad.
185float4 LV_ProjectQuadLightIrradianceSH(float3 shadingPosition, float3 lightVertices[4]) {
186    // Transform the vertices into local space centered on the shading position,
187    // project, the polygon onto the unit sphere.
188    [unroll] for (uint edge0 = 0; edge0 < 4; edge0++) {
189        lightVertices[edge0] = normalize(lightVertices[edge0] - shadingPosition);
190    }
191
192    // Precomputed directions of rotated zonal harmonics,
193    // and associated weights for each basis function.
194    // I.E. \omega_{l,d} and \alpha_{l,d}^m in the paper respectively.
195    const float3 zhDir0 = float3(0.866025, -0.500001, -0.000004);
196    const float3 zhDir1 = float3(-0.759553, 0.438522, -0.480394);
197    const float3 zhDir2 = float3(-0.000002, 0.638694,  0.769461);
198    const float3 zhWeightL1y = float3(2.1995339f, 2.50785367f, 1.56572711f);
199    const float3 zhWeightL1z = float3(-1.82572523f, -2.08165037f, 0.00000000f);
200    const float3 zhWeightL1x = float3(2.42459869f, 1.44790525f, 0.90397552f);
201
202    float solidAngle = 0.0;
203    float3 surfaceIntegral = 0.0;
204    [loop] for (uint edge1 = 0; edge1 < 4; edge1++) {
205        uint next = (edge1 + 1) % 4;
206        uint prev = (edge1 + 4 - 1) % 4;
207        float3 prevVert = lightVertices[prev];
208        float3 thisVert = lightVertices[edge1];
209        float3 nextVert = lightVertices[next];
210
211        // Compute the solid angle subtended by the polygon at the shading position,
212        // using Arvo's formula (5.1) https://dl.acm.org/doi/pdf/10.1145/218380.218467.
213        // The L0 term is directly proportional to the solid angle.
214        float3 a = cross(thisVert, prevVert);
215        float3 b = cross(thisVert, nextVert);
216        float lenA = length(a);
217        float lenB = length(b);
218        solidAngle += LV_FastAcos(clamp(dot(a, b) / (lenA * lenB), -1, 1));
219
220        // Compute the integral of the legendre polynomials over the surface of the
221        // projected polygon for each zonal harmonic direction (S_l in the paper).
222        // Computed as a sum of line integrals over the edges of the polygon.
223        float3 mu = b * rcp(lenB);
224        float cosGamma = dot(thisVert, nextVert);
225        float gamma = LV_FastAcos(clamp(cosGamma, -1, 1));
226        surfaceIntegral.x += gamma * dot(zhDir0, mu);
227        surfaceIntegral.y += gamma * dot(zhDir1, mu);
228        surfaceIntegral.z += gamma * dot(zhDir2, mu);
229    }
230    solidAngle = solidAngle - LV_PI2;
231    surfaceIntegral *= 0.5;
232
233    // The L0 term is just the projection of the solid angle onto the L0 basis function.
234    const float normalizationL0 = 0.5f * sqrt(1.0f / LV_PI);
235    float l0 = normalizationL0 * solidAngle;
236
237    // Combine each surface (sub)integral with the associated weights to get
238    // full surface integral for each L1 SH basis function.
239    float l1y = dot(zhWeightL1y, surfaceIntegral);
240    float l1z = dot(zhWeightL1z, surfaceIntegral);
241    float l1x = dot(zhWeightL1x, surfaceIntegral);
242
243    // The l0, l1y, l1z, l1x are raw SH coefficients for radiance from the polygon.
244    // We need to apply some more transformations before we are done:
245    // (1) We want the coefficients for irradiance, so we need to convolve with the
246    //     clamped cosine kernel, as detailed in https://cseweb.ucsd.edu/~ravir/papers/envmap/envmap.pdf.
247    //     The kernel has coefficients PI and 2/3*PI for L0 and L1 respectively.
248    // (2) Unity's area lights underestimate the irradiance by a factor of PI for historical reasons.
249    //     We need to divide by PI to match this 'incorrect' behavior.
250    // (3) Unity stores SH coefficients (unity_SHAr..unity_SHC) pre-multiplied with the constant
251    //     part of each SH basis function, so we need to multiply by constant part to match it.
252    const float cosineKernelL0 = LV_PI; // (1)
253    const float cosineKernelL1 = LV_PI2 / 3.0f; // (1)
254    const float oneOverPi = 1.0f / LV_PI; // (2)
255    const float normalizationL1 = 0.5f * sqrt(3.0f / LV_PI); // (3)
256    const float weightL0 = cosineKernelL0 * normalizationL0 * oneOverPi; // (1), (2), (3)
257    const float weightL1 = cosineKernelL1 * normalizationL1 * oneOverPi; // (1), (2), (3)
258    l0  *= weightL0;
259    l1y *= weightL1;
260    l1z *= weightL1;
261    l1x *= weightL1;
262
263    return float4(l1x, l1y, l1z, l0);
264}
265
266// Samples a quad light, including culling
267void LV_QuadLight(float3 worldPos, float3 centroidPos, float4 rotationQuat, float2 size, float3 color, float sqMaxDist, float occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
268
269    float3 lightToWorldPos = worldPos - centroidPos;
270
271    // Normal culling
272    float3 normal = LV_MultiplyVectorByQuaternion(float3(0, 0, 1), rotationQuat);
273    [branch] if (dot(normal, lightToWorldPos) < 0.0) return;
274
275    // Attenuate the light based on distance to the bounding sphere, so we don't get hard seam at the edge.
276    float sqCutoffDist = sqMaxDist - dot(lightToWorldPos, lightToWorldPos);
277    color.rgb *= saturate(sqCutoffDist / sqMaxDist) * LV_PI * occlusion;
278
279    // Compute the vertices of the quad
280    float2 halfSize = size * 0.5f;
281    float3 xAxis = LV_MultiplyVectorByQuaternion(float3(1, 0, 0), rotationQuat);
282    float3 yAxis = cross(normal, xAxis);
283    float3 verts[4];
284    verts[0] = centroidPos + (-halfSize.x * xAxis) + ( halfSize.y * yAxis);
285    verts[1] = centroidPos + ( halfSize.x * xAxis) + ( halfSize.y * yAxis);
286    verts[2] = centroidPos + ( halfSize.x * xAxis) + (-halfSize.y * yAxis);
287    verts[3] = centroidPos + (-halfSize.x * xAxis) + (-halfSize.y * yAxis);
288
289    // Project irradiance from the area light
290    float4 areaLightSH = LV_ProjectQuadLightIrradianceSH(worldPos, verts);
291
292    // If the magnitude of L1 is greater than L0, we may get negative values
293    // when reconstructing. To avoid, normalize L1. This is effectively de-ringing.
294    float lenL1 = length(areaLightSH.xyz);
295    if (lenL1 > areaLightSH.w) areaLightSH.xyz *= areaLightSH.w / lenL1;
296
297    L0  += areaLightSH.w * color.rgb;
298    L1r += areaLightSH.xyz * color.r;
299    L1g += areaLightSH.xyz * color.g;
300    L1b += areaLightSH.xyz * color.b;
301
302    count++;
303}
304
305// Calculates point light attenuation. Returns false if it's culled
306float3 LV_PointLightAttenuation(float sqdist, float sqlightSize, float3 color, float brightnessCutoff, float sqMaxDist) {
307    float mask = saturate(1 - sqdist / sqMaxDist);
308    return mask * mask * color * sqlightSize / (sqdist + sqlightSize);
309}
310
311// Calculates point light solid angle coefficient
312float LV_PointLightSolidAngle(float sqdist, float sqlightSize) {
313    return saturate(sqrt(sqdist / (sqlightSize + sqdist)));
314}
315
316// Calculares a spherical light source
317void LV_SphereLight(float sqdist, float3 dirN, float sqlightSize, float3 color, float occlusion, float sqMaxDist, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
318
319    float3 att = LV_PointLightAttenuation(sqdist, sqlightSize, color, _UdonLightBrightnessCutoff, sqMaxDist);
320
321    float3 l0 = att * occlusion;
322    float3 l1 = dirN * LV_PointLightSolidAngle(sqdist, sqlightSize);
323
324    L0 += l0;
325    L1r += l0.r * l1;
326    L1g += l0.g * l1;
327    L1b += l0.b * l1;
328    count++;
329
330}
331
332// Calculares a spherical spot light source
333void LV_SphereSpotLight(float sqdist, float3 dirN, float sqlightSize, float3 att, float spotMask, float cosAngle, float coneFalloff, float occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
334
335    float smoothedCone = LV_Smoothstep01(saturate(spotMask * coneFalloff));
336    float3 l0 = att * (occlusion * smoothedCone);
337    float3 l1 = dirN * LV_PointLightSolidAngle(sqdist, sqlightSize * saturate(1 - cosAngle));
338
339    L0 += l0;
340    L1r += l0.r * l1;
341    L1g += l0.g * l1;
342    L1b += l0.b * l1;
343    count++;
344
345}
346
347// Calculares a spherical spot light source
348void LV_SphereSpotLightCookie(float sqdist, float3 dirN, float sqlightSize, float3 att, float4 lightRot, float tanAngle, uint customId, float occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
349
350    float3 localDir = LV_MultiplyVectorByQuaternion(-dirN, lightRot);
351    float2 uv = localDir.xy * rcp(localDir.z * tanAngle);
352    [branch] if (
353        localDir.z <= 0.0 || // Culling by direction
354        abs(uv.x) > 1.0 || abs(uv.y) > 1.0 // Culling by UV
355    ) return;
356
357    uint id = (uint) _UdonPointLightVolumeCubeCount * 5 - customId - 1;
358    float3 uvid = float3(uv * 0.5 + 0.5, id);
359    float angleSize = saturate(rsqrt(1 + tanAngle * tanAngle));
360    float4 cookie = LV_SAMPLE(_UdonPointLightVolumeTexture, uvid);
361
362    float3 l0 = att * cookie.rgb * (cookie.a * occlusion);
363    float3 l1 = dirN * LV_PointLightSolidAngle(sqdist, sqlightSize * (1 - angleSize));
364
365    L0 += l0;
366    L1r += l0.r * l1;
367    L1g += l0.g * l1;
368    L1b += l0.b * l1;
369    count++;
370
371}
372
373// Calculares a spherical spot light source
374void LV_SphereSpotLightAttenuationLUT(float sqdist, float3 dirN, float sqlightSize, float3 color, float spotMask, float cosAngle, uint customId, float occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
375
376    float dirRadius = sqdist * abs(sqlightSize);
377    float spot = 1 - saturate(spotMask * rcp(1 - cosAngle));
378    uint id = (uint) _UdonPointLightVolumeCubeCount * 5 + customId - 1;
379    float3 uvid = float3(sqrt(float2(spot, dirRadius)), id);
380    float3 att = color.rgb * LV_SAMPLE(_UdonPointLightVolumeTexture, uvid).xyz * occlusion;
381
382    L0 += att;
383    L1r += dirN * att.r;
384    L1g += dirN * att.g;
385    L1b += dirN * att.b;
386
387    count++;
388
389}
390
391// Samples a spot light, point light or quad/area light
392void LV_PointLight(uint id, float3 worldPos, float4 occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, inout uint count) {
393
394    // IDs and range data
395    float3 customID_data = _UdonPointLightVolumeCustomID[id];
396    int shadowId = (int) customID_data.y; // Shadowmask id
397    int customId = (int) customID_data.x; // Custom Texture ID
398    float sqrRange = customID_data.z; // Squared culling distance
399
400    float4 pos = _UdonPointLightVolumePosition[id]; // Light position and inversed squared range
401    float3 dir = pos.xyz - worldPos;
402    float sqlen = max(dot(dir, dir), 1e-6);
403    [branch] if (sqlen > sqrRange) return; // Early distance based culling
404    float3 dirN = dir * rsqrt(sqlen);
405
406    // Processing lights occlusion
407    float lightOcclusion = 1;
408    if (_UdonLightVolumeOcclusionCount != 0 && shadowId >= 0) {
409        lightOcclusion = dot(occlusion, float4(shadowId == 0, shadowId == 1, shadowId == 2, shadowId == 3));
410    }
411
412    float4 color = _UdonPointLightVolumeColor[id]; // Color, angle
413    float4 ldir = _UdonPointLightVolumeDirection[id]; // Dir + falloff or Rotation
414
415    [branch] if (pos.w < 0) { // It is a spot light
416
417        float angle = color.w;
418        float spotMask = dot(ldir, -dirN) - angle;
419        [branch] if(customId >= 0 && spotMask < 0) return; // Spot cone based culling
420
421        [branch] if (customId > 0) {  // If it uses Attenuation LUT
422
423            LV_SphereSpotLightAttenuationLUT(sqlen, dirN, -pos.w, color.rgb, spotMask, angle, customId, lightOcclusion, L0, L1r, L1g, L1b, count);
424
425        } else { // If it uses default parametric attenuation
426
427            float3 att = LV_PointLightAttenuation(sqlen, -pos.w, color, _UdonLightBrightnessCutoff, sqrRange);
428
429            [branch] if (customId < 0) { // If uses cookie
430
431                LV_SphereSpotLightCookie(sqlen, dirN, -pos.w, att, ldir, angle, customId, lightOcclusion, L0, L1r, L1g, L1b, count);
432
433            } else { // If it uses default parametric attenuation
434
435                LV_SphereSpotLight(sqlen, dirN, -pos.w, att, spotMask, angle, ldir.w, lightOcclusion, L0, L1r, L1g, L1b, count);
436
437            }
438
439        }
440
441    } else if (color.w <= 1.5f) { // It is a point light
442
443        [branch] if (customId > 0) { // Using LUT
444
445            float invSqRange = abs(pos.w); // Sign of range defines if it's point light (positive) or a spot light (negative)
446            float dirRadius = sqlen * invSqRange;
447            uint id = (uint) _UdonPointLightVolumeCubeCount * 5 + customId;
448            float3 uvid = float3(sqrt(float2(0, dirRadius)), id);
449            float3 att = color.rgb * LV_SAMPLE(_UdonPointLightVolumeTexture, uvid).xyz * lightOcclusion;
450
451            L0 += att;
452            L1r += dirN * att.r;
453            L1g += dirN * att.g;
454            L1b += dirN * att.b;
455
456            count++;
457
458        } else { // If it uses default parametric attenuation
459
460            float3 l0 = 0, l1r = 0, l1g = 0, l1b = 0;
461            LV_SphereLight(sqlen, dirN, pos.w, color.rgb, lightOcclusion, sqrRange, l0, l1r, l1g, l1b, count);
462
463            float3 cubeColor = 1;
464            [branch] if (customId < 0) { // If it uses a cubemap
465                uint id = -customId - 1; // Cubemap ID starts from zero and should not take in count texture array slices count.
466                cubeColor = LV_SampleCubemapArray(id, LV_MultiplyVectorByQuaternion(dirN, ldir)).xyz;
467            }
468
469            L0 += l0 * cubeColor;
470            L1r += l1r * cubeColor.r;
471            L1g += l1g * cubeColor.g;
472            L1b += l1b * cubeColor.b;
473        }
474
475    } else { // It is an area light
476
477        LV_QuadLight(worldPos, pos.xyz, ldir, float2(pos.w, color.w - 2.0f), color.rgb, sqrRange, lightOcclusion, L0, L1r, L1g, L1b, count);
478
479    }
480
481}
482
483// Samples 3 SH textures and packing them into L1 channels
484void LV_SampleLightVolumeTex(float3 uvw0, float3 uvw1, float3 uvw2, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b) {
485    // Sampling 3D Atlas
486    float4 tex0 = LV_SAMPLE(_UdonLightVolume, uvw0);
487    float4 tex1 = LV_SAMPLE(_UdonLightVolume, uvw1);
488    float4 tex2 = LV_SAMPLE(_UdonLightVolume, uvw2);
489    // Packing final data
490    L0 = tex0.rgb;
491    L1r = float3(tex1.r, tex2.r, tex0.a);
492    L1g = float3(tex1.g, tex2.g, tex1.a);
493    L1b = float3(tex1.b, tex2.b, tex2.a);
494}
495
496// Bounds mask for a volume rotated in world space, using local UVW
497float LV_BoundsMask(float3 localUVW, float3 invLocalEdgeSmooth) {
498    float3 distToMin = (localUVW + 0.5) * invLocalEdgeSmooth;
499    float3 distToMax = (0.5 - localUVW) * invLocalEdgeSmooth;
500    float3 fade = saturate(min(distToMin, distToMax));
501    return fade.x * fade.y * fade.z;
502}
503
504// Default light probes SH components
505void LV_SampleLightProbe(inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b) {
506    L0 += float3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
507    L1r += unity_SHAr.xyz;
508    L1g += unity_SHAg.xyz;
509    L1b += unity_SHAb.xyz;
510}
511
512// Applies deringing to light probes. Useful if they baked with Bakery L1
513void LV_SampleLightProbeDering(inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b) {
514    L0 += float3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
515    L1r += unity_SHAr.xyz * 0.565f;
516    L1g += unity_SHAg.xyz * 0.565f;
517    L1b += unity_SHAb.xyz * 0.565f;
518}
519
520// Samples a Volume with ID and Local UVW
521void LV_SampleVolume(uint id, float3 localUVW, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, out float4 occlusion) {
522
523    // Additive UVW
524    //uint uvwID = id * 3;
525    //float4 uvwPos0 = _UdonLightVolumeUvwScale[uvwID];
526    //float4 uvwPos1 = _UdonLightVolumeUvwScale[uvwID + 1];
527    //float4 uvwPos2 = _UdonLightVolumeUvwScale[uvwID + 2];
528    //float3 uvwScale = float3(uvwPos0.w, uvwPos1.w, uvwPos2.w);
529
530    //float3 uvwScaled = saturate(localUVW + 0.5) * uvwScale;
531    //float3 uvw0 = uvwPos0.xyz + uvwScaled;
532    //float3 uvw1 = uvwPos1.xyz + uvwScaled;
533    //float3 uvw2 = uvwPos2.xyz + uvwScaled;
534
535    // Legacy! Commented code above will be used in future releases! Legacy!
536    uint uvwID = id * 6;
537    float3 uvwScaled = saturate(localUVW + 0.5) * (_UdonLightVolumeUvw[uvwID + 1].xyz - _UdonLightVolumeUvw[uvwID].xyz);
538    float3 uvw0 = uvwScaled + _UdonLightVolumeUvw[uvwID].xyz;
539    float3 uvw1 = uvwScaled + _UdonLightVolumeUvw[uvwID + 2].xyz;
540    float3 uvw2 = uvwScaled + _UdonLightVolumeUvw[uvwID + 4].xyz;
541
542    // Sample additive
543    float3 l0, l1r, l1g, l1b;
544    LV_SampleLightVolumeTex(uvw0, uvw1, uvw2, l0, l1r, l1g, l1b);
545
546    // Sample occlusion
547    float4 uvwOcclusion = _UdonLightVolumeOcclusionUvw[id];
548    [branch] if (uvwOcclusion.x >= 0) {
549        occlusion = 1.0f - LV_SAMPLE(_UdonLightVolume, uvwOcclusion.xyz + uvwScaled * uvwOcclusion.w);
550    } else {
551        occlusion = 1;
552    }
553
554    // Color correction
555    float4 color = _UdonLightVolumeColor[id];
556    L0 += l0 * color.rgb;
557    l1r *= color.r;
558    l1g *= color.g;
559    l1b *= color.b;
560
561    // Rotate if needed
562    if (color.a != 0) {
563        //float4 r = _UdonLightVolumeRotationQuaternion[id];
564        //L1r = LV_MultiplyVectorByQuaternion(L1r, r);
565        //L1g = LV_MultiplyVectorByQuaternion(L1g, r);
566        //L1b = LV_MultiplyVectorByQuaternion(L1b, r);
567
568        // Legacy to support older light volumes worlds! Commented code above will be used in future releases! Legacy!
569        float3 r0 = _UdonLightVolumeRotation[id * 2].xyz;
570        float3 r1 = _UdonLightVolumeRotation[id * 2 + 1].xyz;
571        L1r += LV_MultiplyVectorByMatrix2x3(l1r, r0, r1);
572        L1g += LV_MultiplyVectorByMatrix2x3(l1g, r0, r1);
573        L1b += LV_MultiplyVectorByMatrix2x3(l1b, r0, r1);
574    } else {
575        L1r += l1r;
576        L1g += l1g;
577        L1b += l1b;
578    }
579
580}
581
582float4 LV_SampleVolumeOcclusion(uint id, float3 localUVW) {
583
584    // Sample occlusion
585    float4 uvwOcclusion = _UdonLightVolumeOcclusionUvw[id];
586
587    [branch] if (uvwOcclusion.x >= 0) {
588        //uint uvwID = id * 3;
589        //float4 uvwPos0 = _UdonLightVolumeUvwScale[uvwID];
590        //float4 uvwPos1 = _UdonLightVolumeUvwScale[uvwID + 1];
591        //float4 uvwPos2 = _UdonLightVolumeUvwScale[uvwID + 2];
592        //float3 uvwScale = float3(uvwPos0.w, uvwPos1.w, uvwPos2.w);
593        //float3 uvwScaled = saturate(localUVW + 0.5) * uvwScale;
594
595        // Legacy to support older light volumes worlds! Commented code above will be used in future releases! Legacy!
596        uint uvwID = id * 6;
597        float3 uvwScaled = saturate(localUVW + 0.5) * (_UdonLightVolumeUvw[uvwID + 1].xyz - _UdonLightVolumeUvw[uvwID].xyz);
598
599        return 1.0f - LV_SAMPLE(_UdonLightVolume, uvwOcclusion.xyz + uvwScaled * uvwOcclusion.w);
600    } else {
601        return 1;
602    }
603
604}
605
606// Calculates L1 SH based on the world position and occlusion factor. Only samples point lights, not light volumes.
607void LV_PointLightVolumeSH(float3 worldPos, float4 occlusion, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b) {
608
609    uint pointCount = min((uint) _UdonPointLightVolumeCount, VRCLV_MAX_LIGHTS_COUNT);
610    [branch] if (pointCount == 0) return;
611
612    uint maxOverdraw = min((uint) _UdonLightVolumeAdditiveMaxOverdraw, VRCLV_MAX_LIGHTS_COUNT);
613    uint pcount = 0; // Point lights counter
614
615    [loop] for (uint pid = 0; pid < pointCount && pcount < maxOverdraw; pid++) {
616        LV_PointLight(pid, worldPos, occlusion, L0, L1r, L1g, L1b, pcount);
617    }
618
619}
620
621// Calculates L1 SH and occlusion based on the world position. Only samples light volumes, not point lights.
622void LV_LightVolumeSH(float3 worldPos, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, out float4 occlusion) {
623
624    // Initializing output variables
625    occlusion = 1;
626    float4 mOcclusion = 1; // Multiplicative occlusion. Applies on top of regular occlusion
627
628    // Clamping gloabal iteration counts
629    uint volumesCount = min((uint) _UdonLightVolumeCount, VRCLV_MAX_VOLUMES_COUNT);
630
631    //if (_UdonLightVolumeVersion < VRCLV_VERSION || volumesCount == 0 ) { // Fallback to default light probes if Light Volume are not enabled or a version is too old to have a support
632    [branch] if (volumesCount == 0) { // Legacy! Fallback to default light probes if Light Volume are not enabled or a version is too old to have a support. Legacy!
633        LV_SampleLightProbe(L0, L1r, L1g, L1b);
634        return;
635    }
636
637    uint maxOverdraw = min((uint) _UdonLightVolumeAdditiveMaxOverdraw, VRCLV_MAX_VOLUMES_COUNT);
638    uint additiveCount = min((uint) _UdonLightVolumeAdditiveCount, VRCLV_MAX_VOLUMES_COUNT);
639    bool lightProbesBlend = _UdonLightVolumeProbesBlend;
640
641    uint volumeID_A = -1; // Main, dominant volume ID
642    uint volumeID_B = -1; // Secondary volume ID to blend main with
643
644    float3 localUVW   = 0; // Last local UVW to use in disabled Light Probes mode
645    float3 localUVW_A = 0; // Main local UVW
646    float3 localUVW_B = 0; // Secondary local UVW
647
648    // Are A and B volumes NOT found?
649    bool isNoA = true;
650    bool isNoB = true;
651
652    // Additive volumes variables
653    uint addVolumesCount = 0;
654
655    // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
656    [loop] for (uint id = 0; id < volumesCount; id++) {
657        localUVW = LV_LocalFromVolume(id, worldPos);
658        [branch] if (LV_PointLocalAABB(localUVW)) { // Intersection test
659            [branch] if (id < additiveCount) { // Sampling additive volumes
660                [branch] if (addVolumesCount < maxOverdraw) {
661                    float4 occ; // Multiplicative occlusion
662                    LV_SampleVolume(id, localUVW, L0, L1r, L1g, L1b, occ);
663                    mOcclusion *= occ;
664                    addVolumesCount++;
665                }
666            } else if (isNoA) { // First, searching for volume A
667                volumeID_A = id;
668                localUVW_A = localUVW;
669                isNoA = false;
670            } else { // Next, searching for volume B if A found
671                volumeID_B = id;
672                localUVW_B = localUVW;
673                isNoB = false;
674                break;
675            }
676        }
677    }
678
679    // If no volumes found, using Light Probes as fallback
680    [branch] if (isNoA && lightProbesBlend) {
681        LV_SampleLightProbe(L0, L1r, L1g, L1b);
682        occlusion *= mOcclusion;
683        return;
684    }
685
686    // Fallback to lowest weight light volume if outside of every volume
687    localUVW_A = isNoA ? localUVW : localUVW_A;
688    volumeID_A = isNoA ? volumesCount - 1 : volumeID_A;
689
690    // Volume A SH components, occlusion, and mask to blend volume sides
691    float3 L0_A  = 0;
692    float3 L1r_A = 0;
693    float3 L1g_A = 0;
694    float3 L1b_A = 0;
695    float4 occlusion_A = 1;
696
697    // Sampling Light Volume A
698    LV_SampleVolume(volumeID_A, localUVW_A, L0_A, L1r_A, L1g_A, L1b_A, occlusion_A);
699
700    float mask = LV_BoundsMask(localUVW_A, _UdonLightVolumeInvLocalEdgeSmooth[volumeID_A]);
701    [branch] if (mask == 1 || isNoA || (_UdonLightVolumeSharpBounds && isNoB)) { // Returning SH A result if it's the center of mask or out of bounds
702        L0  += L0_A;
703        L1r += L1r_A;
704        L1g += L1g_A;
705        L1b += L1b_A;
706        occlusion = occlusion_A;
707        occlusion *= mOcclusion;
708        return;
709    }
710
711    // Volume B SH components and occlusion
712    float3 L0_B  = 0;
713    float3 L1r_B = 0;
714    float3 L1g_B = 0;
715    float3 L1b_B = 0;
716    float4 occlusion_B = 1;
717
718    [branch] if (isNoB && lightProbesBlend) { // No Volume found and light volumes blending enabled
719
720        // Sample Light Probes B
721        LV_SampleLightProbe(L0_B, L1r_B, L1g_B, L1b_B);
722
723    } else { // Blending Volume A and Volume B
724
725        // If no volume b found, use last one found to fallback
726        localUVW_B = isNoB ? localUVW : localUVW_B;
727        volumeID_B = isNoB ? volumesCount - 1 : volumeID_B;
728
729        // Sampling Light Volume B
730        LV_SampleVolume(volumeID_B, localUVW_B, L0_B, L1r_B, L1g_B, L1b_B, occlusion_B);
731
732    }
733
734    // Lerping occlusion
735    occlusion = lerp(occlusion_B, occlusion_A, mask);
736    occlusion *= mOcclusion;
737
738    // Lerping SH components
739    L0  += lerp(L0_B,  L0_A,  mask);
740    L1r += lerp(L1r_B, L1r_A, mask);
741    L1g += lerp(L1g_B, L1g_A, mask);
742    L1b += lerp(L1b_B, L1b_A, mask);
743
744}
745
746// Calculates L1 SH based on the world position from additive volumes only. Only samples light volumes, not point lights.
747// Also returns an occlusion factor, which may be used for point light shadows.
748void LV_LightVolumeAdditiveSH(float3 worldPos, inout float3 L0, inout float3 L1r, inout float3 L1g, inout float3 L1b, out float4 occlusion) {
749
750    // Initializing output variables
751    occlusion = 1;
752    float4 mOcclusion = 1; // Multiplicative occlusion. Applies on top of regular occlusion
753
754    // Clamping gloabal iteration counts
755    uint additiveCount = min((uint) _UdonLightVolumeAdditiveCount, VRCLV_MAX_VOLUMES_COUNT);
756    //if (_UdonLightVolumeVersion < VRCLV_VERSION || (additiveCount == 0 && pointCount == 0)) return;
757    [branch] if (additiveCount == 0 && (uint) _UdonPointLightVolumeCount == 0) return; // Legacy!
758
759    uint volumesCount = min((uint) _UdonLightVolumeCount, VRCLV_MAX_VOLUMES_COUNT);
760    uint maxOverdraw = min((uint) _UdonLightVolumeAdditiveMaxOverdraw, VRCLV_MAX_VOLUMES_COUNT);
761
762    uint volumeID_A = -1; // Main, dominant volume ID
763    uint volumeID_B = -1; // Secondary volume ID to blend main with
764
765    float3 localUVW   = 0; // Last local UVW to use in disabled Light Probes mode
766    float3 localUVW_A = 0; // Main local UVW for Y Axis and Free rotations
767    float3 localUVW_B = 0; // Secondary local UVW
768
769    // Are A and B volumes NOT found?
770    bool isNoA = true;
771    bool isNoB = true;
772
773    // Additive volumes variables
774    uint addVolumesCount = 0;
775
776    // Iterating through all light volumes with simplified algorithm requiring Light Volumes to be sorted by weight in descending order
777    uint count = min(_UdonLightVolumeOcclusionCount == 0 ? additiveCount : volumesCount, VRCLV_MAX_VOLUMES_COUNT); // Only use all volumes if occlusion volumes are enabled
778    [loop] for (uint id = 0; id < count; id++) {
779        localUVW = LV_LocalFromVolume(id, worldPos);
780        [branch] if (LV_PointLocalAABB(localUVW)) { // Intersection test
781            [branch] if (id < additiveCount) { // Sampling additive volumes
782                [branch] if (addVolumesCount < maxOverdraw) {
783                    float4 occ; // Multiplicative occlusion
784                    LV_SampleVolume(id, localUVW, L0, L1r, L1g, L1b, occ);
785                    mOcclusion *= occ;
786                    addVolumesCount++;
787                }
788            } else if (isNoA) { // First, searching for volume A
789                volumeID_A = id;
790                localUVW_A = localUVW;
791                isNoA = false;
792            } else { // Next, searching for volume B if A found
793                volumeID_B = id;
794                localUVW_B = localUVW;
795                isNoB = false;
796                break;
797            }
798        }
799    }
800
801    // If no volumes found, or we don't need the occlusion data, we are done
802    [branch] if (isNoA || _UdonLightVolumeOcclusionCount == 0) {
803        occlusion *= mOcclusion;
804        return;
805    }
806
807    // Fallback to lowest weight light volume if outside of every volume
808    localUVW_A = isNoA ? localUVW : localUVW_A;
809    volumeID_A = isNoA ? volumesCount - 1 : volumeID_A;
810
811    // Sampling Light Volume A
812    occlusion = LV_SampleVolumeOcclusion(volumeID_A, localUVW_A);
813    float mask = LV_BoundsMask(localUVW_A, _UdonLightVolumeInvLocalEdgeSmooth[volumeID_A]);
814
815    [branch] if (mask == 1 || (_UdonLightVolumeSharpBounds && isNoB)) {
816        occlusion *= mOcclusion;
817        return; // Returning A result if it's the center of mask or out of bounds
818    }
819
820    // Blending Volume A and Volume B
821    [branch] if (isNoB) occlusion = lerp(1, occlusion, mask);
822    else occlusion = lerp(LV_SampleVolumeOcclusion(volumeID_B, localUVW_B), occlusion, mask);
823
824    occlusion *= mOcclusion;
825
826}
827
828// Calculates speculars for light volumes or any SH L1 data with privided f0
829float3 LightVolumeSpecular(float3 f0, float smoothness, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
830
831    float3 specColor = max(float3(dot(reflect(-L1r, worldNormal), viewDir), dot(reflect(-L1g, worldNormal), viewDir), dot(reflect(-L1b, worldNormal), viewDir)), 0);
832
833    float3 rDir = normalize(normalize(L1r) + viewDir);
834    float3 gDir = normalize(normalize(L1g) + viewDir);
835    float3 bDir = normalize(normalize(L1b) + viewDir);
836
837    float rNh = saturate(dot(worldNormal, rDir));
838    float gNh = saturate(dot(worldNormal, gDir));
839    float bNh = saturate(dot(worldNormal, bDir));
840
841    float roughness = 1 - smoothness * 0.9f;
842    float roughExp = roughness * roughness;
843
844    float rSpec = LV_DistributionGGX(rNh, roughExp);
845    float gSpec = LV_DistributionGGX(gNh, roughExp);
846    float bSpec = LV_DistributionGGX(bNh, roughExp);
847
848    float3 specs = (rSpec + gSpec + bSpec) * f0;
849    float3 coloredSpecs = specs * specColor;
850
851    float3 a = coloredSpecs + specs * L0;
852    float3 b = coloredSpecs * 3;
853
854    return max(lerp(a, b, smoothness) * 0.5f, 0.0);
855
856}
857
858// Calculates speculars for light volumes or any SH L1 data
859float3 LightVolumeSpecular(float3 albedo, float smoothness, float metallic, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
860    float3 specularf0 = lerp(0.04f, albedo, metallic);
861    return LightVolumeSpecular(specularf0, smoothness, worldNormal, viewDir, L0, L1r, L1g, L1b);
862}
863
864// Calculates speculars for light volumes or any SH L1 data, but simplified, with only one dominant direction with provided f0
865float3 LightVolumeSpecularDominant(float3 f0, float smoothness, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
866
867    float3 dominantDir = L1r + L1g + L1b;
868    float3 dir = normalize(normalize(dominantDir) + viewDir);
869    float nh = saturate(dot(worldNormal, dir));
870
871    float roughness = 1 - smoothness * 0.9f;
872    float roughExp = roughness * roughness;
873
874    float spec = LV_DistributionGGX(nh, roughExp);
875
876    return max(spec * L0 * f0, 0.0) * 1.5f;
877
878}
879
880// Calculates speculars for light volumes or any SH L1 data, but simplified, with only one dominant direction
881float3 LightVolumeSpecularDominant(float3 albedo, float smoothness, float metallic, float3 worldNormal, float3 viewDir, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
882    float3 specularf0 = lerp(0.04f, albedo, metallic);
883    return LightVolumeSpecularDominant(specularf0, smoothness, worldNormal, viewDir, L0, L1r, L1g, L1b);
884}
885
886// Calculate Light Volume Color based on all SH components provided and the world normal
887float3 LightVolumeEvaluate(float3 worldNormal, float3 L0, float3 L1r, float3 L1g, float3 L1b) {
888    return float3(LV_EvaluateSH(L0.r, L1r, worldNormal), LV_EvaluateSH(L0.g, L1g, worldNormal), LV_EvaluateSH(L0.b, L1b, worldNormal));
889}
890
891// Calculates L1 SH based on the world position. Samples both light volumes and point lights.
892void LightVolumeSH(float3 worldPos, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b, float3 worldPosOffset = 0) {
893    L0 = 0; L1r = 0; L1g = 0; L1b = 0;
894    if (_UdonLightVolumeEnabled == 0) {
895        LV_SampleLightProbeDering(L0, L1r, L1g, L1b);
896    } else {
897        float4 occlusion = 1;
898        LV_LightVolumeSH(worldPos + worldPosOffset, L0, L1r, L1g, L1b, occlusion);
899        LV_PointLightVolumeSH(worldPos, occlusion, L0, L1r, L1g, L1b);
900    }
901}
902
903// Calculates L1 SH based on the world position from additive volumes only. Samples both light volumes and point lights.
904void LightVolumeAdditiveSH(float3 worldPos, out float3 L0, out float3 L1r, out float3 L1g, out float3 L1b, float3 worldPosOffset = 0) {
905    L0 = 0; L1r = 0; L1g = 0; L1b = 0;
906    if (_UdonLightVolumeEnabled != 0) {
907        float4 occlusion = 1;
908        LV_LightVolumeAdditiveSH(worldPos + worldPosOffset, L0, L1r, L1g, L1b, occlusion);
909        LV_PointLightVolumeSH(worldPos, occlusion, L0, L1r, L1g, L1b);
910    }
911}
912
913// Calculates L0 SH based on the world position. Samples both light volumes and point lights.
914float3 LightVolumeSH_L0(float3 worldPos, float3 worldPosOffset = 0) {
915    if (_UdonLightVolumeEnabled == 0) {
916        return float3(unity_SHAr.w, unity_SHAg.w, unity_SHAb.w);
917    } else {
918        float3 L0 = 0; float4 occlusion = 1;
919        float3 unused_L1; // Let's just pray that compiler will strip everything x.x
920        LV_LightVolumeSH(worldPos + worldPosOffset, L0, unused_L1, unused_L1, unused_L1, occlusion);
921        LV_PointLightVolumeSH(worldPos, occlusion, L0, unused_L1, unused_L1, unused_L1);
922        return L0;
923    }
924}
925
926// Calculates L0 SH based on the world position from additive volumes only. Samples both light volumes and point lights.
927float3 LightVolumeAdditiveSH_L0(float3 worldPos, float3 worldPosOffset = 0) {
928    if (_UdonLightVolumeEnabled == 0) {
929        return 0;
930    } else {
931        float3 L0 = 0; float4 occlusion = 1;
932        float3 unused_L1; // Let's just pray that compiler will strip everything x.x
933        LV_LightVolumeAdditiveSH(worldPos + worldPosOffset, L0, unused_L1, unused_L1, unused_L1, occlusion);
934        LV_PointLightVolumeSH(worldPos, occlusion, L0, unused_L1, unused_L1, unused_L1);
935        return L0;
936    }
937}
938
939// Checks if Light Volumes are used in this scene. Returns 0 if not, returns 1 if enabled
940float LightVolumesEnabled() {
941    return _UdonLightVolumeEnabled;
942}
943
944// Returns the light volumes version
945float LightVolumesVersion() {
946    return _UdonLightVolumeVersion == 0 ? _UdonLightVolumeEnabled : _UdonLightVolumeVersion;
947}
948
949#endif
950