#pragma kernel RenderStereo RWStructuredBuffer result; RWStructuredBuffer forceWaitResultBuffer; StructuredBuffer cameraPixels; uint equirectangularWidth; uint equirectangularHeight; uint ssaaFactor; uint cameraWidth; uint cameraHeight; float tanHalfHFov, tanHalfVFov, hFovAdjust, vFovAdjust, interpupillaryDistance, circleRadius; uint numCirclePoints; uint circlePointStart, circlePointEnd, circlePointCircularBufferStart, circlePointCircularBufferSize; uint leftRightPass; uint forceWaitValue; uint cameraPixelsSentinelIdx; [numthreads(32,32,1)] // Must match threadsX, threadsY in CapturePanorama.cs void RenderStereo (uint3 dtid : SV_DispatchThreadID) { if (dtid.x >= equirectangularWidth || dtid.y >= equirectangularHeight) // In case width/height not multiple of numthreads return; if (dtid.x == equirectangularWidth - 1 && dtid.y == equirectangularHeight - 1 && dtid.z == 1) { forceWaitResultBuffer[0] = forceWaitValue; // Used on CPU side to force a wait for this operation to complete result[equirectangularWidth * equirectangularHeight * 2] = cameraPixels[cameraPixelsSentinelIdx]; // Sentinel value - set correctly only if set correctly in input buffer } static const float pi = 3.14159265f; uint2 pos = dtid.xy; uint2 loopStart = pos * ssaaFactor; uint2 loopEnd = loopStart + uint2(ssaaFactor, ssaaFactor); uint i = dtid.z; float4 totalColor = float4(0.0f, 0.0f, 0.0f, 0.0f); for (uint y = loopStart.y; y < loopEnd.y; y++) { for (uint x = loopStart.x; x < loopEnd.x; x++) { float xcoord = (float)x / (equirectangularWidth * ssaaFactor); float ycoord = (float)y / (equirectangularHeight * ssaaFactor); float latitude = (ycoord - 0.5f) * pi; float sinLat, cosLat; sincos(latitude, sinLat, cosLat); float longitude = (xcoord * 2.0f - 1.0f) * pi; float sinLong, cosLong; sincos(longitude, sinLong, cosLong); // Scale IPD down as latitude moves toward poles to avoid discontinuities float latitudeNormalized = latitude / (pi / 2.0f); // Map to [-1, 1] // float ipdScale = 1.0f; // float ipdScale = 1.0f - latitudeNormalized * latitudeNormalized; float ipdScale = 1.5819767068693265f * exp(-latitudeNormalized * latitudeNormalized) - 0.5819767068693265f; // float ipdScale = 1.1565176427496657f * exp(-2.0f * latitudeNormalized * latitudeNormalized) - 0.15651764274966568f; // float ipdScale = 1.0000454019910097f * exp(-10.0f * latitudeNormalized * latitudeNormalized) - 0.00004540199100968779f; float scaledEyeRadius = ipdScale * interpupillaryDistance / 2.0f; // The following is equivalent to: // Quaternion eyesRotation = Quaternion.Euler(0.0f, longitude * 360.0f / (2 * pi), 0.0f); // float3 initialEyePosition = (i == 0 ? float3.left : float3.right) * scaledEyeRadius; // float3 pos = eyesRotation * initialEyePosition; // eye position // float3 dir = eyesRotation * float3.forward; // gaze direction float3 dir = float3(sinLong, 0.0f, cosLong); // Find place on circle where gaze ray crosses circle. // Simplest way to do this is solve it geometrically assuming longitude=0, then rotate. float angle = (pi/2.0f - acos(scaledEyeRadius/circleRadius)); if (i == 0) angle = -angle; float circlePointAngle = longitude + angle; if (circlePointAngle < 0.0f) circlePointAngle += 2 * pi; if (circlePointAngle >= 2 * pi) circlePointAngle -= 2 * pi; float circlePointNumber = circlePointAngle / (2 * pi) * numCirclePoints; uint circlePoint0 = (uint)floor(circlePointNumber); if (circlePoint0 < circlePointStart) circlePoint0 += numCirclePoints; // Deal with an edge case when doing final slice with SSAA > 1 if (circlePoint0 < circlePointStart || circlePoint0 + 1 >= circlePointEnd) return; uint cameraNum; float u, v; float ipdScaleLerp = 1.0f - ipdScale * 5.0f; // Scale [0, 0.2] to [0, 1] and reverse // Top/bottom cap float4 colorCap = float4(0, 0, 0, 0); if (ipdScaleLerp > 0.0f) { float equirectRayDirectionX = cosLat * sinLong; float equirectRayDirectionY = sinLat; float equirectRayDirectionZ = cosLat * cosLong; float distance = 1.0f / equirectRayDirectionY; u = equirectRayDirectionX * distance; v = equirectRayDirectionZ * distance; if (u * u <= 1 && v * v <= 1) { if (equirectRayDirectionY > 0.0f) { cameraNum = 0; } else { u = -u; cameraNum = 1; } u = (u + 1.0f) * 0.5f; v = (v + 1.0f) * 0.5f; // GetCameraPixelBilinear(cameraPixels, cameraNum, u, v); u *= cameraWidth; v *= cameraHeight; uint left = (uint)floor(u); uint right = min(cameraWidth - 1, left + 1); uint top = (uint)floor(v); uint bottom = min(cameraHeight - 1, top + 1); float uFrac = frac(u); float vFrac = frac(v); uint baseIdx = cameraNum * cameraWidth * cameraHeight; uint topRow = baseIdx + top * cameraWidth; uint bottomRow = baseIdx + bottom * cameraWidth; uint topLeft = cameraPixels[topRow + left ]; uint topRight = cameraPixels[topRow + right]; uint bottomLeft = cameraPixels[bottomRow + left ]; uint bottomRight = cameraPixels[bottomRow + right]; float r = lerp(lerp( topLeft >> 16 , bottomLeft >> 16 , vFrac), lerp( topRight >> 16 , bottomRight >> 16 , vFrac), uFrac); float g = lerp(lerp((topLeft >> 8) & 0xFF, (bottomLeft >> 8) & 0xFF, vFrac), lerp((topRight >> 8) & 0xFF, (bottomRight >> 8) & 0xFF, vFrac), uFrac); float b = lerp(lerp( topLeft & 0xFF, bottomLeft & 0xFF, vFrac), lerp( topRight & 0xFF, bottomRight & 0xFF, vFrac), uFrac); float4 col = float4(r, g, b, 255.0f); colorCap = col; } } float4 color0 = float4(0, 0, 0, 0), color1 = float4(0, 0, 0, 0); for (uint j=0; j < 2; j++) { uint circlePointIdx = (circlePoint0 + j) % numCirclePoints; float cameraPointAngle = 2 * pi * circlePointIdx / numCirclePoints; float sinCameraPointAngle, cosCameraPointAngle; sincos(cameraPointAngle, sinCameraPointAngle, cosCameraPointAngle); // Equivalent to (using fact that both dir and circlePointNorm are unit vectors): // Quaternion circlePointRotation = Quaternion.Euler(0.0f, cameraPointAngle * 360.0f / (2 * pi), 0.0f); // float3 circlePointNormal = circlePointRotation * float3.forward; // float newLongitudeDegrees = sign(cross(circlePointNormal, dir).y) * angle(circlePointNormal, dir); // Clamp here avoids numerical out-of-bounds trouble when circlePointAngle = longitude float newLongitude = sign(dir.x * cosCameraPointAngle - dir.z * sinCameraPointAngle) * acos(clamp(dir.z * cosCameraPointAngle + dir.x * sinCameraPointAngle, -1.0f, 1.0f)); float sinNewLong, cosNewLong; sincos(newLongitude, sinNewLong, cosNewLong); // Select which of the two cameras for this point to use and adjust ray to make camera plane perpendicular to axes // 2 + because first two are top/bottom uint cameraNumBase = 2 + ((circlePoint0 + j + circlePointCircularBufferStart - circlePointStart) % circlePointCircularBufferSize) * 2; float3 textureRayDirAdjusted; if (leftRightPass) { cameraNum = cameraNumBase + (newLongitude >= 0.0f ? 1 : 0); float longitudeAdjust = (newLongitude >= 0.0f ? -hFovAdjust : hFovAdjust); float longSum = newLongitude + longitudeAdjust; float sinLongSum, cosLongSum; sincos(longSum, sinLongSum, cosLongSum); // Equivalent to: // float3 textureRayDir = Quaternion.Euler(-latitude * 360.0f / (2 * pi), newLongitude * 360.0f / (2 * pi), 0.0f) * float3.forward; // float3 textureRayDirAdjusted = Quaternion.Euler(0.0f, longitudeAdjust * 360.0f / (2 * pi), 0.0f) * textureRayDir; textureRayDirAdjusted = float3(cosLat * sinLongSum, sinLat, cosLat * cosLongSum); // float3 textureRayDirAdjusted = float3( // sin(latitude + newLongitude + longitudeAdjust) - sinLat * cosLongSum, // sinLat, // cos(latitude + newLongitude + longitudeAdjust) + sinLat * sinLongSum); } else // if (!leftRightPass) { cameraNum = cameraNumBase + (latitude >= 0.0f ? 1 : 0); float latitudeAdjust = (latitude >= 0.0f ? vFovAdjust : -vFovAdjust); float sinLatAdjust, cosLatAdjust; sincos(latitudeAdjust, sinLatAdjust, cosLatAdjust); // Equivalent to: // textureRayDirAdjusted = Quaternion.Euler(latitudeAdjust * 360.0f / (2 * pi), 0.0f, 0.0f) * textureRayDir; textureRayDirAdjusted = float3(cosLat * sinNewLong, cosLatAdjust * sinLat - cosLat * cosNewLong * sinLatAdjust, sinLatAdjust * sinLat + cosLat * cosNewLong * cosLatAdjust); } u = textureRayDirAdjusted.x / textureRayDirAdjusted.z / tanHalfHFov; v = -textureRayDirAdjusted.y / textureRayDirAdjusted.z / tanHalfVFov; if (! (textureRayDirAdjusted.z > 0.0f && u * u <= 1.0f && v * v <= 1.0f) ) return; u = (u + 1.0f) * 0.5f; v = (v + 1.0f) * 0.5f; // GetCameraPixelBilinear(cameraPixels, cameraNum, u, v); u *= cameraWidth; v *= cameraHeight; uint left = (uint)floor(u); uint right = min(cameraWidth - 1, left + 1); uint top = (uint)floor(v); uint bottom = min(cameraHeight - 1, top + 1); float uFrac = frac(u); float vFrac = frac(v); uint baseIdx = cameraNum * cameraWidth * cameraHeight; uint topRow = baseIdx + top * cameraWidth; uint bottomRow = baseIdx + bottom * cameraWidth; uint topLeft = cameraPixels[topRow + left ]; uint topRight = cameraPixels[topRow + right]; uint bottomLeft = cameraPixels[bottomRow + left ]; uint bottomRight = cameraPixels[bottomRow + right]; float r = lerp(lerp( topLeft >> 16 , bottomLeft >> 16 , vFrac), lerp( topRight >> 16 , bottomRight >> 16 , vFrac), uFrac); float g = lerp(lerp((topLeft >> 8) & 0xFF, (bottomLeft >> 8) & 0xFF, vFrac), lerp((topRight >> 8) & 0xFF, (bottomRight >> 8) & 0xFF, vFrac), uFrac); float b = lerp(lerp( topLeft & 0xFF, bottomLeft & 0xFF, vFrac), lerp( topRight & 0xFF, bottomRight & 0xFF, vFrac), uFrac); float4 col = float4(r, g, b, 255.0f); if (j == 0) color0 = col; else color1 = col; } float4 c = lerp(color0, color1, frac(circlePointNumber)); if (colorCap.a > 0.0f && ipdScaleLerp > 0.0f) c = lerp(c, colorCap, ipdScaleLerp); totalColor += float4(c.r, c.g, c.b, 255.0f); } } totalColor /= ssaaFactor * ssaaFactor; result[((dtid.y + equirectangularHeight * i) * equirectangularWidth) + dtid.x] = ((uint)totalColor.r << 16) | ((uint)totalColor.g << 8) | (uint)totalColor.b; }