#version 450
/*------------------------------------------------------------------------------------------------
VolumetricHologram | VERTEX_SHADER
--------------------------------------------------------------------------------------------------
Unlit Shader meant to create rich-detailed, and highly optimized holograms meant to look 
like made of thousands of particles. Fixed-function Animations to take advantage of the
many "Quads" that compose the Mesh
/* ---------------------------------------------------------------------------------------------*/

//------------------------------------------------------------------------------------------------
// Input and Output
//------------------------------------------------

// IN 
layout(location = 0) in vec3 pos; // Vertex Pos
layout(location = 1) in vec4 color; // RGB = Quad's Local Pos, A = Quad's Id
layout(location = 2) in vec3 normal; // Quad Normal
layout(location = 3) in vec4 uv0; // XY = TexCoord, Z = Quad Depth, W = Burst Distance
layout(location = 4) in vec4 uv1; // XYZ = Burst Point, W = Burst Id

// OUT
layout(location = 0) out vec4 fragColor;
layout(location = 1) out vec3 fragWorldPos;
layout(location = 2) out vec3 fragWorldNormal;
layout(location = 3) out vec4 fragData;

//------------------------------------------------------------------------------------------------
// Custom Shader Utility and Helper Files
//------------------------------------------------

#include "UtilityCommon.cshf"
#define GLOBAL_UNIFORM_BINDING 0
#include "UniformCommon.cshf"
#include "PushCommon.cshf"
#define CUSTOM_UNIFORM_BINDING 2
#include "CustomParamsCommon.cshf"

//------------------------------------------------------------------------------------------------
// Texture Samplers
//------------------------------------------------

layout(binding = 3) uniform sampler2D HeightMapSampler;

//------------------------------------------------------------------------------------------------
// Parameters
//------------------------------------------------

// From Custom Params Uniform Buffer
#define uc_enableStrobe			custBuffer.paramsA.z
#define uc_enableViewCulling	custBuffer.paramsA.w
#define uc_viewCullThreshold	custBuffer.paramsB.x
#define uc_tileScaleMax			custBuffer.paramsB.y
#define uc_camDistInfluence		custBuffer.paramsB.z
#define uc_camDistExpo			custBuffer.paramsB.w
#define uc_enableCamDistScaling custBuffer.paramsC.x
#define uc_tileScaleMin			custBuffer.paramsC.y
#define uc_enableBillboarding	custBuffer.paramsC.z
#define uc_billboardPerQuad		custBuffer.paramsC.w
#define uc_oscillationExtents	custBuffer.paramsD.y
#define uc_undulationExtents	custBuffer.paramsD.w
#define uc_tornadoSlider		custBuffer.paramsE.x
#define uc_explodeSlider		custBuffer.paramsE.y
#define uc_fringeExpo			custBuffer.paramsE.w
#define uc_undulateRate			custBuffer.paramsF.x
#define uc_undulateOffsetScale	custBuffer.paramsF.y
#define uc_burstSlider			custBuffer.paramsF.z
#define uc_gridifySlider		custBuffer.paramsF.w
#define uc_strobeGradient		custBuffer.paramsG.x
#define uc_strobeSlice			custBuffer.paramsG.y
#define uc_animateIntensity		custBuffer.paramsG.z
#define uc_enableLighting		custBuffer.paramsE.z

// Constants that ideally should be passed-in to the shader
#define uc_burstCount			100
#define uc_horizontalLight		-1.0f
#define uc_verticalLight		1.0f
#define uc_decompressScalar 	10.0f

#define BURST_DIST_ENCODED

//------------------------------------------------------------------------------------------------
// HELPER FUNCTIONS
//------------------------------------------------

// 0 to 1 interpolation between start, end, and two control points
vec3 su_calc_cubic_bezier_curve(vec3 start, vec3 C0, vec3 C1, vec3 end, float blend) 
{
    const float invBlend = 1.0f - blend;

    // Compute weighted contributions of each point along the curve
    const vec3 finalStart  = (invBlend * invBlend * invBlend) * start;
    const vec3 finalC0 = 3.0f * (invBlend * invBlend * blend) * C0;
    const vec3 finalC1 = 3.0f * (invBlend * blend * blend) * C1;
    const vec3 finalEnd  = (blend * blend * blend) * end;

	// Sum all contributions for final interpolated pos
	const vec3 iterpolatedPos = finalStart + finalC0 + finalC1 + finalEnd;
    return iterpolatedPos;

} // su_calc_cubic_bezier_curve()

//------------------------------------------------

// 0-1 interpolation between start, end, and midpoint
vec3 su_calc_bezier_curve(vec3 start, vec3 mid, vec3 end, float blend) 
{
    const float invBlend = 1.0f - blend;

    const vec3 finalStart = invBlend * invBlend * start; 
    const vec3 finalMid = 2 * invBlend * blend * mid;
    const vec3 finalEnd = blend * blend * end;

	// Sum of the three points
	const vec3 interpolatedPos = finalStart + finalMid + finalEnd;
    return interpolatedPos;

} // su_calc_bezier_curve()

//------------------------------------------------

// Three-Way Lerp
float su_lerp_chain(float a, float b, float c, float blend) 
{
    const float ab = mix(a, b, blend);
    const float bc = mix(b, c, blend);
    return mix(ab, bc, blend);

} // su_lerp_chain()

//------------------------------------------------

// Four-Way Lerp
float su_lerp_chain(float a, float b, float c, float d, float blend) 
{
    const float ab = mix(a, b, blend);
    const float bc = mix(b, c, blend);
    const float cd = mix(c, d, blend);
    
    const float abc = mix(ab, bc, blend);
    const float bcd = mix(bc, cd, blend);
    
    return mix(abc, bcd, blend);

} // su_lerp_chain()

//------------------------------------------------

// For giving the Quad a convincing decay "bounce" at end of explosion
float su_bounce(float freq, float dec, float blend) 
{
	const float frequency = mix(1.0f, 6.0f, freq);  // Higher value = more rapid bounces
    const float decay = mix(1.1f, 6.1f, dec);       // Lower value = longer-lasting bounces
    const float intensity = 0.5f;   				// Increase amplitude of the bounce

	// Oscillation with decay
	const float decayFactor = exp(-blend * decay);
    const float bounceFactor = sin(blend * frequency * kTwoPi) * decayFactor; 
	const float finalOffset = abs(bounceFactor) * intensity;

	return finalOffset;

} // su_bounce()

//------------------------------------------------

// A variant of bouncing with simular results as above for explosions
float su_lerp_bounce(float a, float b, float blend) 
{
	const float frequency = 10.0f;  // Higher value = more rapid bounces
    const float decay = 3.0f;       // Lower value = longer-lasting bounces
    const float intensity = 1.0f;   // Increase amplitude of the bounce

    const float bounce = sin(blend * frequency * kTwoPi) * exp(-blend * decay); // Oscillation with decay
	const float result = mix(a, b, blend) + bounce * intensity * (b - a); // Adjust bounce intensity based on range
    return result; 

} // su_lerp_bounce()

//------------------------------------------------

float su_normalize_to_range(const float val, const float minVal, const float maxVal) 
{
    return (val - minVal) / (maxVal - minVal);
	
} // su_normalize_to_range()

//------------------------------------------------

// so that 0 to 1 gradients remap so nothing is greater than 0, and the 0-1 driver gets everything to 1
float su_remap_to_driver(const float val, const float driver)
{
	const float fullDriver = driver * 2.0f; // so when driver == 1, all val also = 1
	const float offsetVal = val - 1.0f; // So that range is -1 to 0
	const float remapped = su_saturate(offsetVal + fullDriver);
	return remapped;

} // su_remap_to_driver()

//------------------------------------------------

// return the final Height Pos the Quad needs to be to conform to the landscape
float conform_to_terrain(const vec3 pos, const vec3 offset)
{
	// Terrain Data etc
	const vec3 worldObjectPos = push.modelMatrix[3].xyz;

	// These values should be passed in
	const vec3 terrainPos = vec3(0.0f, 0.0f, 0.0f);
	const float terrainExtents = 100.0f;
	const float terrainHeightExtents = 10.0f; 
	const float slightOffset = 0.05f;

	// Use Quad's final Pos as UVs
	const vec2 heightUV = pos.xz + vec2(offset.x, offset.z) - worldObjectPos.xz;
	const vec2 terrainUV = heightUV / terrainExtents;
	const vec2 finalHeightUV = (terrainUV * -1.0f) + 0.5f;

	// If beyond boundries of terrain, "fall off" edge
	if (finalHeightUV.x <= 0.0f || finalHeightUV.x >= 1.0f || finalHeightUV.y <= 0.0f || finalHeightUV.y >= 1.0f)
	{
		const float nudgeOffset = terrainExtents * 0.25f;
		return -abs(pos.y) - nudgeOffset;
	}

	// Sample Heightmap and remap to get final Y Pos
	const float heightSample = texture(HeightMapSampler, finalHeightUV).r;
	const float heightRemapped = (heightSample * 2.0f) - 1.0f;
	const float terrainHeight = heightRemapped * terrainHeightExtents;
	const float offsetTerrainHeight = terrainHeight - terrainPos.y + slightOffset;
	const float distToTerrain = pos.y - offsetTerrainHeight;
	const float finalHeight = -distToTerrain;

	return finalHeight;

} // conform_to_terrain()

//------------------------------------------------

// Utilize Burst Data passed in through UV Channels to "burst" specific areas of Mesh over time
vec4 burst(const vec3 quadPos, const vec3 quadNormal, const float seed, const float driver, const vec4 burstData, const vec2 burstDataB)
{
	// Burst Params that should be passed in
	const float burstRadius = 3.0f;
	const float skExplodeMin = 0.4f;
	const float skExplodeMax = 16.0f;
	const float skExplodeScale = 8.0f;
	const int skBurstSameCount = 10;

	const vec3 burstPos = burstData.xyz;
	const int burstId = int(burstData.w);

	const int finalCount = uc_burstCount / skBurstSameCount; 
	const int finalId = burstId % finalCount;

	const float iter = floor(driver * finalCount);
	const float slice = 1.0f / float(finalCount);
	const float start = iter * slice;
	const float end = start + slice;
	const float blend = smoothstep(start, end, driver);

	const float idMatch = (iter == finalId) ? 1.0f : 0.0f;
    const float idBeyond = (iter > finalId) ? 1.0f : 0.0f;

	const vec3 burstDir = su_safe_normalize(quadPos - burstPos);

#ifdef BURST_DIST_ENCODED
	const float distToBurstOrigin = burstDataB.y;
#else
	const float distToBurstOrigin = distance(quadPos, burstPos); // If Data not available, calc now
#endif
	const float scaledDist = distToBurstOrigin / burstRadius;
	const float distMask = 1.0f - su_saturate(scaledDist);

	const float explodeIntensity = mix(skExplodeMin, skExplodeMax, uc_animateIntensity);
	const float explodeBlend = pow(blend, 1.0f) * idMatch;
	const float explodeMask = mix(explodeBlend, 1.0f, idBeyond);
	const float explodeStrength = explodeIntensity * explodeMask * distMask;
	const vec3 finalExplodeOffset = burstDir * explodeStrength;

	const float scaleBlend = pow(blend, 1.0f) * idMatch;
	const float scaleMask = mix(scaleBlend, 1.0f, idBeyond);
	const float scaleOverTime = su_lerp_chain(1.0f, skExplodeScale, 0.0f, scaleMask);

	const float finalScale = scaleOverTime;

	return vec4(finalExplodeOffset, finalScale); 

} // burst()

//------------------------------------------------

// animate so the hologram "explodes into a billion pieces"
vec3 explode(const vec3 quadPos, const vec3 quadNormal, const float seed, const float driver)
{
	// TODO:: Wrap into Function
	const int shapeRange = 6500;
	const float minShapeStrength = 0.1f;
	const float maxShapeStrength = 4.0f;
	const float shapeSeed = fract(seed * shapeRange);
	const float spreadStrengthShape = max(shapeSeed, minShapeStrength) * maxShapeStrength;

	// TODO:: Wrap into Function
	const int upRange = 2255;
	const float maxHeight = 4.5f;
	const float minHeight = 0.1f;
	const float upSeed = fract(seed * upRange);
	const float finalVertExtents = max(upSeed, minHeight) * maxHeight;

	// TODO:: Wrap into Function
	const int bounceRange = 7600;
	const float bounceExtents = fract(seed * bounceRange);

	// TODO:: Wrap into Function
	const int decayRange = 1200;
	const float decayFactor = fract(seed * decayRange);

	// How many different "timings" for the Quads through the Stages
	const int stageRange = 450; 
	const float stageSeed = fract(seed * stageRange);
	const float stageOffsetExtents = 0.12f;
	const float stageBlendOffset = stageSeed * stageOffsetExtents;

	// Starting Blend for the Stages of Explosion
	const float driverScalar = 1.0f + stageOffsetExtents;
	const float offsetDriver = (driver * driverScalar) - stageBlendOffset;
	const float blend = su_saturate(offsetDriver);

	// Drivers (in-sequence) for the various stages of explosion
	const float implodeSpan = 0.1f; 
	const float popSpan = 0.5f; 
	const float implodeEnd = implodeSpan; 
	const float popEnd = implodeEnd + popSpan; 
	const float implodeDriver = smoothstep(0.0f, implodeEnd, blend); // 0% to 10%
	const float vertDriver = smoothstep(implodeEnd, popEnd, blend); // 10% to 60%
	const float bounceDriver = smoothstep(popEnd, 1.0f, blend); // 60% to 100%
	const float spreadDriver = smoothstep(implodeEnd, 1.0f, blend); // 10% to 100%

	// Implode
	const float implodeExpo = 0.75f;
	const float implodeIntensity = 0.9f;
	const float implodeGradient = su_normalize_to_range(quadPos.y, push.minBounds.y, push.maxBounds.y);
	const float implodeInvMask = 1.0f - su_saturate(implodeGradient);
	const float implodeExpoMask = pow(implodeInvMask, implodeExpo);
	const vec3 implodeNormal = quadPos * implodeExpoMask * implodeIntensity;
	const vec3 implodeOffset = -mix(vec3(0.0f), implodeNormal, implodeDriver);

	// The final 'star-burst' shape when Quads are spread out
	const float spreadX = 0.4f;
	const float spreadZ = spreadX * 0.5f;
	const float spreadOscRate = 8.0f;
	const float spreadShapeA = sin(quadNormal.x * spreadOscRate) * spreadX;
	const float spreadShapeB = cos(quadNormal.z * spreadOscRate * 2.0f) * spreadZ;
	const float finalSpreadShape = spreadStrengthShape + spreadShapeA + spreadShapeB;

	// The Full Extent Spread Offset
	const float fullSpreadExtent = mix(1.0f, 40.0f, uc_animateIntensity);
	const float spreadOverTime = mix(0.0f, fullSpreadExtent, spreadDriver);
	const float spreadIntensity = spreadOverTime * finalSpreadShape;
	const vec3 spreadOffset =  quadNormal * spreadIntensity;
	const vec3 finalSpreadDir = implodeOffset + spreadOffset;

	// Conformed to Terrain
	const float snappedToTerrain = conform_to_terrain(quadPos, finalSpreadDir);

	// All Blended Height Positions
	const float groundHeight = 0.0f;
	const float distToGround = abs(quadPos.y - groundHeight);
	const float heightOffsetStart = groundHeight;
	const float heightMiddle = quadPos.y + finalVertExtents;

	// Either to the terrain or specific infinite plane
	//const float heightOffsetEnd = -distToGround;
	const float heightOffsetEnd = snappedToTerrain;

	// Vertical Movement (Explode, Fall, and Bounce)
	const float riseAndFall = su_lerp_chain(heightOffsetStart, heightMiddle, heightMiddle, heightOffsetEnd, vertDriver);
	const float bounceIntensityOverTime = heightOffsetEnd - heightOffsetStart; // Bounce gets weaker over time
	const float bounceOverTime = su_bounce(bounceExtents, decayFactor, bounceDriver) * bounceIntensityOverTime;
	const float finalPosY = riseAndFall - bounceOverTime;
	
	// Composite XZ Spread/Implosion with Vertical Pop, Fall, and Bounce
	const vec3 finalExplodeOffset = vec3(finalSpreadDir.x, finalPosY, finalSpreadDir.z);
	return finalExplodeOffset;

} // explode()

//------------------------------------------------

// Calculate Oscillation Mask based on the View Angle to get movement on "edge" of the hologram
float calc_fringe_mask(const vec3 quadPos, const vec3 quadNormal, const vec3 worldCamPos)
{
	const vec3 tempWorldPos = (push.modelMatrix * vec4(quadPos, 1.0f)).xyz;
	const vec3 viewDir = normalize(worldCamPos - tempWorldPos);
	const float fringeTest = dot(quadNormal, viewDir);
	const float invFringeTest = 1.0f - abs(fringeTest);
	const float finalFringe = pow(invFringeTest, uc_fringeExpo);
	return finalFringe;

} // calc_fringe_mask()

//------------------------------------------------

// Mask Based on Quad Pos, for creating "tornado" undulation
float calc_tornado_mask(const vec3 pos, const vec3 extents, const float driver, const float seed)
{
	// Y-Gradient
	const float pingpong = su_lerp_chain(0.0f, 1.0f, 0.0f, driver);
	const float posGradient = su_normalize_to_range(pos.y, push.minBounds.y, push.maxBounds.y);
	const float undulatePosMask = su_remap_to_driver(posGradient, pingpong);

	// Random Seed
	const float gridSeed = fract(seed * 2468.0f); 
	const float blendById = su_remap_to_driver(gridSeed, driver);
	const float randoSeedMask = su_lerp_chain(0.0f, 1.0f, 0.0f, blendById);

	// Blend Between Gradient and Seed
	const float skRandomRatio = 0.33f;
	const float finalMask = mix(undulatePosMask, randoSeedMask, skRandomRatio);
	return finalMask;

} // calc_tornado_mask()

//------------------------------------------------

vec3 calc_tornado(const float mask, const float seed, const vec3 pos)
{
	// Parameters to drive Tornado visuals
	const float intensity = mix(1.2f, 5.1f, uc_animateIntensity);
	const float rate = 2.0f;
	const float offsetScale = 40.0f;
	const float minSeedB = 0.1f;
	const float rateB = 10.0f;
	const float finalIntensity = 10.0f;
	const float tornadoTime = uBuffer.gameTime * rate;

	// Layer A:: Macro Spin
	const float oscOffsetA = seed * offsetScale;
	const float oscTimeA = tornadoTime - oscOffsetA;
	const float oscExtentsA = mask * intensity;
	const vec3 oscAX = sin(oscTimeA) * kWorldRight * oscExtentsA;
	const vec3 oscAZ = cos(oscTimeA) * kWorldForward * oscExtentsA;
	const vec3 layerA = oscAX + oscAZ;

	// Layer B:: Micro Spin
	const float extentsB = mix(0.1f, 0.5f, uc_animateIntensity);
	const float seedForTime = fract(seed * 123) + minSeedB;
	const float rangeSeedB = fract(seed * 346) + minSeedB;
	const float tornadoTimeB = tornadoTime * rateB;
	const float oscTimeB = tornadoTimeB * seedForTime;
	const float oscExtentsB = mask * intensity * rangeSeedB * extentsB;
	const vec3 oscBX = sin(oscTimeB) * kWorldRight * oscExtentsB;
	const vec3 oscBZ = cos(oscTimeB) * kWorldForward * oscExtentsB;
	const vec3 layerB = oscBX + oscBZ;

	// Layer C:: Medium Spin
	const float intensityC = mask * mix(0.5f, 2.5f, uc_animateIntensity);
	const float oscOffsetC = seed * 2.0f;
	const float gradientC = pos.y * 0.1f;
	const float sineTimeC = tornadoTime - gradientC - oscOffsetC;
	const vec3 layerC = sin(sineTimeC) * kWorldRight * intensityC;

	// Output
	const vec3 finalOsc = (layerA + layerB + layerC) * finalIntensity;
	return finalOsc;

} // calc_tornado()

//------------------------------------------------

// Ambient Local-Space Undulation
vec3 calc_undulation(const float mask, const float seed)
{
	const float intensity = 0.1f;
	const float oscOffsetXZ = seed * uc_undulateOffsetScale;
	const float oscOffsetY = oscOffsetXZ * 2.0f;
	const float undulateTime = uBuffer.gameTime * uc_undulateRate;

	const float oscExtentsXZ = uc_oscillationExtents * mask * intensity;
	const float oscExtentsY = uc_undulationExtents * mask * intensity;
	
	const float oscTimeXZ = undulateTime - oscOffsetXZ;
	const float oscTimeY = (undulateTime * 2.0f) - oscOffsetY;

	const vec3 oscX = sin(oscTimeXZ) * kWorldRight * oscExtentsXZ;
	const vec3 oscZ = cos(oscTimeXZ) * kWorldForward * oscExtentsXZ;
	const vec3 oscY = sin(oscTimeY) * kWorldUp * oscExtentsY;

	const vec3 finalOsc = oscX + oscY + oscZ;

	return finalOsc;

} // calc_undulation() 

//------------------------------------------------

// Uses Quad ID to offset all Quads into NxN Grid
vec3 calc_gridify_layout(const vec3 meshExtents, const float seed)
{
	// Parameters to drive Gridify visuals
	const float gridExtents = mix(2.0f, 6.0f, uc_animateIntensity);
	const float range = 1000.0f;

	const vec3 extentsHalf = (meshExtents / 2.0f) * gridExtents;
	const vec3 extentsFull = meshExtents * gridExtents;

	const float col = fract(seed * range);
	const float row = seed;
	const float gridX = (row * extentsFull.x) - extentsHalf.x;
	const float gridY = (col * extentsFull.y);
	const float gridZ = meshExtents.z;

	const vec3 gridCoord = vec3(gridX, gridY, gridZ);
	return gridCoord;

} // calc_gridify_layout()

//------------------------------------------------

// Go from "Idle" and fly into Grid Points
vec4 grid_fly_anim(const vec3 extents, const vec3 startPos, const vec3 gridPos, const float seed, const vec3 quadPos, const float driver)
{
	// Blend via Vertex Pos
	const float posGradient = su_normalize_to_range(quadPos.x, push.minBounds.x, push.maxBounds.x);
	const float blendByPos = su_remap_to_driver(posGradient, driver);

	// Random Seed
	const float gridSeed = fract(seed * 3356.0f); 
	const float blendById = su_remap_to_driver(gridSeed, driver);

	// Blended Seed
	const float blend = 0.75f;
	const float animBlend = mix(blendByPos, blendById, blend); // Mostly random, little positional

	const vec3 extentsOffsetA = extents * vec3(6.0f, -3.0f, 3.0f);
	const vec3 extentsOffsetB = extents * vec3(1.0f, 6.0f, -3.0f);
	const vec3 midPosA = startPos + extentsOffsetA;
	const vec3 midPosB = startPos + extentsOffsetB; 
	const vec3 lerpedPos = su_calc_cubic_bezier_curve(startPos, midPosA, midPosB, gridPos, animBlend);

	return vec4(lerpedPos, 1.0f);

} // grid_fly_anim()

//------------------------------------------------------------------------------------------------
// MAIN
//------------------------------------------------

void main()
{
	// Optimization and Feature Flags
	const bool enableLightingFlag = (uc_enableLighting != 0.0f);
	const bool enableStrobeFlag = (uc_enableStrobe != 0.0f);
	const bool enableViewCullFlag = (uc_enableViewCulling != 0.0f);
	const bool enableCamDistScaleFlag = (uc_enableCamDistScaling != 0.0f);
	const bool enableBillboardingFlag = (uc_enableBillboarding != 0.0f);
	const bool billboardPerQuad = (uc_billboardPerQuad != 0.0f);

	// Get Pos, Normals etc from Buffers and Push Constants
	const vec3 worldObjectPos = push.modelMatrix[3].xyz; 
	const vec3 worldObjectScale = su_extract_scale_from_xform(push.modelMatrix); 
	const vec3 worldCamPos = uBuffer.invViewMat[3].xyz; 
	const vec3 modelNormal = normalize(normal);
	const vec3 worldNormal = normalize(mat3(push.normalMatrix) * normal);
	const mat4 projViewMat = uBuffer.projMat * uBuffer.viewMat;
	const vec3 meshExtents = push.maxBounds.xyz - push.minBounds.xyz;

	// Extract Quad's World Pos (four vertices should have same value)
	const vec3 decompressedPos = (color.rgb - 0.5f) * uc_decompressScalar;
	const float decompressedId = color.a; // Vertex Colors are stored as vec4 (not 8-bit components hmm...TODO:: Optimize)
	const vec3 axisSign = sign(decompressedPos);

	//--------------------------------------------------

	// Ambient World Oscillation
	const float fringeMask = calc_fringe_mask(decompressedPos, worldNormal, worldCamPos); 

	const float tornadoMask = calc_tornado_mask(decompressedPos, meshExtents, uc_tornadoSlider, decompressedId);
	const vec3 tornadoOffset = calc_tornado(tornadoMask, decompressedId, decompressedPos);

	const float finalUndulateMask = su_saturate(fringeMask);
	const vec3 ambientUndulate = calc_undulation(fringeMask, decompressedId);

	//--------------------------------------------------

	// Explosion
	const vec3 explodeOffset = explode(decompressedPos, worldNormal, decompressedId, uc_explodeSlider);

	// Burst
	const vec4 burstResult = burst(decompressedPos, worldNormal, decompressedId, uc_burstSlider, uv1, uv0.zw);
	const vec3 burstOffset = burstResult.xyz;
	const float burstScale = burstResult.w;

	//--------------------------------------------------

	// Composite Vertex Animations Together
	const vec3 finalPosOffset = ambientUndulate + explodeOffset + burstOffset + tornadoOffset;
	const float finalAnimScale = burstScale;

	//--------------------------------------------------

	// Pos of Quad in World-Space (Want final Pos of Quad to use Features)
	const vec3 quadPos = decompressedPos + finalPosOffset;
	const vec3 worldQuadPos = (push.modelMatrix * vec4(quadPos, 1.0f)).xyz;

	// Calculate the direction from the quad to the camera
	const vec3 target = (billboardPerQuad) ? (worldQuadPos) : worldObjectPos;
	const vec3 camToTarget = target - worldCamPos;
    const vec3 quadFacingDir = -normalize(camToTarget);

	// Create a Rotation Matrix
	mat3 billboardMat = mat3(1.0f);
	vec3 billboardScale = vec3(1.0f);
	if (enableBillboardingFlag)
	{
		// Get Axis of the Quad
		const vec3 viewRight = normalize(cross(kWorldUp, quadFacingDir)); // Right direction of the quad
		const vec3 viewUp = normalize(cross(quadFacingDir, viewRight)); // Recompute up direction after right cross
		const vec3 viewForward = -quadFacingDir;

		// Need to 'Counter-Rotate' the Quads to ensure correct billboarding
		// This extra work seems kinda costly, perhaps alternative is to disallow Object Rotation?
		const mat3 inverseObjectRot = inverse(mat3(push.modelMatrix));
		billboardMat = inverseObjectRot * mat3(viewRight, viewUp, viewForward);		
		billboardScale = worldObjectScale; // Need to reapply Object Scale since inverseRot negates it
	}

	// Strobe Feature
	float strobeTest = 1.0f;
	if (enableStrobeFlag)
	{
		const float skStrobeRate = 60.0f;
		const float strobeOffset = decompressedPos.y * uc_strobeGradient;
		const float strobeTime = (uBuffer.gameTime * skStrobeRate) - strobeOffset;
		const float strobeRange = mod(strobeTime, uc_strobeSlice);
		strobeTest = (strobeRange > 1.0f) ? 1.0f : 0.0f;
	}

	// For Testing View Culling
	float viewCullTest = 1.0f;
	if (enableViewCullFlag)
	{
		const vec3 testNormal = quadFacingDir; // The View Direction
		const float dotTest = dot(worldNormal, testNormal);
		viewCullTest = (dotTest > uc_viewCullThreshold) ? 1.0f: 0.0f;
	}	

	// Create Dist Scale Factor 
	float distScale = uc_tileScaleMax; // "Regular" Size
	if (enableCamDistScaleFlag)
	{
		const vec3 camToQuad = worldQuadPos - worldCamPos;
		const float distToCamera = length(camToQuad);
		const float scaledDistToCam = su_saturate(distToCamera / uc_camDistInfluence);
		const float powDistToCam = pow(scaledDistToCam, uc_camDistExpo);
		const float camDistBlend = su_saturate(powDistToCam);
		distScale = mix(uc_tileScaleMin, uc_tileScaleMax, camDistBlend);
	}

	// Combine Flags and Scales for final Scale (forcing vertices degenerate if at zero)
	const float finalScale = distScale * strobeTest * viewCullTest;

	//--------------------------------------------------

	// Move our vertex to Model Origin so it can be transformed, then apply final World Transform
	const vec3 originPos = pos - decompressedPos;
	const vec3 scaledPos = originPos * finalScale * billboardScale * finalAnimScale;
	const vec3 rotatedPos = billboardMat * scaledPos;
	const vec3 translatedPos = rotatedPos + quadPos;
	const vec4 revertedPos = push.modelMatrix * vec4(translatedPos, 1.0f);

	// Rotate Normals also
	const vec3 rotatedNormal = billboardMat * worldNormal; 

	//--------------------------------------------------

	// Gridify 
	const vec3 meshExtentsScaled = meshExtents * 0.75f;
	const vec3 gridOffset = calc_gridify_layout(meshExtentsScaled, decompressedId);
	const vec3 tileScaledOnlyPos = originPos * uc_tileScaleMax;
	const vec3 linearGridPos = worldObjectPos + tileScaledOnlyPos + gridOffset;

	// Animation Fun Via Gridify 
	const vec4 gridFlyPos = grid_fly_anim(meshExtentsScaled, revertedPos.xyz, linearGridPos, decompressedId, decompressedPos, uc_gridifySlider);

	//--------------------------------------------------

	// Lighting
	// Intentionally using Two-Lights Only
	// These "Lights" are meant to excentuate detail on Hologram
	// Not meant to reflect Scene's Lighting
	float finalLightCalc = 1.0f;
	if (enableLightingFlag)
	{
		const float skLightExpo = 2.5f;
		const float skMinLight = 0.05f;

		const vec3 lightDir = vec3(0.1f, uc_verticalLight, 0.0f);
		const float lightDotA = dot(worldNormal, lightDir);
		const float lightA = max(0.0f, lightDotA);

		const vec3 lightDirB = vec3(uc_horizontalLight, -0.9f, 0.0f);
		const float lightDotB = dot(worldNormal, lightDirB);
		const float lightB = max(0.0f, lightDotB);

		const float lightIntensity = lightA + lightB;
		const float lightIntensityExpo = pow(lightIntensity, skLightExpo);

		// Output Get Minimum
		finalLightCalc = max(skMinLight, lightIntensityExpo);
	}

	//--------------------------------------------------

	// Which Pos to transform into Screen-Space
	//const vec4 posForFinal = (uc_gridifySlider > 0.0f) ? gridFlyPos : revertedPos;
	const vec4 posForFinal = mix(revertedPos, gridFlyPos, uc_gridifySlider);

	// Final Pos
	const vec4 finalPos = projViewMat * posForFinal;

	// Final UV
	const float quadDepth = uv0.z; 
	const vec2 in_uv = uv0.xy;
	const float oneSixteenth = 0.0625f;
	const float uvOffset = decompressedId * oneSixteenth; 
	const float uvScale = finalScale * 0.5f;
	const vec2 finalUV = (in_uv * uvScale) + uvOffset;
	const vec3 finalColor = vec3(finalLightCalc);

	//--------------------------------------------------

	// Output
	gl_Position = finalPos;
	fragColor = vec4(finalColor, color.a);
	fragWorldPos = revertedPos.xyz;
	fragWorldNormal = worldNormal;
	fragData = vec4(finalUV, quadDepth, finalUndulateMask);

} // main()