yum-archive/2ner
A toon shader for Unity's BIRP.
git clone https://git.yummers.dev/yum-archive/2ner
0e53f4f
master
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