Compare commits
10 Commits
v0.0.4
...
a3635b5d31
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3635b5d31 | ||
|
|
bc9aa6fbad | ||
|
|
0c16665610 | ||
|
|
46f2f1ece5 | ||
|
|
4ffbb97abf | ||
|
|
98f5cbe309 | ||
|
|
93d856b3b6 | ||
| 6ea6971dd6 | |||
| 163d70e9bd | |||
| 8afef5065a |
@@ -273,3 +273,5 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
|
|||||||
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
|
||||||
- allow shaders to read other shaders data store based on name? or output over OSC
|
- allow shaders to read other shaders data store based on name? or output over OSC
|
||||||
- Mipmapping for shader-declared textures
|
- Mipmapping for shader-declared textures
|
||||||
|
- Anotate included shaders
|
||||||
|
- allow 3 vector exposed controls
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
if (!sInteractiveResize && pOpenGLComposite)
|
if (!sInteractiveResize && pOpenGLComposite)
|
||||||
{
|
{
|
||||||
wglMakeCurrent(hDC, hRC);
|
wglMakeCurrent(hDC, hRC);
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL(true);
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
RaiseStatusControls(sStatusStrip);
|
RaiseStatusControls(sStatusStrip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -501,6 +501,9 @@ bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& pa
|
|||||||
|
|
||||||
void ControlServer::BroadcastStateLocked()
|
void ControlServer::BroadcastStateLocked()
|
||||||
{
|
{
|
||||||
|
if (mClients.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||||
for (auto it = mClients.begin(); it != mClients.end();)
|
for (auto it = mClients.begin(); it != mClients.end();)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
*mRuntimeHost,
|
*mRuntimeHost,
|
||||||
[this]() { renderEffect(); },
|
[this]() { renderEffect(); },
|
||||||
[this]() { ProcessScreenshotRequest(); },
|
[this]() { ProcessScreenshotRequest(); },
|
||||||
[this]() { paintGL(); });
|
[this]() { paintGL(false); });
|
||||||
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||||
*mVideoIO,
|
*mVideoIO,
|
||||||
*mRenderer,
|
*mRenderer,
|
||||||
@@ -156,8 +156,26 @@ error:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::paintGL()
|
void OpenGLComposite::paintGL(bool force)
|
||||||
{
|
{
|
||||||
|
if (!force)
|
||||||
|
{
|
||||||
|
if (IsIconic(hGLWnd))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const unsigned previewFps = mRuntimeHost ? mRuntimeHost->GetPreviewFps() : 30u;
|
||||||
|
if (previewFps == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const auto minimumInterval = std::chrono::microseconds(1000000 / (previewFps == 0 ? 1u : previewFps));
|
||||||
|
if (mLastPreviewPresentTime != std::chrono::steady_clock::time_point() &&
|
||||||
|
now - mLastPreviewPresentTime < minimumInterval)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryEnterCriticalSection(&pMutex))
|
if (!TryEnterCriticalSection(&pMutex))
|
||||||
{
|
{
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
@@ -165,6 +183,7 @@ void OpenGLComposite::paintGL()
|
|||||||
}
|
}
|
||||||
|
|
||||||
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||||
|
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
LeaveCriticalSection(&pMutex);
|
LeaveCriticalSection(&pMutex);
|
||||||
}
|
}
|
||||||
@@ -314,9 +333,28 @@ void OpenGLComposite::renderEffect()
|
|||||||
}
|
}
|
||||||
else if (mRuntimeHost)
|
else if (mRuntimeHost)
|
||||||
{
|
{
|
||||||
if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates))
|
const unsigned renderWidth = mVideoIO->InputFrameWidth();
|
||||||
|
const unsigned renderHeight = mVideoIO->InputFrameHeight();
|
||||||
|
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
|
||||||
|
const bool renderStateCacheValid =
|
||||||
|
!mCachedLayerRenderStates.empty() &&
|
||||||
|
mCachedRenderStateVersion == renderStateVersion &&
|
||||||
|
mCachedRenderStateWidth == renderWidth &&
|
||||||
|
mCachedRenderStateHeight == renderHeight;
|
||||||
|
|
||||||
|
if (renderStateCacheValid)
|
||||||
|
{
|
||||||
|
layerStates = mCachedLayerRenderStates;
|
||||||
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (mRuntimeHost->TryGetLayerRenderStates(renderWidth, renderHeight, layerStates))
|
||||||
{
|
{
|
||||||
mCachedLayerRenderStates = layerStates;
|
mCachedLayerRenderStates = layerStates;
|
||||||
|
mCachedRenderStateVersion = renderStateVersion;
|
||||||
|
mCachedRenderStateWidth = renderWidth;
|
||||||
|
mCachedRenderStateHeight = renderHeight;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -324,6 +362,7 @@ void OpenGLComposite::renderEffect()
|
|||||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
|
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
|
||||||
mRenderPass->Render(
|
mRenderPass->Render(
|
||||||
hasInputSource,
|
hasInputSource,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
class VideoIODevice;
|
class VideoIODevice;
|
||||||
class OpenGLVideoIOBridge;
|
class OpenGLVideoIOBridge;
|
||||||
@@ -64,7 +65,7 @@ public:
|
|||||||
std::string GetOscAddress() const;
|
std::string GetOscAddress() const;
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
void resizeGL(WORD width, WORD height);
|
||||||
void paintGL();
|
void paintGL(bool force = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resizeWindow(int width, int height);
|
void resizeWindow(int width, int height);
|
||||||
@@ -87,8 +88,12 @@ private:
|
|||||||
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
||||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||||
|
uint64_t mCachedRenderStateVersion = 0;
|
||||||
|
unsigned mCachedRenderStateWidth = 0;
|
||||||
|
unsigned mCachedRenderStateHeight = 0;
|
||||||
std::atomic<bool> mUseCommittedLayerStates;
|
std::atomic<bool> mUseCommittedLayerStates;
|
||||||
std::atomic<bool> mScreenshotRequested;
|
std::atomic<bool> mScreenshotRequested;
|
||||||
|
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ void OpenGLRenderPass::Render(
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
const std::vector<RenderPassDescriptor> passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
|
const std::vector<RenderPassDescriptor>& passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
|
||||||
for (const RenderPassDescriptor& pass : passes)
|
for (const RenderPassDescriptor& pass : passes)
|
||||||
{
|
{
|
||||||
RenderLayerPass(
|
RenderLayerPass(
|
||||||
@@ -71,9 +71,9 @@ void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned input
|
|||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||||
glUseProgram(mRenderer.DecodeProgram());
|
glUseProgram(mRenderer.DecodeProgram());
|
||||||
|
|
||||||
const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution");
|
const GLint packedResolutionLocation = mRenderer.DecodePackedResolutionLocation();
|
||||||
const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution");
|
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
|
||||||
const GLint inputPixelFormatLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uInputPixelFormat");
|
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
|
||||||
if (packedResolutionLocation >= 0)
|
if (packedResolutionLocation >= 0)
|
||||||
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
|
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
|
||||||
if (decodedResolutionLocation >= 0)
|
if (decodedResolutionLocation >= 0)
|
||||||
@@ -96,7 +96,8 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
|||||||
// Flatten the layer stack into concrete GL passes. A layer may now contain
|
// Flatten the layer stack into concrete GL passes. A layer may now contain
|
||||||
// several shader passes, but the outer stack still sees one visible output
|
// several shader passes, but the outer stack still sees one visible output
|
||||||
// per layer.
|
// per layer.
|
||||||
std::vector<RenderPassDescriptor> passes;
|
std::vector<RenderPassDescriptor>& passes = mPassScratch;
|
||||||
|
passes.clear();
|
||||||
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
|
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
|
||||||
std::size_t descriptorCount = 0;
|
std::size_t descriptorCount = 0;
|
||||||
for (std::size_t index = 0; index < passCount; ++index)
|
for (std::size_t index = 0; index < passCount; ++index)
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ private:
|
|||||||
|
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
ShaderTextureBindings mTextureBindings;
|
ShaderTextureBindings mTextureBindings;
|
||||||
|
mutable std::vector<RenderPassDescriptor> mPassScratch;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
#include "VideoIOFormat.h"
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
|
||||||
@@ -21,6 +23,11 @@ OpenGLRenderPipeline::OpenGLRenderPipeline(
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpenGLRenderPipeline::~OpenGLRenderPipeline()
|
||||||
|
{
|
||||||
|
ResetAsyncReadbackState();
|
||||||
|
}
|
||||||
|
|
||||||
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||||
{
|
{
|
||||||
const VideoIOState& state = context.videoState;
|
const VideoIOState& state = context.videoState;
|
||||||
@@ -62,9 +69,9 @@ void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
|||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||||
glUseProgram(mRenderer.OutputPackProgram());
|
glUseProgram(mRenderer.OutputPackProgram());
|
||||||
|
|
||||||
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
|
const GLint outputResolutionLocation = mRenderer.OutputPackResolutionLocation();
|
||||||
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
|
const GLint activeWordsLocation = mRenderer.OutputPackActiveWordsLocation();
|
||||||
const GLint packFormatLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputPackFormat");
|
const GLint packFormatLocation = mRenderer.OutputPackFormatLocation();
|
||||||
if (outputResolutionLocation >= 0)
|
if (outputResolutionLocation >= 0)
|
||||||
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
||||||
if (activeWordsLocation >= 0)
|
if (activeWordsLocation >= 0)
|
||||||
@@ -78,18 +85,195 @@ void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
|||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
|
||||||
{
|
{
|
||||||
|
if (requiredBytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ResetAsyncReadbackState();
|
||||||
|
mAsyncReadbackBytes = requiredBytes;
|
||||||
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
|
{
|
||||||
|
glGenBuffers(1, &slot.pixelPackBuffer);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
||||||
|
slot.sizeBytes = requiredBytes;
|
||||||
|
slot.inFlight = false;
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
mAsyncReadbackWriteIndex = 0;
|
||||||
|
mAsyncReadbackReadIndex = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::ResetAsyncReadbackState()
|
||||||
|
{
|
||||||
|
FlushAsyncReadbackPipeline();
|
||||||
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
|
slot.sizeBytes = 0;
|
||||||
|
|
||||||
|
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||||
|
{
|
||||||
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
|
{
|
||||||
|
if (slot.pixelPackBuffer != 0)
|
||||||
|
{
|
||||||
|
glDeleteBuffers(1, &slot.pixelPackBuffer);
|
||||||
|
slot.pixelPackBuffer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mAsyncReadbackWriteIndex = 0;
|
||||||
|
mAsyncReadbackReadIndex = 0;
|
||||||
|
mAsyncReadbackBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
||||||
|
{
|
||||||
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
|
{
|
||||||
|
if (slot.fence != nullptr)
|
||||||
|
{
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
slot.fence = nullptr;
|
||||||
|
}
|
||||||
|
slot.inFlight = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAsyncReadbackWriteIndex = 0;
|
||||||
|
mAsyncReadbackReadIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
|
||||||
|
{
|
||||||
|
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||||
|
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
|
||||||
|
const GLenum format = usePackedOutput ? GL_RGBA : GL_BGRA;
|
||||||
|
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||||
|
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
|
||||||
|
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
|
||||||
|
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
|
||||||
|
|
||||||
|
if (requiredBytes == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mAsyncReadbackBytes != requiredBytes
|
||||||
|
|| mAsyncReadbackFormat != format
|
||||||
|
|| mAsyncReadbackType != type
|
||||||
|
|| mAsyncReadbackFramebuffer != framebuffer)
|
||||||
|
{
|
||||||
|
mAsyncReadbackFormat = format;
|
||||||
|
mAsyncReadbackType = type;
|
||||||
|
mAsyncReadbackFramebuffer = framebuffer;
|
||||||
|
if (!EnsureAsyncReadbackBuffers(requiredBytes))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
|
||||||
|
if (slot.fence != nullptr)
|
||||||
|
{
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
slot.fence = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
||||||
|
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
|
||||||
|
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
slot.inFlight = slot.fence != nullptr;
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
|
||||||
|
{
|
||||||
|
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackReadIndex];
|
||||||
|
if (!slot.inFlight || slot.fence == nullptr || slot.pixelPackBuffer == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
|
||||||
|
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
|
||||||
|
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
slot.fence = nullptr;
|
||||||
|
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||||
|
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
|
if (mappedBytes == nullptr)
|
||||||
|
{
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
slot.inFlight = false;
|
||||||
|
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
|
||||||
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
slot.inFlight = false;
|
||||||
|
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
||||||
|
CacheOutputFrame(outputFrame);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFrame)
|
||||||
|
{
|
||||||
|
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
|
||||||
|
mCachedOutputFrame.resize(byteCount);
|
||||||
|
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
|
||||||
|
{
|
||||||
|
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||||
|
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
if (usePackedOutput)
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
|
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, outputFrame.bytes);
|
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
||||||
|
{
|
||||||
|
if (TryConsumeAsyncReadback(outputFrame, 500000))
|
||||||
|
{
|
||||||
|
QueueAsyncReadback(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If async readback misses the playout deadline, prefer a fresh synchronous
|
||||||
|
// frame over reusing stale cached output, then restart the async pipeline.
|
||||||
|
if (outputFrame.bytes != nullptr)
|
||||||
|
{
|
||||||
|
ReadOutputFrameSynchronously(state, outputFrame.bytes);
|
||||||
|
CacheOutputFrame(outputFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushAsyncReadbackPipeline();
|
||||||
|
QueueAsyncReadback(state);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class OpenGLRenderer;
|
class OpenGLRenderer;
|
||||||
class RuntimeHost;
|
class RuntimeHost;
|
||||||
@@ -26,10 +29,26 @@ public:
|
|||||||
RenderEffectCallback renderEffect,
|
RenderEffectCallback renderEffect,
|
||||||
OutputReadyCallback outputReady,
|
OutputReadyCallback outputReady,
|
||||||
PaintCallback paint);
|
PaintCallback paint);
|
||||||
|
~OpenGLRenderPipeline();
|
||||||
|
|
||||||
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct AsyncReadbackSlot
|
||||||
|
{
|
||||||
|
GLuint pixelPackBuffer = 0;
|
||||||
|
GLsync fence = nullptr;
|
||||||
|
std::size_t sizeBytes = 0;
|
||||||
|
bool inFlight = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
|
||||||
|
void ResetAsyncReadbackState();
|
||||||
|
void FlushAsyncReadbackPipeline();
|
||||||
|
void QueueAsyncReadback(const VideoIOState& state);
|
||||||
|
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
|
||||||
|
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
|
||||||
|
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
|
||||||
void PackOutputFor10Bit(const VideoIOState& state);
|
void PackOutputFor10Bit(const VideoIOState& state);
|
||||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
@@ -38,4 +57,12 @@ private:
|
|||||||
RenderEffectCallback mRenderEffect;
|
RenderEffectCallback mRenderEffect;
|
||||||
OutputReadyCallback mOutputReady;
|
OutputReadyCallback mOutputReady;
|
||||||
PaintCallback mPaint;
|
PaintCallback mPaint;
|
||||||
|
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
|
||||||
|
std::size_t mAsyncReadbackWriteIndex = 0;
|
||||||
|
std::size_t mAsyncReadbackReadIndex = 0;
|
||||||
|
std::size_t mAsyncReadbackBytes = 0;
|
||||||
|
GLenum mAsyncReadbackFormat = GL_BGRA;
|
||||||
|
GLenum mAsyncReadbackType = GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||||
|
GLuint mAsyncReadbackFramebuffer = 0;
|
||||||
|
std::vector<unsigned char> mCachedOutputFrame;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ PFNGLGENBUFFERSPROC glGenBuffers;
|
|||||||
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
||||||
PFNGLBINDBUFFERPROC glBindBuffer;
|
PFNGLBINDBUFFERPROC glBindBuffer;
|
||||||
PFNGLBUFFERDATAPROC glBufferData;
|
PFNGLBUFFERDATAPROC glBufferData;
|
||||||
|
PFNGLMAPBUFFERPROC glMapBuffer;
|
||||||
|
PFNGLUNMAPBUFFERPROC glUnmapBuffer;
|
||||||
PFNGLBUFFERSUBDATAPROC glBufferSubData;
|
PFNGLBUFFERSUBDATAPROC glBufferSubData;
|
||||||
PFNGLBINDBUFFERBASEPROC glBindBufferBase;
|
PFNGLBINDBUFFERBASEPROC glBindBufferBase;
|
||||||
PFNGLACTIVETEXTUREPROC glActiveTexture;
|
PFNGLACTIVETEXTUREPROC glActiveTexture;
|
||||||
@@ -131,6 +133,8 @@ bool ResolveGLExtensions()
|
|||||||
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
|
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
|
||||||
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
|
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
|
||||||
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
|
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
|
||||||
|
glMapBuffer = (PFNGLMAPBUFFERPROC) wglGetProcAddress("glMapBuffer");
|
||||||
|
glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) wglGetProcAddress("glUnmapBuffer");
|
||||||
glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData");
|
glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData");
|
||||||
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase");
|
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase");
|
||||||
glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture");
|
glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture");
|
||||||
@@ -176,6 +180,8 @@ bool ResolveGLExtensions()
|
|||||||
&& glDeleteBuffers
|
&& glDeleteBuffers
|
||||||
&& glBindBuffer
|
&& glBindBuffer
|
||||||
&& glBufferData
|
&& glBufferData
|
||||||
|
&& glMapBuffer
|
||||||
|
&& glUnmapBuffer
|
||||||
&& glBufferSubData
|
&& glBufferSubData
|
||||||
&& glBindBufferBase
|
&& glBindBufferBase
|
||||||
&& glActiveTexture
|
&& glActiveTexture
|
||||||
|
|||||||
@@ -89,6 +89,11 @@
|
|||||||
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
|
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
|
||||||
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
|
||||||
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
|
||||||
|
#define GL_ALREADY_SIGNALED 0x911A
|
||||||
|
#define GL_TIMEOUT_EXPIRED 0x911B
|
||||||
|
#define GL_CONDITION_SATISFIED 0x911C
|
||||||
|
#define GL_WAIT_FAILED 0x911D
|
||||||
|
#define GL_READ_ONLY 0x88B8
|
||||||
|
|
||||||
typedef struct __GLsync *GLsync;
|
typedef struct __GLsync *GLsync;
|
||||||
typedef unsigned __int64 GLuint64;
|
typedef unsigned __int64 GLuint64;
|
||||||
@@ -100,6 +105,8 @@ typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
|
|||||||
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
|
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
|
||||||
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
|
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
|
||||||
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
|
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
|
||||||
|
typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERPROC) (GLenum target, GLenum access);
|
||||||
|
typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target);
|
||||||
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
|
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
|
||||||
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
|
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
|
||||||
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
|
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
|
||||||
@@ -159,6 +166,8 @@ extern PFNGLGENBUFFERSPROC glGenBuffers;
|
|||||||
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
|
||||||
extern PFNGLBINDBUFFERPROC glBindBuffer;
|
extern PFNGLBINDBUFFERPROC glBindBuffer;
|
||||||
extern PFNGLBUFFERDATAPROC glBufferData;
|
extern PFNGLBUFFERDATAPROC glBufferData;
|
||||||
|
extern PFNGLMAPBUFFERPROC glMapBuffer;
|
||||||
|
extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
|
||||||
extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
|
extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
|
||||||
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
|
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
|
||||||
extern PFNGLACTIVETEXTUREPROC glActiveTexture;
|
extern PFNGLACTIVETEXTUREPROC glActiveTexture;
|
||||||
|
|||||||
@@ -71,6 +71,9 @@ void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader,
|
|||||||
mDecodeProgram = program;
|
mDecodeProgram = program;
|
||||||
mDecodeVertexShader = vertexShader;
|
mDecodeVertexShader = vertexShader;
|
||||||
mDecodeFragmentShader = fragmentShader;
|
mDecodeFragmentShader = fragmentShader;
|
||||||
|
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
|
||||||
|
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
|
||||||
|
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
||||||
@@ -78,6 +81,9 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
|
|||||||
mOutputPackProgram = program;
|
mOutputPackProgram = program;
|
||||||
mOutputPackVertexShader = vertexShader;
|
mOutputPackVertexShader = vertexShader;
|
||||||
mOutputPackFragmentShader = fragmentShader;
|
mOutputPackFragmentShader = fragmentShader;
|
||||||
|
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
|
||||||
|
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
|
||||||
|
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
|
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
|
||||||
@@ -217,6 +223,9 @@ void OpenGLRenderer::DestroyDecodeShaderProgram()
|
|||||||
glDeleteProgram(mDecodeProgram);
|
glDeleteProgram(mDecodeProgram);
|
||||||
mDecodeProgram = 0;
|
mDecodeProgram = 0;
|
||||||
}
|
}
|
||||||
|
mDecodePackedResolutionLocation = -1;
|
||||||
|
mDecodeDecodedResolutionLocation = -1;
|
||||||
|
mDecodeInputPixelFormatLocation = -1;
|
||||||
|
|
||||||
if (mDecodeFragmentShader != 0)
|
if (mDecodeFragmentShader != 0)
|
||||||
{
|
{
|
||||||
@@ -238,6 +247,9 @@ void OpenGLRenderer::DestroyOutputPackShaderProgram()
|
|||||||
glDeleteProgram(mOutputPackProgram);
|
glDeleteProgram(mOutputPackProgram);
|
||||||
mOutputPackProgram = 0;
|
mOutputPackProgram = 0;
|
||||||
}
|
}
|
||||||
|
mOutputPackResolutionLocation = -1;
|
||||||
|
mOutputPackActiveWordsLocation = -1;
|
||||||
|
mOutputPackFormatLocation = -1;
|
||||||
|
|
||||||
if (mOutputPackFragmentShader != 0)
|
if (mOutputPackFragmentShader != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ public:
|
|||||||
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
||||||
GLuint DecodeProgram() const { return mDecodeProgram; }
|
GLuint DecodeProgram() const { return mDecodeProgram; }
|
||||||
GLuint OutputPackProgram() const { return mOutputPackProgram; }
|
GLuint OutputPackProgram() const { return mOutputPackProgram; }
|
||||||
|
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
|
||||||
|
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
|
||||||
|
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
|
||||||
|
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
|
||||||
|
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
|
||||||
|
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
||||||
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
||||||
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
||||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||||
@@ -101,9 +107,15 @@ private:
|
|||||||
GLuint mDecodeProgram = 0;
|
GLuint mDecodeProgram = 0;
|
||||||
GLuint mDecodeVertexShader = 0;
|
GLuint mDecodeVertexShader = 0;
|
||||||
GLuint mDecodeFragmentShader = 0;
|
GLuint mDecodeFragmentShader = 0;
|
||||||
|
GLint mDecodePackedResolutionLocation = -1;
|
||||||
|
GLint mDecodeDecodedResolutionLocation = -1;
|
||||||
|
GLint mDecodeInputPixelFormatLocation = -1;
|
||||||
GLuint mOutputPackProgram = 0;
|
GLuint mOutputPackProgram = 0;
|
||||||
GLuint mOutputPackVertexShader = 0;
|
GLuint mOutputPackVertexShader = 0;
|
||||||
GLuint mOutputPackFragmentShader = 0;
|
GLuint mOutputPackFragmentShader = 0;
|
||||||
|
GLint mOutputPackResolutionLocation = -1;
|
||||||
|
GLint mOutputPackActiveWordsLocation = -1;
|
||||||
|
GLint mOutputPackFormatLocation = -1;
|
||||||
GLsizeiptr mGlobalParamsUBOSize = 0;
|
GLsizeiptr mGlobalParamsUBOSize = 0;
|
||||||
int mViewWidth = 0;
|
int mViewWidth = 0;
|
||||||
int mViewHeight = 0;
|
int mViewHeight = 0;
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
|
|||||||
|
|
||||||
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
||||||
{
|
{
|
||||||
std::vector<unsigned char> buffer;
|
std::vector<unsigned char>& buffer = mScratchBuffer;
|
||||||
|
buffer.clear();
|
||||||
buffer.reserve(512);
|
buffer.reserve(512);
|
||||||
|
|
||||||
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "OpenGLRenderer.h"
|
#include "OpenGLRenderer.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class GlobalParamsBuffer
|
class GlobalParamsBuffer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -12,4 +14,5 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
|
std::vector<unsigned char> mScratchBuffer;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -841,6 +841,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
|
|||||||
}
|
}
|
||||||
|
|
||||||
reloadRequested = mReloadRequested;
|
reloadRequested = mReloadRequested;
|
||||||
|
if (registryChanged || reloadRequested)
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch (const std::exception& exception)
|
catch (const std::exception& exception)
|
||||||
@@ -884,6 +886,7 @@ bool RuntimeHost::AddLayer(const std::string& shaderId, std::string& error)
|
|||||||
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
||||||
mPersistentState.layers.push_back(layer);
|
mPersistentState.layers.push_back(layer);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,6 +903,7 @@ bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error)
|
|||||||
|
|
||||||
mPersistentState.layers.erase(it);
|
mPersistentState.layers.erase(it);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -921,6 +925,7 @@ bool RuntimeHost::MoveLayer(const std::string& layerId, int direction, std::stri
|
|||||||
|
|
||||||
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
|
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,6 +954,7 @@ bool RuntimeHost::MoveLayerToIndex(const std::string& layerId, std::size_t targe
|
|||||||
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
||||||
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -964,6 +970,7 @@ bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std:
|
|||||||
|
|
||||||
layer->bypass = bypassed;
|
layer->bypass = bypassed;
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,6 +995,7 @@ bool RuntimeHost::SetLayerShader(const std::string& layerId, const std::string&
|
|||||||
layer->parameterValues.clear();
|
layer->parameterValues.clear();
|
||||||
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1024,6 +1032,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,6 +1041,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
layer->parameterValues[parameterId] = normalized;
|
layer->parameterValues[parameterId] = normalized;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1079,6 +1089,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1087,6 +1098,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
matchedLayer->parameterValues[parameterIt->id] = normalized;
|
matchedLayer->parameterValues[parameterIt->id] = normalized;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1110,6 +1122,7 @@ bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string&
|
|||||||
|
|
||||||
layer->parameterValues.clear();
|
layer->parameterValues.clear();
|
||||||
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1169,6 +1182,7 @@ bool RuntimeHost::LoadStackPreset(const std::string& presetName, std::string& er
|
|||||||
|
|
||||||
mPersistentState.layers = nextLayers;
|
mPersistentState.layers = nextLayers;
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,10 +1211,21 @@ bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned he
|
|||||||
|
|
||||||
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||||
{
|
{
|
||||||
|
const bool changed = mHasSignal != hasSignal ||
|
||||||
|
mSignalWidth != width ||
|
||||||
|
mSignalHeight != height ||
|
||||||
|
mSignalModeName != modeName;
|
||||||
mHasSignal = hasSignal;
|
mHasSignal = hasSignal;
|
||||||
mSignalWidth = width;
|
mSignalWidth = width;
|
||||||
mSignalHeight = height;
|
mSignalHeight = height;
|
||||||
mSignalModeName = modeName;
|
mSignalModeName = modeName;
|
||||||
|
if (changed)
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::MarkRenderStateDirtyLocked()
|
||||||
|
{
|
||||||
|
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
@@ -1456,6 +1481,11 @@ bool RuntimeHost::LoadConfig(std::string& error)
|
|||||||
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
|
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
|
||||||
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
||||||
}
|
}
|
||||||
|
if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
|
||||||
|
{
|
||||||
|
const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
|
||||||
|
mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
||||||
|
}
|
||||||
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
|
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
|
||||||
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
|
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
|
||||||
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
||||||
@@ -1674,6 +1704,8 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
|||||||
++it;
|
++it;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkRenderStateDirtyLocked();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1840,6 +1872,7 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
|
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
|
||||||
app.set("autoReload", JsonValue(mAutoReloadEnabled));
|
app.set("autoReload", JsonValue(mAutoReloadEnabled));
|
||||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
||||||
|
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));
|
||||||
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
||||||
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
|
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
|
||||||
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
|
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public:
|
|||||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||||
std::string BuildStateJson() const;
|
std::string BuildStateJson() const;
|
||||||
|
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||||
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
||||||
@@ -64,6 +65,7 @@ public:
|
|||||||
unsigned short GetServerPort() const { return mServerPort; }
|
unsigned short GetServerPort() const { return mServerPort; }
|
||||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
||||||
|
unsigned GetPreviewFps() const { return mConfig.previewFps; }
|
||||||
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
|
||||||
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
|
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
|
||||||
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
|
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
|
||||||
@@ -80,6 +82,7 @@ private:
|
|||||||
unsigned short oscPort = 9000;
|
unsigned short oscPort = 9000;
|
||||||
bool autoReload = true;
|
bool autoReload = true;
|
||||||
unsigned maxTemporalHistoryFrames = 4;
|
unsigned maxTemporalHistoryFrames = 4;
|
||||||
|
unsigned previewFps = 30;
|
||||||
bool enableExternalKeying = false;
|
bool enableExternalKeying = false;
|
||||||
std::string inputVideoFormat = "1080p";
|
std::string inputVideoFormat = "1080p";
|
||||||
std::string inputFrameRate = "59.94";
|
std::string inputFrameRate = "59.94";
|
||||||
@@ -135,6 +138,7 @@ private:
|
|||||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||||
std::string GenerateLayerId();
|
std::string GenerateLayerId();
|
||||||
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
|
void MarkRenderStateDirtyLocked();
|
||||||
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
@@ -179,6 +183,7 @@ private:
|
|||||||
bool mAutoReloadEnabled;
|
bool mAutoReloadEnabled;
|
||||||
std::chrono::steady_clock::time_point mStartTime;
|
std::chrono::steady_clock::time_point mStartTime;
|
||||||
std::chrono::steady_clock::time_point mLastScanTime;
|
std::chrono::steady_clock::time_point mLastScanTime;
|
||||||
std::atomic<uint64_t> mFrameCounter;
|
std::atomic<uint64_t> mFrameCounter{ 0 };
|
||||||
|
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
||||||
uint64_t mNextLayerId;
|
uint64_t mNextLayerId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,5 +8,6 @@
|
|||||||
"outputFrameRate": "59.94",
|
"outputFrameRate": "59.94",
|
||||||
"autoReload": true,
|
"autoReload": true,
|
||||||
"maxTemporalHistoryFrames": 12,
|
"maxTemporalHistoryFrames": 12,
|
||||||
|
"previewFps": 30,
|
||||||
"enableExternalKeying": true
|
"enableExternalKeying": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
|
|||||||
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
|
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
|
||||||
float uvLength = length(uv);
|
float uvLength = length(uv);
|
||||||
|
|
||||||
|
// First warp: convert to polar space and twist the angle more near the
|
||||||
|
// center, creating the large spiral motion.
|
||||||
float speed = spinRotation * spinEase * 0.2;
|
float speed = spinRotation * spinEase * 0.2;
|
||||||
if (isRotate)
|
if (isRotate)
|
||||||
speed = time * speed;
|
speed = time * speed;
|
||||||
@@ -19,6 +21,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
|
|||||||
speed = (time + seed * 17.0) * spinSpeed;
|
speed = (time + seed * 17.0) * spinSpeed;
|
||||||
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
|
||||||
|
|
||||||
|
// Second warp: a short iterative feedback loop turns the spiral into
|
||||||
|
// painterly bands while preserving a fixed compile-time loop bound.
|
||||||
for (int i = 0; i < 5; ++i)
|
for (int i = 0; i < 5; ++i)
|
||||||
{
|
{
|
||||||
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
|
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
|
||||||
@@ -32,6 +36,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
|
|||||||
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
|
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
|
||||||
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
|
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
|
||||||
float c3p = 1.0 - min(1.0, c1p + c2p);
|
float c3p = 1.0 - min(1.0, c1p + c2p);
|
||||||
|
// Three soft band weights drive the palette; lighting rides on the brightest
|
||||||
|
// bands so the swirl keeps dimensional highlights.
|
||||||
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
|
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
|
||||||
|
|
||||||
float safeContrast = max(contrast, 0.001);
|
float safeContrast = max(contrast, 0.001);
|
||||||
|
|||||||
102
shaders/crt-bulge/shader.json
Normal file
102
shaders/crt-bulge/shader.json
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"id": "crt-bulge",
|
||||||
|
"name": "CRT Bulge",
|
||||||
|
"description": "Warps the image like convex CRT glass, with optional rounded screen edges and vignette darkening.",
|
||||||
|
"category": "Distortion",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "bulgeAmount",
|
||||||
|
"label": "Bulge",
|
||||||
|
"type": "float",
|
||||||
|
"default": -0.04,
|
||||||
|
"min": -0.5,
|
||||||
|
"max": 0.8,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Positive values swell the center outward; negative values pinch it inward."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zoom",
|
||||||
|
"label": "Zoom",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.04,
|
||||||
|
"min": 0.5,
|
||||||
|
"max": 2,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Scales the source before distortion, useful for hiding warped edges."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edgeRoundness",
|
||||||
|
"label": "Edge Roundness",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.08,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0.35,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Rounds the visible screen corners like older CRT glass."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edgeFeather",
|
||||||
|
"label": "Edge Feather",
|
||||||
|
"type": "float",
|
||||||
|
"default": 2,
|
||||||
|
"min": 0,
|
||||||
|
"max": 24,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Softens the rounded screen edge in pixels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sourceEdgeFeather",
|
||||||
|
"label": "Source Edge Feather",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.5,
|
||||||
|
"min": 0,
|
||||||
|
"max": 16,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Antialiases warped source edges when the distortion reveals outside-frame pixels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vignetteAmount",
|
||||||
|
"label": "Vignette",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.18,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Darkens the glass toward the screen edges."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edgeMode",
|
||||||
|
"label": "Edge Mode",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "black",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "black",
|
||||||
|
"label": "Black"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "clamp",
|
||||||
|
"label": "Clamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "mirror",
|
||||||
|
"label": "Mirror"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Chooses how warped samples outside the source frame are filled."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "outsideColor",
|
||||||
|
"label": "Outside Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": "Color used outside the curved screen or source frame."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
71
shaders/crt-bulge/shader.slang
Normal file
71
shaders/crt-bulge/shader.slang
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
float mirroredCoordinate(float coordinate)
|
||||||
|
{
|
||||||
|
float wrapped = frac(coordinate * 0.5) * 2.0;
|
||||||
|
return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
float roundedBoxMask(float2 point, float2 halfSize, float radius, float feather)
|
||||||
|
{
|
||||||
|
float2 distanceToEdge = abs(point) - (halfSize - radius);
|
||||||
|
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))) - radius;
|
||||||
|
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
|
||||||
|
float signedDistance = outsideDistance + insideDistance;
|
||||||
|
return 1.0 - smoothstep(0.0, max(feather, 0.00001), signedDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
float sourceBoundsMask(float2 uv, float2 resolution)
|
||||||
|
{
|
||||||
|
float2 pixel = 1.0 / max(resolution, float2(1.0, 1.0));
|
||||||
|
float2 feather = pixel * max(sourceEdgeFeather, 0.0);
|
||||||
|
float left = smoothstep(0.0, max(feather.x, 0.00001), uv.x);
|
||||||
|
float right = 1.0 - smoothstep(1.0 - max(feather.x, 0.00001), 1.0, uv.x);
|
||||||
|
float top = smoothstep(0.0, max(feather.y, 0.00001), uv.y);
|
||||||
|
float bottom = 1.0 - smoothstep(1.0 - max(feather.y, 0.00001), 1.0, uv.y);
|
||||||
|
return saturate(left * right * top * bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 applyBulge(float2 uv, float2 resolution)
|
||||||
|
{
|
||||||
|
float2 centered = uv * 2.0 - 1.0;
|
||||||
|
float aspect = resolution.x / max(resolution.y, 1.0);
|
||||||
|
float2 aspectCentered = float2(centered.x * aspect, centered.y);
|
||||||
|
float radiusSq = dot(aspectCentered, aspectCentered);
|
||||||
|
float amount = clamp(bulgeAmount, -0.95, 0.95);
|
||||||
|
float scale = 1.0 / max(1.0 + amount * radiusSq, 0.05);
|
||||||
|
return centered * scale / max(zoom, 0.001) * 0.5 + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
|
||||||
|
{
|
||||||
|
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||||
|
|
||||||
|
if (edgeMode == 1)
|
||||||
|
return sampleVideo(clamp(uv, 0.0, 1.0));
|
||||||
|
if (edgeMode == 2)
|
||||||
|
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||||
|
|
||||||
|
float edgeMask = sourceBoundsMask(uv, resolution);
|
||||||
|
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
|
||||||
|
return lerp(outsideColor, color, edgeMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
|
float2 sourceUv = applyBulge(context.uv, resolution);
|
||||||
|
|
||||||
|
bool insideSource = false;
|
||||||
|
float4 color = sampleWarped(sourceUv, resolution, insideSource);
|
||||||
|
|
||||||
|
float2 centered = context.uv * 2.0 - 1.0;
|
||||||
|
float feather = max(edgeFeather, 0.0) / min(resolution.x, resolution.y);
|
||||||
|
float screenMask = roundedBoxMask(centered, float2(1.0, 1.0), saturate(edgeRoundness), feather);
|
||||||
|
color = lerp(outsideColor, color, screenMask);
|
||||||
|
|
||||||
|
float2 aspectCentered = float2(centered.x * resolution.x / max(resolution.y, 1.0), centered.y);
|
||||||
|
float edgeDistance = saturate(length(aspectCentered) * 0.72);
|
||||||
|
float vignette = lerp(1.0, 1.0 - saturate(vignetteAmount), smoothstep(0.35, 1.05, edgeDistance));
|
||||||
|
color.rgb *= vignette;
|
||||||
|
|
||||||
|
return saturate(color);
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
|||||||
{
|
{
|
||||||
float safeHalfFov = max(halfFov, 0.0001);
|
float safeHalfFov = max(halfFov, 0.0001);
|
||||||
|
|
||||||
|
// Match common fisheye projection families while keeping the selected FOV
|
||||||
|
// normalized to the same source-image radius.
|
||||||
if (fisheyeModel == 1)
|
if (fisheyeModel == 1)
|
||||||
{
|
{
|
||||||
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
||||||
@@ -49,6 +51,7 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
|||||||
|
|
||||||
float3 equirectangularRay(float2 uv)
|
float3 equirectangularRay(float2 uv)
|
||||||
{
|
{
|
||||||
|
// Convert equirectangular UVs into longitude/latitude on the unit sphere.
|
||||||
float longitude = (uv.x - 0.5) * TWO_PI;
|
float longitude = (uv.x - 0.5) * TWO_PI;
|
||||||
float latitude = (0.5 - uv.y) * PI;
|
float latitude = (0.5 - uv.y) * PI;
|
||||||
float latitudeCos = cos(latitude);
|
float latitudeCos = cos(latitude);
|
||||||
@@ -82,6 +85,8 @@ float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
|
|||||||
float inwardLength = max(length(inward), 0.000001);
|
float inwardLength = max(length(inward), 0.000001);
|
||||||
inward /= inwardLength;
|
inward /= inwardLength;
|
||||||
|
|
||||||
|
// Outside the fisheye image, sample back inward from the nearest edge so the
|
||||||
|
// fill looks like stretched lens content instead of a hard color plate.
|
||||||
float blurDistance = max(edgeBlur, 0.0);
|
float blurDistance = max(edgeBlur, 0.0);
|
||||||
float4 color = sampleVideo(clampedUv) * 0.32;
|
float4 color = sampleVideo(clampedUv) * 0.32;
|
||||||
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
|
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
|
||||||
@@ -114,6 +119,7 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float phi = atan2(ray.y, ray.x);
|
float phi = atan2(ray.y, ray.x);
|
||||||
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
||||||
|
|
||||||
|
// Project the mirrored sphere ray back into the circular fisheye source.
|
||||||
float2 sourceUv = float2(
|
float2 sourceUv = float2(
|
||||||
center.x + cos(phi) * fisheyeRadius * radius.x,
|
center.x + cos(phi) * fisheyeRadius * radius.x,
|
||||||
center.y - sin(phi) * fisheyeRadius * radius.y
|
center.y - sin(phi) * fisheyeRadius * radius.y
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
|||||||
{
|
{
|
||||||
float safeHalfFov = max(halfFov, 0.0001);
|
float safeHalfFov = max(halfFov, 0.0001);
|
||||||
|
|
||||||
|
// Different fisheye lenses map angle to image radius differently. Normalize
|
||||||
|
// each model by the selected half-FOV so the outer lens edge stays at 1.0.
|
||||||
if (fisheyeModel == 1)
|
if (fisheyeModel == 1)
|
||||||
{
|
{
|
||||||
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
|
||||||
@@ -67,6 +69,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0));
|
float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0));
|
||||||
float tanHalfFov = tan(virtualFov * 0.5);
|
float tanHalfFov = tan(virtualFov * 0.5);
|
||||||
|
|
||||||
|
// Build a virtual output-camera ray, then rotate it into the fisheye lens
|
||||||
|
// coordinate system before asking where that ray lands on the source image.
|
||||||
float3 ray = outputProjection == 1
|
float3 ray = outputProjection == 1
|
||||||
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
|
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
|
||||||
: buildRectilinearRay(screen, outputAspect, tanHalfFov);
|
: buildRectilinearRay(screen, outputAspect, tanHalfFov);
|
||||||
@@ -86,6 +90,7 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float phi = atan2(ray.y, ray.x);
|
float phi = atan2(ray.y, ray.x);
|
||||||
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
|
||||||
|
|
||||||
|
// Polar lens coordinates become UVs inside the circular fisheye image.
|
||||||
float2 sourceUv = float2(
|
float2 sourceUv = float2(
|
||||||
center.x + cos(phi) * fisheyeRadius * radius.x,
|
center.x + cos(phi) * fisheyeRadius * radius.x,
|
||||||
center.y - sin(phi) * fisheyeRadius * radius.y
|
center.y - sin(phi) * fisheyeRadius * radius.y
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ float3 matteSampleColor(float2 uv, ShaderContext context)
|
|||||||
if (blur <= 0.0001)
|
if (blur <= 0.0001)
|
||||||
return center;
|
return center;
|
||||||
|
|
||||||
|
// Pre-blur only the color used for screen comparison; the final image keeps
|
||||||
|
// its original detail and alpha is refined in a later pass.
|
||||||
float2 radius = pixel * blur;
|
float2 radius = pixel * blur;
|
||||||
float3 color = center * 0.36;
|
float3 color = center * 0.36;
|
||||||
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
|
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
|
||||||
@@ -37,6 +39,8 @@ float keyDistanceAt(float2 uv, ShaderContext context)
|
|||||||
float3 color = matteSampleColor(uv, context);
|
float3 color = matteSampleColor(uv, context);
|
||||||
float3 keyColor = saturate(screenColor.rgb);
|
float3 keyColor = saturate(screenColor.rgb);
|
||||||
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
|
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
|
||||||
|
// Direction distance is less sensitive to brightness, while chroma distance
|
||||||
|
// follows broadcast-style color difference; screenBalance blends the two.
|
||||||
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
|
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
|
||||||
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
|
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
|
||||||
}
|
}
|
||||||
@@ -65,6 +69,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
|
|||||||
|
|
||||||
if (aaRadius > 0.0001)
|
if (aaRadius > 0.0001)
|
||||||
{
|
{
|
||||||
|
// A small fixed kernel smooths edges and collects min/max alpha for
|
||||||
|
// black/white cleanup without needing dynamic loops or arrays.
|
||||||
float2 radius = pixel * aaRadius;
|
float2 radius = pixel * aaRadius;
|
||||||
float2 halfRadius = radius * 0.5;
|
float2 halfRadius = radius * 0.5;
|
||||||
float alphaMin = centerAlpha;
|
float alphaMin = centerAlpha;
|
||||||
@@ -126,6 +132,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
|
|||||||
alpha = centerAlpha;
|
alpha = centerAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Final matte shaping happens after blur/cleanup so clip and contrast affect
|
||||||
|
// the refined edge rather than the raw screen-distance estimate.
|
||||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||||
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
|
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
|
||||||
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
|
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
|
||||||
@@ -135,6 +143,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
|
|||||||
float spillAmountForColor(float3 color)
|
float spillAmountForColor(float3 color)
|
||||||
{
|
{
|
||||||
float3 keyColor = saturate(screenColor.rgb);
|
float3 keyColor = saturate(screenColor.rgb);
|
||||||
|
// Measure spill as color energy aligned with the screen color minus the
|
||||||
|
// strongest opposing channel, leaving neutral highlights mostly intact.
|
||||||
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
|
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
|
||||||
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
|
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
|
||||||
return saturate(keyComponent - opposingComponent + despillBias);
|
return saturate(keyComponent - opposingComponent + despillBias);
|
||||||
@@ -187,6 +197,8 @@ float4 applyKey(ShaderContext context)
|
|||||||
float cropMask = cropMaskAt(context.uv, context);
|
float cropMask = cropMaskAt(context.uv, context);
|
||||||
alpha *= cropMask;
|
alpha *= cropMask;
|
||||||
|
|
||||||
|
// Edge recovery is strongest around 50% alpha, where fringing usually lives,
|
||||||
|
// and fades away for solid foreground/background pixels.
|
||||||
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
|
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
|
||||||
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
|
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
|
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
|
||||||
float clampedSteps = clamp(raySteps, 1.0, 77.0);
|
float clampedSteps = clamp(raySteps, 1.0, 77.0);
|
||||||
|
|
||||||
|
// Ray-march a folded procedural field. distanceToSurface advances the ray,
|
||||||
|
// while inverse-distance accumulation creates the glowing filaments.
|
||||||
for (int i = 0; i < 77; ++i)
|
for (int i = 0; i < 77; ++i)
|
||||||
{
|
{
|
||||||
if (float(i) >= clampedSteps)
|
if (float(i) >= clampedSteps)
|
||||||
@@ -49,11 +51,14 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
|
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
|
||||||
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
|
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
|
||||||
|
|
||||||
|
// Color comes from pre-fold space so the palette varies smoothly even as
|
||||||
|
// the geometry folds into repeated cells.
|
||||||
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
|
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
|
||||||
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
|
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
|
||||||
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
|
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
|
||||||
|
|
||||||
position = abs(frac(position) - 0.5);
|
position = abs(frac(position) - 0.5);
|
||||||
|
// Distance to a tiny box/cross primitive inside each repeated cell.
|
||||||
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
|
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
|
||||||
accumulated += palette.w * palette / distanceToSurface;
|
accumulated += palette.w * palette / distanceToSurface;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ float3 sampleLutCell(float3 index)
|
|||||||
float g = floor(index.g + 0.5);
|
float g = floor(index.g + 0.5);
|
||||||
float b = floor(index.b + 0.5);
|
float b = floor(index.b + 0.5);
|
||||||
|
|
||||||
|
// The 33^3 cube is packed as blue slices laid horizontally, with red across
|
||||||
|
// each slice and green down the atlas.
|
||||||
float atlasWidth = LUT_SIZE * LUT_SIZE;
|
float atlasWidth = LUT_SIZE * LUT_SIZE;
|
||||||
float2 lutUv;
|
float2 lutUv;
|
||||||
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
|
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
|
||||||
@@ -30,6 +32,9 @@ float3 applyLut33(float3 color)
|
|||||||
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
|
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
|
||||||
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
|
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
|
||||||
|
|
||||||
|
// Tetrahedral interpolation chooses one of six paths through the cube.
|
||||||
|
// This avoids the muddy diagonals that simple trilinear LUT sampling can
|
||||||
|
// introduce for strong grades.
|
||||||
if (blend.r > blend.g)
|
if (blend.r > blend.g)
|
||||||
{
|
{
|
||||||
if (blend.g > blend.b)
|
if (blend.g > blend.b)
|
||||||
@@ -55,6 +60,8 @@ float hash12(float2 value)
|
|||||||
|
|
||||||
float3 outputDither(float2 pixel)
|
float3 outputDither(float2 pixel)
|
||||||
{
|
{
|
||||||
|
// Subtract paired hashes to center the dither around zero, then scale to
|
||||||
|
// roughly one 8-bit code value.
|
||||||
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
|
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
|
||||||
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
|
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
|
||||||
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));
|
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
|
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
|
||||||
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
|
||||||
|
|
||||||
|
// Build a skewed coordinate system around an offset "black hole" so the
|
||||||
|
// waves pinch and stretch instead of staying radially symmetric.
|
||||||
float iterator = 0.2;
|
float iterator = 0.2;
|
||||||
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
|
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
|
||||||
float2 blackholeCenter = p - iterator * diagonal;
|
float2 blackholeCenter = p - iterator * diagonal;
|
||||||
@@ -30,6 +32,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float2 v = singularitySpiral(c, time, iterator);
|
float2 v = singularitySpiral(c, time, iterator);
|
||||||
float2 waves = float2(0.0001, 0.0001);
|
float2 waves = float2(0.0001, 0.0001);
|
||||||
|
|
||||||
|
// Iterative sine feedback creates the accretion texture; the iterator value
|
||||||
|
// also damps later steps to keep the pattern stable.
|
||||||
for (; iterator < 9.0; iterator += 1.0)
|
for (; iterator < 9.0; iterator += 1.0)
|
||||||
{
|
{
|
||||||
waves += 1.0 + sin(v);
|
waves += 1.0 + sin(v);
|
||||||
@@ -40,6 +44,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius;
|
float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius;
|
||||||
float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001);
|
float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001);
|
||||||
float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness;
|
float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness;
|
||||||
|
// Exponential falloff turns the accumulated wave field into bright rims and
|
||||||
|
// a darker center without hard thresholds.
|
||||||
float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift);
|
float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift);
|
||||||
float4 waveColor = waves.xyyx;
|
float4 waveColor = waves.xyyx;
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"id": "vignetteAmount",
|
"id": "vignetteAmount",
|
||||||
"label": "Vignette",
|
"label": "Vignette",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"default": 0.18,
|
"default": 0.3,
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 0.6,
|
"max": 0.6,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
@@ -154,6 +154,46 @@
|
|||||||
"max": 6,
|
"max": 6,
|
||||||
"step": 0.05,
|
"step": 0.05,
|
||||||
"description": "Scale of the generated noise pattern."
|
"description": "Scale of the generated noise pattern."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scanlineAmount",
|
||||||
|
"label": "Scanlines",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.08,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0.35,
|
||||||
|
"step": 0.005,
|
||||||
|
"description": "Subtle alternating-field luma modulation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "chromaCrawlAmount",
|
||||||
|
"label": "Chroma Crawl",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.035,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0.2,
|
||||||
|
"step": 0.005,
|
||||||
|
"description": "Moving color shimmer around high-contrast edges."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "generationLoss",
|
||||||
|
"label": "Generation Loss",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.18,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Raises blacks, softens detail, lowers contrast, and desaturates chroma like copied tape."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sharpnessDrift",
|
||||||
|
"label": "Sharpness Drift",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.12,
|
||||||
|
"min": 0,
|
||||||
|
"max": 0.6,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Slowly varies picture softness to mimic unstable tape focus."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ float2 jumpy(float2 uv, float framecount)
|
|||||||
float2 look = uv;
|
float2 look = uv;
|
||||||
float m = frac(framecount / 4.0);
|
float m = frac(framecount / 4.0);
|
||||||
float dy = look.y - m;
|
float dy = look.y - m;
|
||||||
|
// Localize the horizontal tear to a moving scanline window instead of
|
||||||
|
// bending the whole frame equally.
|
||||||
float window = 1.0 / (1.0 + 80.0 * dy * dy);
|
float window = 1.0 / (1.0 + 80.0 * dy * dy);
|
||||||
look.x += 0.05 * sin(look.y * 10.0 + framecount) / 20.0 * onOff(4.0, 4.0, 0.3, framecount) * (0.5 + cos(framecount * 20.0)) * window;
|
look.x += 0.05 * sin(look.y * 10.0 + framecount) / 20.0 * onOff(4.0, 4.0, 0.3, framecount) * (0.5 + cos(framecount * 20.0)) * window;
|
||||||
float vShift = (0.1 * wiggle) * 0.4 * onOff(2.0, 3.0, 0.9, framecount) * (sin(framecount) * sin(framecount * 20.0) + (0.5 + 0.1 * sin(framecount * 200.0) * cos(framecount)));
|
float vShift = (0.1 * wiggle) * 0.4 * onOff(2.0, 3.0, 0.9, framecount) * (sin(framecount) * sin(framecount * 20.0) + (0.5 + 0.1 * sin(framecount * 200.0) * cos(framecount)));
|
||||||
@@ -44,11 +46,16 @@ float noiseHash(float2 p)
|
|||||||
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gold Noise (c)2015 dcerisano@standard3d.com, adapted for Slang.
|
float staticHash(float2 p)
|
||||||
float goldNoise(float2 xy, float seed)
|
|
||||||
{
|
{
|
||||||
const float phi = 1.61803398874989484820459;
|
float3 p3 = frac(float3(p.x, p.y, p.x) * 0.1031);
|
||||||
return frac(tan(distance(xy * phi, xy) * seed) * xy.x);
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return frac((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float seededStaticHash(float2 p, float seed)
|
||||||
|
{
|
||||||
|
return staticHash(p + float2(seed * 37.13, seed * 17.71));
|
||||||
}
|
}
|
||||||
|
|
||||||
float grainScalar(float2 uv)
|
float grainScalar(float2 uv)
|
||||||
@@ -59,13 +66,17 @@ float grainScalar(float2 uv)
|
|||||||
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
|
||||||
{
|
{
|
||||||
float safeGrainSize = max(grainSize, 0.001);
|
float safeGrainSize = max(grainSize, 0.001);
|
||||||
|
// Quantize the coordinates first so larger grain sizes become visible
|
||||||
|
// chroma blocks rather than simply lower-frequency smooth noise.
|
||||||
float2 baseUv = uv * outputResolution * float2(0.85, 0.95) / safeGrainSize;
|
float2 baseUv = uv * outputResolution * float2(0.85, 0.95) / safeGrainSize;
|
||||||
float2 grainUv = floor(baseUv) + 0.5;
|
float2 grainUv = floor(baseUv) + 0.5;
|
||||||
float2 drift = float2(time * 19.7, time * 23.3);
|
float frame = floor(time * 59.94);
|
||||||
|
|
||||||
float r = grainScalar(grainUv + drift + float2(13.1, 71.7));
|
// Change the grain field per frame instead of drifting it through UV space;
|
||||||
float g = grainScalar(grainUv * float2(1.03, 0.97) + drift * 1.11 + float2(47.2, 19.4));
|
// continuous drift can alias into horizontal bands that march down-frame.
|
||||||
float b = grainScalar(grainUv * float2(0.96, 1.05) + drift * 0.91 + float2(83.6, 53.8));
|
float r = staticHash(grainUv + float2(frame * 17.0 + 13.1, frame * 3.0 + 71.7));
|
||||||
|
float g = staticHash(grainUv * float2(1.03, 0.97) + float2(frame * 11.0 + 47.2, frame * 5.0 + 19.4));
|
||||||
|
float b = staticHash(grainUv * float2(0.96, 1.05) + float2(frame * 7.0 + 83.6, frame * 13.0 + 53.8));
|
||||||
|
|
||||||
return float3(r, g, b) * 2.0 - 1.0;
|
return float3(r, g, b) * 2.0 - 1.0;
|
||||||
}
|
}
|
||||||
@@ -87,6 +98,8 @@ float valueNoise2(float2 p)
|
|||||||
float tapeLineNoise(float2 uv, float time, float2 outputResolution)
|
float tapeLineNoise(float2 uv, float time, float2 outputResolution)
|
||||||
{
|
{
|
||||||
float y = floor(uv.y * outputResolution.y);
|
float y = floor(uv.y * outputResolution.y);
|
||||||
|
// Combine stable per-line noise with frame-rate noise so bands have both
|
||||||
|
// slow tape wander and fast electronic shimmer.
|
||||||
float slowLine = valueNoise2(float2(y * 0.021, floor(time * 10.0)));
|
float slowLine = valueNoise2(float2(y * 0.021, floor(time * 10.0)));
|
||||||
float fastLine = noiseHash(float2(y * 1.73, floor(time * 59.94)));
|
float fastLine = noiseHash(float2(y * 1.73, floor(time * 59.94)));
|
||||||
float line = (slowLine * 0.7 + fastLine * 0.3) * 2.0 - 1.0;
|
float line = (slowLine * 0.7 + fastLine * 0.3) * 2.0 - 1.0;
|
||||||
@@ -102,16 +115,19 @@ float3 analogStatic(float2 uv, float time, float2 outputResolution)
|
|||||||
float frame = floor(time * 59.94);
|
float frame = floor(time * 59.94);
|
||||||
float seed = frac(time);
|
float seed = frac(time);
|
||||||
|
|
||||||
|
// Several differently skewed hashes keep the snow from forming obvious
|
||||||
|
// diagonal or grid patterns at broadcast frame cadence.
|
||||||
float2 goldPixel = pixel + float2(0.37, 0.61) + frame;
|
float2 goldPixel = pixel + float2(0.37, 0.61) + frame;
|
||||||
float snowA = goldNoise(goldPixel, seed + 0.1);
|
float snowA = seededStaticHash(goldPixel, seed + 0.1);
|
||||||
float snowB = goldNoise(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2);
|
float snowB = seededStaticHash(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2);
|
||||||
float snowC = goldNoise(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
|
float snowC = seededStaticHash(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
|
||||||
float snow = (snowA * 0.72 + snowB * 0.28) * 2.0 - 1.0;
|
float snow = (snowA * 0.72 + snowB * 0.28) * 2.0 - 1.0;
|
||||||
|
|
||||||
float lineNoise = tapeLineNoise(uv, time, safeResolution);
|
float lineNoise = tapeLineNoise(uv, time, safeResolution);
|
||||||
float dropoutSeed = goldNoise(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
|
float dropoutSeed = seededStaticHash(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
|
||||||
float dropout = smoothstep(0.965, 1.0, dropoutSeed);
|
float dropout = smoothstep(0.965, 1.0, dropoutSeed);
|
||||||
float fleck = smoothstep(0.988, 1.0, snowA) - smoothstep(0.0, 0.012, snowC);
|
float fleckSeed = seededStaticHash(pixel + float2(frame * 13.0, -frame * 7.0), seed + 0.5);
|
||||||
|
float fleck = smoothstep(0.992, 1.0, fleckSeed) - smoothstep(0.0, 0.008, snowC);
|
||||||
|
|
||||||
float scan = sin(uv.y * safeResolution.y * 3.14159265);
|
float scan = sin(uv.y * safeResolution.y * 3.14159265);
|
||||||
float scanMask = 0.55 + 0.45 * scan * scan;
|
float scanMask = 0.55 + 0.45 * scan * scan;
|
||||||
@@ -138,6 +154,85 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
|
|||||||
return sum;
|
return sum;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
|
||||||
|
{
|
||||||
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
||||||
|
float2 offset = pixel * radius;
|
||||||
|
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
|
||||||
|
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
|
||||||
|
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
|
||||||
|
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
|
||||||
|
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolution)
|
||||||
|
{
|
||||||
|
float amount = saturate(chromaCrawlAmount);
|
||||||
|
if (amount <= 0.0001)
|
||||||
|
return color;
|
||||||
|
|
||||||
|
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
|
||||||
|
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
|
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
|
||||||
|
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
|
||||||
|
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
|
||||||
|
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
|
||||||
|
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
|
||||||
|
|
||||||
|
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
|
||||||
|
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
|
||||||
|
float3 crawled = color;
|
||||||
|
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
|
||||||
|
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
|
||||||
|
return crawled;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applyGenerationLoss(float3 color, float2 uv, float2 outputResolution)
|
||||||
|
{
|
||||||
|
float loss = saturate(generationLoss);
|
||||||
|
if (loss <= 0.0001)
|
||||||
|
return color;
|
||||||
|
|
||||||
|
float3 softened = softCrossBlur(uv, outputResolution, 0.85 + loss * 2.2);
|
||||||
|
color = lerp(color, softened, loss * 0.42);
|
||||||
|
|
||||||
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
|
float3 gray = float3(luma, luma, luma);
|
||||||
|
color = lerp(color, gray, loss * 0.32);
|
||||||
|
color = (color - 0.5) * (1.0 - loss * 0.18) + 0.5;
|
||||||
|
color = color * (1.0 - loss * 0.08) + float3(0.035, 0.035, 0.04) * loss;
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applySharpnessDrift(float3 color, float2 uv, float time, float2 outputResolution)
|
||||||
|
{
|
||||||
|
float drift = saturate(sharpnessDrift);
|
||||||
|
if (drift <= 0.0001)
|
||||||
|
return color;
|
||||||
|
|
||||||
|
float wobble = 0.5 + 0.5 * sin(time * 1.7 + sin(time * 0.37) * 2.0);
|
||||||
|
float radius = 0.35 + wobble * 2.25;
|
||||||
|
float3 softened = softCrossBlur(uv, outputResolution, radius);
|
||||||
|
return lerp(color, softened, drift * (0.35 + 0.65 * wobble));
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applySubtleScanlines(float3 color, float2 uv, float time, float2 outputResolution)
|
||||||
|
{
|
||||||
|
float amount = saturate(scanlineAmount);
|
||||||
|
if (amount <= 0.0001)
|
||||||
|
return color;
|
||||||
|
|
||||||
|
float scan = sin((uv.y * outputResolution.y + floor(time * 59.94) * 0.5) * 3.14159265);
|
||||||
|
float field = 0.5 + 0.5 * scan;
|
||||||
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
|
float visibility = lerp(1.0, 0.45, saturate(luma));
|
||||||
|
float modulation = 1.0 - amount * visibility * (0.35 + 0.65 * field);
|
||||||
|
color.rgb *= modulation;
|
||||||
|
color.rgb += amount * 0.015 * (1.0 - field);
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
|
||||||
float3 blurVhs(float2 uv, float d, int sampleCount)
|
float3 blurVhs(float2 uv, float d, int sampleCount)
|
||||||
{
|
{
|
||||||
float3 sum = float3(0.0, 0.0, 0.0);
|
float3 sum = float3(0.0, 0.0, 0.0);
|
||||||
@@ -146,6 +241,8 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
|
|||||||
float2 pixelOffset = float2(d, 0.0);
|
float2 pixelOffset = float2(d, 0.0);
|
||||||
float2 scale = 0.66 * 8.0 * pixelOffset;
|
float2 scale = 0.66 * 8.0 * pixelOffset;
|
||||||
|
|
||||||
|
// The circular tap pattern approximates soft tape smear while keeping the
|
||||||
|
// maximum loop bound fixed for shader compilation.
|
||||||
for (int i = 0; i < 15; ++i)
|
for (int i = 0; i < 15; ++i)
|
||||||
{
|
{
|
||||||
if (i >= sampleCount)
|
if (i >= sampleCount)
|
||||||
@@ -170,6 +267,8 @@ float4 buildTapeSmear(ShaderContext context)
|
|||||||
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
|
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
|
||||||
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
|
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
|
||||||
|
|
||||||
|
// Split the source into YIQ, smear each component by a different amount,
|
||||||
|
// then recombine to mimic luma/chroma bandwidth mismatch on tape.
|
||||||
float d = 0.1 - round(frac(time / 3.0)) * 0.1;
|
float d = 0.1 - round(frac(time / 3.0)) * 0.1;
|
||||||
uv = jumpy(uv, framecount);
|
uv = jumpy(uv, framecount);
|
||||||
float s = 0.0001 * -d + 0.0001 * wiggle * sin(time * wiggleSpeed);
|
float s = 0.0001 * -d + 0.0001 * wiggle * sin(time * wiggleSpeed);
|
||||||
@@ -202,6 +301,8 @@ float4 finishVhs(ShaderContext context)
|
|||||||
float time = distortedTapeTime(context);
|
float time = distortedTapeTime(context);
|
||||||
float3 color = sampleVideo(context.uv).rgb;
|
float3 color = sampleVideo(context.uv).rgb;
|
||||||
|
|
||||||
|
// Radial red/blue offsets create lens and deck misregistration before the
|
||||||
|
// wider tape effects are layered in.
|
||||||
float2 centered = context.uv * 2.0 - 1.0;
|
float2 centered = context.uv * 2.0 - 1.0;
|
||||||
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
||||||
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
|
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
|
||||||
@@ -219,16 +320,24 @@ float4 finishVhs(ShaderContext context)
|
|||||||
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
||||||
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
||||||
|
|
||||||
|
// Bloom and fade are applied as separate layers so highlights glow without
|
||||||
|
// flattening the full picture into the faded black level.
|
||||||
float3 bloomSource = softBloom(context.uv, context.outputResolution, 2.0 + smear * 2.5);
|
float3 bloomSource = softBloom(context.uv, context.outputResolution, 2.0 + smear * 2.5);
|
||||||
float bloomLuma = dot(bloomSource, float3(0.299, 0.587, 0.114));
|
float bloomLuma = dot(bloomSource, float3(0.299, 0.587, 0.114));
|
||||||
float bloomMask = smoothstep(0.32, 1.0, bloomLuma) * bloomAmount;
|
float bloomMask = smoothstep(0.32, 1.0, bloomLuma) * bloomAmount;
|
||||||
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
color = lerp(color, bloomSource, bloomAmount * 0.18);
|
||||||
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
|
||||||
|
|
||||||
|
color = applySharpnessDrift(color, context.uv, time, context.outputResolution);
|
||||||
|
color = applyGenerationLoss(color, context.uv, context.outputResolution);
|
||||||
|
color = applyChromaCrawl(color, context.uv, time, context.outputResolution);
|
||||||
|
|
||||||
float3 speckle = animatedChromaGrain(context.uv, time, context.outputResolution, noiseSize);
|
float3 speckle = animatedChromaGrain(context.uv, time, context.outputResolution, noiseSize);
|
||||||
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
float luma = dot(color, float3(0.299, 0.587, 0.114));
|
||||||
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
|
||||||
float chunkiness = lerp(1.0, 2.4, saturate((noiseSize - 1.0) / 5.0));
|
float chunkiness = lerp(1.0, 2.4, saturate((noiseSize - 1.0) / 5.0));
|
||||||
|
// Push darker regions harder: analog noise reads most naturally in shadows
|
||||||
|
// and avoids washing out bright highlights.
|
||||||
float3 chromaNoise = float3(speckle.x * 1.2, speckle.y * 0.28, speckle.z * 1.35);
|
float3 chromaNoise = float3(speckle.x * 1.2, speckle.y * 0.28, speckle.z * 1.35);
|
||||||
color += chromaNoise * noiseAmount * noiseMask * chunkiness;
|
color += chromaNoise * noiseAmount * noiseMask * chunkiness;
|
||||||
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.2 * chunkiness, 0.35);
|
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.2 * chunkiness, 0.35);
|
||||||
@@ -244,6 +353,8 @@ float4 finishVhs(ShaderContext context)
|
|||||||
color = color * (1.0 - fadeAmount * 0.08) + float3(0.055, 0.055, 0.065) * fadeAmount;
|
color = color * (1.0 - fadeAmount * 0.08) + float3(0.055, 0.055, 0.065) * fadeAmount;
|
||||||
color = lerp(color, softBloom(context.uv, context.outputResolution, 1.0 + smear), fadeAmount * 0.12);
|
color = lerp(color, softBloom(context.uv, context.outputResolution, 1.0 + smear), fadeAmount * 0.12);
|
||||||
|
|
||||||
|
color = applySubtleScanlines(color, context.uv, time, context.outputResolution);
|
||||||
|
|
||||||
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
|
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
|
||||||
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
|
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
|
||||||
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
|
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ bool intersectCube(float3 rayOrigin, float3 rayDirection, float halfExtent, out
|
|||||||
float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent);
|
float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent);
|
||||||
float3 boxMax = float3(halfExtent, halfExtent, halfExtent);
|
float3 boxMax = float3(halfExtent, halfExtent, halfExtent);
|
||||||
|
|
||||||
|
// Slab intersection: find the ray interval that overlaps all three box
|
||||||
|
// axes, then keep the nearest positive hit.
|
||||||
float3 invDir = 1.0 / rayDirection;
|
float3 invDir = 1.0 / rayDirection;
|
||||||
float3 t0 = (boxMin - rayOrigin) * invDir;
|
float3 t0 = (boxMin - rayOrigin) * invDir;
|
||||||
float3 t1 = (boxMax - rayOrigin) * invDir;
|
float3 t1 = (boxMax - rayOrigin) * invDir;
|
||||||
@@ -43,6 +45,8 @@ float2 cubeFaceUv(float3 hitPoint, float halfExtent, float zoom)
|
|||||||
float2 uv = float2(0.5, 0.5);
|
float2 uv = float2(0.5, 0.5);
|
||||||
float safeZoom = max(zoom, 0.001);
|
float safeZoom = max(zoom, 0.001);
|
||||||
|
|
||||||
|
// The dominant coordinate tells which face was hit; the other two axes
|
||||||
|
// become that face's local UVs.
|
||||||
if (face.x >= face.y && face.x >= face.z)
|
if (face.x >= face.y && face.x >= face.z)
|
||||||
{
|
{
|
||||||
uv = hitPoint.x > 0.0
|
uv = hitPoint.x > 0.0
|
||||||
@@ -79,6 +83,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float yaw = spin;
|
float yaw = spin;
|
||||||
float pitch = spin * 0.61 + 0.35;
|
float pitch = spin * 0.61 + 0.35;
|
||||||
|
|
||||||
|
// Rotate the camera ray into cube-local space instead of rotating the cube
|
||||||
|
// geometry, which keeps the intersection math axis-aligned.
|
||||||
float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw);
|
float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw);
|
||||||
float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw);
|
float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw);
|
||||||
|
|
||||||
@@ -96,6 +102,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
|
|
||||||
float3 normal;
|
float3 normal;
|
||||||
float3 face = abs(localHit);
|
float3 face = abs(localHit);
|
||||||
|
// Reconstruct the face normal from the hit point so lighting follows the
|
||||||
|
// same face choice used for UV lookup.
|
||||||
if (face.x >= face.y && face.x >= face.z)
|
if (face.x >= face.y && face.x >= face.z)
|
||||||
normal = float3(sign(localHit.x), 0.0, 0.0);
|
normal = float3(sign(localHit.x), 0.0, 0.0);
|
||||||
else if (face.y >= face.x && face.y >= face.z)
|
else if (face.y >= face.x && face.y >= face.z)
|
||||||
|
|||||||
121
shaders/video-plane-3d/shader.json
Normal file
121
shaders/video-plane-3d/shader.json
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{
|
||||||
|
"id": "video-plane-3d",
|
||||||
|
"name": "Video Plane 3D",
|
||||||
|
"description": "Places the video on a perspective 2D plane in 3D space with camera FOV, XYZ position, and pan/tilt/roll controls.",
|
||||||
|
"category": "Projection",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "fovDegrees",
|
||||||
|
"label": "FOV",
|
||||||
|
"type": "float",
|
||||||
|
"default": 45,
|
||||||
|
"min": 5,
|
||||||
|
"max": 150,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Virtual camera vertical field of view in degrees."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "positionX",
|
||||||
|
"label": "X",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": -4,
|
||||||
|
"max": 4,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Horizontal plane position in world units."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "positionY",
|
||||||
|
"label": "Y",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": -4,
|
||||||
|
"max": 4,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Vertical plane position in world units."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "positionZ",
|
||||||
|
"label": "Z",
|
||||||
|
"type": "float",
|
||||||
|
"default": 2.2,
|
||||||
|
"min": 0.1,
|
||||||
|
"max": 10,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Depth of the plane in front of the virtual camera."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "panDegrees",
|
||||||
|
"label": "Pan",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": -180,
|
||||||
|
"max": 180,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Rotates the plane left/right around its vertical axis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tiltDegrees",
|
||||||
|
"label": "Tilt",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": -120,
|
||||||
|
"max": 120,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Rotates the plane up/down around its horizontal axis."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rollDegrees",
|
||||||
|
"label": "Roll",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": -180,
|
||||||
|
"max": 180,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Rotates the plane around its face normal."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "planeScale",
|
||||||
|
"label": "Plane Scale",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.4,
|
||||||
|
"min": 0.05,
|
||||||
|
"max": 6,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Height of the video plane in world units; width follows the source aspect ratio."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "edgeFeather",
|
||||||
|
"label": "Edge Feather",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.5,
|
||||||
|
"min": 0,
|
||||||
|
"max": 24,
|
||||||
|
"step": 0.1,
|
||||||
|
"description": "Softens the plane edge in source pixels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "backgroundMix",
|
||||||
|
"label": "Background Mix",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0,
|
||||||
|
"min": 0,
|
||||||
|
"max": 1,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Mixes the original video behind the projected plane."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "outsideColor",
|
||||||
|
"label": "Outside Color",
|
||||||
|
"type": "color",
|
||||||
|
"default": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"description": "Color used where the camera ray misses the plane."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
84
shaders/video-plane-3d/shader.slang
Normal file
84
shaders/video-plane-3d/shader.slang
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
static const float PI = 3.14159265358979323846;
|
||||||
|
|
||||||
|
float radiansFromDegrees(float degrees)
|
||||||
|
{
|
||||||
|
return degrees * (PI / 180.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rotateX(float3 p, float angle)
|
||||||
|
{
|
||||||
|
float s = sin(angle);
|
||||||
|
float c = cos(angle);
|
||||||
|
return float3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rotateY(float3 p, float angle)
|
||||||
|
{
|
||||||
|
float s = sin(angle);
|
||||||
|
float c = cos(angle);
|
||||||
|
return float3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rotateZ(float3 p, float angle)
|
||||||
|
{
|
||||||
|
float s = sin(angle);
|
||||||
|
float c = cos(angle);
|
||||||
|
return float3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rotateWorldToPlane(float3 value)
|
||||||
|
{
|
||||||
|
float pan = radiansFromDegrees(panDegrees);
|
||||||
|
float tilt = radiansFromDegrees(tiltDegrees);
|
||||||
|
float roll = radiansFromDegrees(rollDegrees);
|
||||||
|
return rotateZ(rotateX(rotateY(value, -pan), -tilt), -roll);
|
||||||
|
}
|
||||||
|
|
||||||
|
float planeEdgeMask(float2 uv, float2 inputResolution)
|
||||||
|
{
|
||||||
|
float2 feather = max(edgeFeather, 0.0) / max(inputResolution, float2(1.0, 1.0));
|
||||||
|
feather = max(feather, float2(0.00001, 0.00001));
|
||||||
|
|
||||||
|
float left = smoothstep(0.0, feather.x, uv.x);
|
||||||
|
float right = 1.0 - smoothstep(1.0 - feather.x, 1.0, uv.x);
|
||||||
|
float top = smoothstep(0.0, feather.y, uv.y);
|
||||||
|
float bottom = 1.0 - smoothstep(1.0 - feather.y, 1.0, uv.y);
|
||||||
|
return saturate(left * right * top * bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 outputResolution = max(context.outputResolution, float2(1.0, 1.0));
|
||||||
|
float outputAspect = outputResolution.x / outputResolution.y;
|
||||||
|
float sourceAspect = context.inputResolution.x / max(context.inputResolution.y, 1.0);
|
||||||
|
float tanHalfFov = tan(radiansFromDegrees(clamp(fovDegrees, 5.0, 150.0)) * 0.5);
|
||||||
|
|
||||||
|
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
|
||||||
|
float3 rayOrigin = float3(0.0, 0.0, 0.0);
|
||||||
|
float3 rayDirection = normalize(float3(screen.x * outputAspect * tanHalfFov, screen.y * tanHalfFov, 1.0));
|
||||||
|
|
||||||
|
float3 planePosition = float3(positionX, positionY, max(positionZ, 0.001));
|
||||||
|
float3 localOrigin = rotateWorldToPlane(rayOrigin - planePosition);
|
||||||
|
float3 localDirection = rotateWorldToPlane(rayDirection);
|
||||||
|
|
||||||
|
float backgroundAmount = saturate(backgroundMix);
|
||||||
|
float4 background = float4(lerp(outsideColor.rgb, context.sourceColor.rgb, backgroundAmount), 1.0);
|
||||||
|
if (abs(localDirection.z) < 0.00001)
|
||||||
|
return background;
|
||||||
|
|
||||||
|
float hitDistance = -localOrigin.z / localDirection.z;
|
||||||
|
if (hitDistance <= 0.0)
|
||||||
|
return background;
|
||||||
|
|
||||||
|
float3 localHit = localOrigin + localDirection * hitDistance;
|
||||||
|
float halfHeight = max(planeScale, 0.001) * 0.5;
|
||||||
|
float halfWidth = halfHeight * sourceAspect;
|
||||||
|
float2 planeUv = float2(
|
||||||
|
localHit.x / max(halfWidth * 2.0, 0.0001) + 0.5,
|
||||||
|
0.5 - localHit.y / max(halfHeight * 2.0, 0.0001)
|
||||||
|
);
|
||||||
|
|
||||||
|
float mask = planeEdgeMask(planeUv, max(context.inputResolution, float2(1.0, 1.0)));
|
||||||
|
float4 planeColor = sampleVideo(clamp(planeUv, 0.0, 1.0));
|
||||||
|
return saturate(lerp(background, planeColor, mask));
|
||||||
|
}
|
||||||
@@ -47,6 +47,35 @@
|
|||||||
"step": 0.1,
|
"step": 0.1,
|
||||||
"description": "Rotates the source image around the frame center."
|
"description": "Rotates the source image around the frame center."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "cropAspect",
|
||||||
|
"label": "Crop Aspect",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "none",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "none",
|
||||||
|
"label": "None"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "4x3",
|
||||||
|
"label": "4:3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "3x2",
|
||||||
|
"label": "3:2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "1x1",
|
||||||
|
"label": "1:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "9x16",
|
||||||
|
"label": "9:16"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Crops the visible image to a centered preset aspect ratio without squeezing the source."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "edgeMode",
|
"id": "edgeMode",
|
||||||
"label": "Edge Mode",
|
"label": "Edge Mode",
|
||||||
|
|||||||
@@ -28,8 +28,42 @@ float2 applyEdgeMode(float2 uv, out bool inside)
|
|||||||
return uv;
|
return uv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float selectedCropAspect()
|
||||||
|
{
|
||||||
|
if (cropAspect == 1)
|
||||||
|
return 4.0 / 3.0;
|
||||||
|
if (cropAspect == 2)
|
||||||
|
return 3.0 / 2.0;
|
||||||
|
if (cropAspect == 3)
|
||||||
|
return 1.0;
|
||||||
|
if (cropAspect == 4)
|
||||||
|
return 9.0 / 16.0;
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool insideCropWindow(float2 uv, float2 resolution)
|
||||||
|
{
|
||||||
|
float targetAspect = selectedCropAspect();
|
||||||
|
if (targetAspect <= 0.0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
float outputAspect = resolution.x / max(resolution.y, 1.0);
|
||||||
|
float2 cropSize = float2(1.0, 1.0);
|
||||||
|
if (outputAspect > targetAspect)
|
||||||
|
cropSize.x = targetAspect / outputAspect;
|
||||||
|
else
|
||||||
|
cropSize.y = outputAspect / targetAspect;
|
||||||
|
|
||||||
|
float2 cropMin = (1.0 - cropSize) * 0.5;
|
||||||
|
float2 cropMax = cropMin + cropSize;
|
||||||
|
return uv.x >= cropMin.x && uv.x <= cropMax.x && uv.y >= cropMin.y && uv.y <= cropMax.y;
|
||||||
|
}
|
||||||
|
|
||||||
float4 shadeVideo(ShaderContext context)
|
float4 shadeVideo(ShaderContext context)
|
||||||
{
|
{
|
||||||
|
if (!insideCropWindow(context.uv, max(context.outputResolution, float2(1.0, 1.0))))
|
||||||
|
return outsideColor;
|
||||||
|
|
||||||
float safeZoom = max(zoom, 0.001);
|
float safeZoom = max(zoom, 0.001);
|
||||||
float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5;
|
float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5;
|
||||||
sourceUv -= pan;
|
sourceUv -= pan;
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float resolutionAspect = max(context.outputResolution.x, 1.0) / max(context.outputResolution.y, 1.0);
|
float resolutionAspect = max(context.outputResolution.x, 1.0) / max(context.outputResolution.y, 1.0);
|
||||||
float width = saturate(overlayScale);
|
float width = saturate(overlayScale);
|
||||||
float height = width * resolutionAspect / targetAspect;
|
float height = width * resolutionAspect / targetAspect;
|
||||||
|
// Keep the scope in a 16:9 frame, then shrink it if the requested scale
|
||||||
|
// would push the overlay beyond the screen bounds.
|
||||||
float fitScale = min(1.0 / max(width, 0.001), 1.0 / max(height, 0.001));
|
float fitScale = min(1.0 / max(width, 0.001), 1.0 / max(height, 0.001));
|
||||||
width *= min(fitScale, 1.0);
|
width *= min(fitScale, 1.0);
|
||||||
height *= min(fitScale, 1.0);
|
height *= min(fitScale, 1.0);
|
||||||
@@ -36,6 +38,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
|
|
||||||
float3 bg = lerp(color.rgb, float3(0.0, 0.0, 0.0), saturate(backgroundOpacity));
|
float3 bg = lerp(color.rgb, float3(0.0, 0.0, 0.0), saturate(backgroundOpacity));
|
||||||
float labelHeight = min(max(pad.x * 0.95, 0.048), 0.12);
|
float labelHeight = min(max(pad.x * 0.95, 0.048), 0.12);
|
||||||
|
// Label textures are authored in UV space, so compensate for the overlay
|
||||||
|
// and output aspect ratios to keep the glyphs from stretching.
|
||||||
float labelWidth = labelHeight * height * max(context.outputResolution.y, 1.0) / max(width * max(context.outputResolution.x, 1.0), 0.001);
|
float labelWidth = labelHeight * height * max(context.outputResolution.y, 1.0) / max(width * max(context.outputResolution.x, 1.0), 0.001);
|
||||||
float labelX = max(pad.x * 0.5, labelWidth * 0.55);
|
float labelX = max(pad.x * 0.5, labelWidth * 0.55);
|
||||||
float y0 = pad.y;
|
float y0 = pad.y;
|
||||||
@@ -63,6 +67,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
float requestedSamples = clamp(waveformSamples, 1.0, 96.0);
|
float requestedSamples = clamp(waveformSamples, 1.0, 96.0);
|
||||||
float density = 0.0;
|
float density = 0.0;
|
||||||
|
|
||||||
|
// For each output pixel, march through source rows at the same X coordinate
|
||||||
|
// and accumulate hits where sampled luma lands near this pixel's Y level.
|
||||||
for (int sampleIndex = 0; sampleIndex < 96; sampleIndex++)
|
for (int sampleIndex = 0; sampleIndex < 96; sampleIndex++)
|
||||||
{
|
{
|
||||||
float samplePosition = float(sampleIndex);
|
float samplePosition = float(sampleIndex);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
float boxMask(float2 point, float2 halfSize, float feather)
|
float boxMask(float2 point, float2 halfSize, float feather)
|
||||||
{
|
{
|
||||||
|
// Signed-distance box mask gives the chart and border pixel-sized feathered
|
||||||
|
// edges without branching per side.
|
||||||
float2 distanceToEdge = abs(point) - halfSize;
|
float2 distanceToEdge = abs(point) - halfSize;
|
||||||
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0)));
|
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0)));
|
||||||
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
|
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
|
||||||
@@ -31,6 +33,8 @@ float applyToneCurve(float linearLevel)
|
|||||||
float patchBrightness(int patchIndex, int count)
|
float patchBrightness(int patchIndex, int count)
|
||||||
{
|
{
|
||||||
int clampedIndex = clamp(patchIndex, 0, max(count - 1, 0));
|
int clampedIndex = clamp(patchIndex, 0, max(count - 1, 0));
|
||||||
|
// Each patch is one stop brighter than the previous patch until it clips at
|
||||||
|
// the requested peak level, matching the Xyla-style exposure ramp.
|
||||||
float linearLevel = baseLevel * exp2(float(clampedIndex));
|
float linearLevel = baseLevel * exp2(float(clampedIndex));
|
||||||
linearLevel = min(linearLevel, peakLevel);
|
linearLevel = min(linearLevel, peakLevel);
|
||||||
return applyToneCurve(linearLevel);
|
return applyToneCurve(linearLevel);
|
||||||
@@ -60,6 +64,8 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
if (reverseOrder)
|
if (reverseOrder)
|
||||||
patchIndex = count - 1 - patchIndex;
|
patchIndex = count - 1 - patchIndex;
|
||||||
|
|
||||||
|
// Build each patch as a slot along the main axis, then mask the cross-axis
|
||||||
|
// extents so vertical and horizontal charts share the same logic.
|
||||||
float patchSlotCenter = (floor(patchPosition) + 0.5) / float(count);
|
float patchSlotCenter = (floor(patchPosition) + 0.5) / float(count);
|
||||||
float localAxis = abs(normalizedAxis - patchSlotCenter) * float(count) * 2.0;
|
float localAxis = abs(normalizedAxis - patchSlotCenter) * float(count) * 2.0;
|
||||||
float safeGapSize = saturate(gapSize);
|
float safeGapSize = saturate(gapSize);
|
||||||
|
|||||||
Reference in New Issue
Block a user