Working
This commit is contained in:
@@ -26,7 +26,7 @@ Supported parameter types:
|
|||||||
|
|
||||||
## Slang contract
|
## Slang contract
|
||||||
|
|
||||||
The runtime owns the fragment entry point, video decode, and final mix/bypass behavior.
|
The runtime owns the fragment entry point, the UYVY-to-RGBA decode pass, and final mix/bypass behavior.
|
||||||
|
|
||||||
Your `shader.slang` file implements:
|
Your `shader.slang` file implements:
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ float4 shadeVideo(ShaderContext context)
|
|||||||
Available built-ins through `ShaderContext`:
|
Available built-ins through `ShaderContext`:
|
||||||
|
|
||||||
- `uv`
|
- `uv`
|
||||||
- `sourceColor`
|
- `sourceColor` - the already-decoded full-resolution RGBA video color at `uv`
|
||||||
- `inputResolution`
|
- `inputResolution`
|
||||||
- `outputResolution`
|
- `outputResolution`
|
||||||
- `time`
|
- `time`
|
||||||
@@ -52,4 +52,4 @@ Manifest parameters are exposed to the shader as globals named by their `id`.
|
|||||||
|
|
||||||
Helper function:
|
Helper function:
|
||||||
|
|
||||||
- `sampleVideo(float2 uv)` returns decoded RGBA video from the live DeckLink input.
|
- `sampleVideo(float2 uv)` returns decoded full-resolution RGBA video from the live DeckLink input.
|
||||||
|
|||||||
@@ -247,6 +247,12 @@ bool ControlServer::HandleHttpRequest(SOCKET clientSocket, const std::string& re
|
|||||||
if (shaderId && parameterId && value && mCallbacks.updateParameter)
|
if (shaderId && parameterId && value && mCallbacks.updateParameter)
|
||||||
success = mCallbacks.updateParameter(shaderId->asString(), parameterId->asString(), SerializeJson(*value, false), actionError);
|
success = mCallbacks.updateParameter(shaderId->asString(), parameterId->asString(), SerializeJson(*value, false), actionError);
|
||||||
}
|
}
|
||||||
|
else if (path == "/api/reset-parameters")
|
||||||
|
{
|
||||||
|
const JsonValue* shaderId = root.find("shaderId");
|
||||||
|
if (shaderId && mCallbacks.resetParameters)
|
||||||
|
success = mCallbacks.resetParameters(shaderId->asString(), actionError);
|
||||||
|
}
|
||||||
else if (path == "/api/set-bypass")
|
else if (path == "/api/set-bypass")
|
||||||
{
|
{
|
||||||
const JsonValue* bypass = root.find("bypass");
|
const JsonValue* bypass = root.find("bypass");
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
std::function<std::string()> getStateJson;
|
std::function<std::string()> getStateJson;
|
||||||
std::function<bool(const std::string&, std::string&)> selectShader;
|
std::function<bool(const std::string&, std::string&)> selectShader;
|
||||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
||||||
|
std::function<bool(const std::string&, std::string&)> resetParameters;
|
||||||
std::function<bool(bool, std::string&)> setBypass;
|
std::function<bool(bool, std::string&)> setBypass;
|
||||||
std::function<bool(double, std::string&)> setMixAmount;
|
std::function<bool(double, std::string&)> setMixAmount;
|
||||||
std::function<bool(std::string&)> reloadShader;
|
std::function<bool(std::string&)> reloadShader;
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
static HGLRC hRC = NULL; // Permenant Rendering context
|
static HGLRC hRC = NULL; // Permenant Rendering context
|
||||||
static HDC hDC = NULL; // Private GDI Device context
|
static HDC hDC = NULL; // Private GDI Device context
|
||||||
static OpenGLComposite* pOpenGLComposite = NULL;
|
static OpenGLComposite* pOpenGLComposite = NULL;
|
||||||
|
static bool sInteractiveResize = false;
|
||||||
|
|
||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
@@ -289,6 +290,21 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_ENTERSIZEMOVE:
|
||||||
|
sInteractiveResize = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_EXITSIZEMOVE:
|
||||||
|
sInteractiveResize = false;
|
||||||
|
if (pOpenGLComposite)
|
||||||
|
{
|
||||||
|
RECT clientRect = {};
|
||||||
|
if (GetClientRect(hWnd, &clientRect))
|
||||||
|
pOpenGLComposite->resizeGL(static_cast<WORD>(clientRect.right - clientRect.left), static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, NULL, FALSE);
|
||||||
|
break;
|
||||||
|
|
||||||
case WM_SIZE:
|
case WM_SIZE:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -301,15 +317,22 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
|
||||||
case WM_PAINT:
|
case WM_PAINT:
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
wglMakeCurrent(hDC, hRC);
|
PAINTSTRUCT paint = {};
|
||||||
|
BeginPaint(hWnd, &paint);
|
||||||
|
EndPaint(hWnd, &paint);
|
||||||
|
|
||||||
if (pOpenGLComposite)
|
if (!sInteractiveResize && pOpenGLComposite)
|
||||||
|
{
|
||||||
|
wglMakeCurrent(hDC, hRC);
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL();
|
||||||
|
wglMakeCurrent( NULL, NULL );
|
||||||
wglMakeCurrent( NULL, NULL );
|
}
|
||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ DEFINE_GUID(IID_PinnedMemoryAllocator,
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr GLuint kVideoTextureUnit = 1;
|
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
||||||
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
const char* kDisplayModeName = "1080p59.94";
|
const char* kDisplayModeName = "1080p59.94";
|
||||||
const char* kVertexShaderSource =
|
const char* kVertexShaderSource =
|
||||||
@@ -67,6 +68,31 @@ const char* kVertexShaderSource =
|
|||||||
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
|
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
|
||||||
" vTexCoord = texCoords[gl_VertexID];\n"
|
" vTexCoord = texCoords[gl_VertexID];\n"
|
||||||
"}\n";
|
"}\n";
|
||||||
|
const char* kDecodeFragmentShaderSource =
|
||||||
|
"#version 430 core\n"
|
||||||
|
"layout(binding = 2) uniform sampler2D uPackedVideoInput;\n"
|
||||||
|
"uniform vec2 uPackedVideoResolution;\n"
|
||||||
|
"uniform vec2 uDecodedVideoResolution;\n"
|
||||||
|
"in vec2 vTexCoord;\n"
|
||||||
|
"layout(location = 0) out vec4 fragColor;\n"
|
||||||
|
"vec4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n"
|
||||||
|
"{\n"
|
||||||
|
" Y = (Y * 256.0 - 16.0) / 219.0;\n"
|
||||||
|
" Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||||
|
" Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||||
|
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
|
||||||
|
"}\n"
|
||||||
|
"void main()\n"
|
||||||
|
"{\n"
|
||||||
|
" vec2 correctedUv = vec2(vTexCoord.x, 1.0 - vTexCoord.y);\n"
|
||||||
|
" ivec2 decodedSize = ivec2(max(uDecodedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||||
|
" ivec2 outputCoord = clamp(ivec2(correctedUv * vec2(decodedSize)), ivec2(0, 0), decodedSize - ivec2(1, 1));\n"
|
||||||
|
" ivec2 packedSize = ivec2(max(uPackedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||||
|
" ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, packedSize.x - 1), clamp(outputCoord.y, 0, packedSize.y - 1));\n"
|
||||||
|
" vec4 macroPixel = texelFetch(uPackedVideoInput, packedCoord, 0);\n"
|
||||||
|
" float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;\n"
|
||||||
|
" fragColor = rec709YCbCr2rgba(ySample, macroPixel.b, macroPixel.r, 1.0);\n"
|
||||||
|
"}\n";
|
||||||
|
|
||||||
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
||||||
{
|
{
|
||||||
@@ -129,9 +155,14 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
mHasNoInputSource(true),
|
mHasNoInputSource(true),
|
||||||
mFastTransferExtensionAvailable(false),
|
mFastTransferExtensionAvailable(false),
|
||||||
mCaptureTexture(0),
|
mCaptureTexture(0),
|
||||||
|
mDecodedTexture(0),
|
||||||
mFBOTexture(0),
|
mFBOTexture(0),
|
||||||
|
mDecodeFrameBuf(0),
|
||||||
mFullscreenVAO(0),
|
mFullscreenVAO(0),
|
||||||
mGlobalParamsUBO(0),
|
mGlobalParamsUBO(0),
|
||||||
|
mDecodeProgram(0),
|
||||||
|
mDecodeVertexShader(0),
|
||||||
|
mDecodeFragmentShader(0),
|
||||||
mProgram(0),
|
mProgram(0),
|
||||||
mVertexShader(0),
|
mVertexShader(0),
|
||||||
mFragmentShader(0),
|
mFragmentShader(0),
|
||||||
@@ -195,6 +226,8 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
glDeleteVertexArrays(1, &mFullscreenVAO);
|
glDeleteVertexArrays(1, &mFullscreenVAO);
|
||||||
if (mGlobalParamsUBO != 0)
|
if (mGlobalParamsUBO != 0)
|
||||||
glDeleteBuffers(1, &mGlobalParamsUBO);
|
glDeleteBuffers(1, &mGlobalParamsUBO);
|
||||||
|
if (mDecodeFrameBuf != 0)
|
||||||
|
glDeleteFramebuffers(1, &mDecodeFrameBuf);
|
||||||
if (mIdFrameBuf != 0)
|
if (mIdFrameBuf != 0)
|
||||||
glDeleteFramebuffers(1, &mIdFrameBuf);
|
glDeleteFramebuffers(1, &mIdFrameBuf);
|
||||||
if (mIdColorBuf != 0)
|
if (mIdColorBuf != 0)
|
||||||
@@ -203,12 +236,15 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
||||||
if (mCaptureTexture != 0)
|
if (mCaptureTexture != 0)
|
||||||
glDeleteTextures(1, &mCaptureTexture);
|
glDeleteTextures(1, &mCaptureTexture);
|
||||||
|
if (mDecodedTexture != 0)
|
||||||
|
glDeleteTextures(1, &mDecodedTexture);
|
||||||
if (mFBOTexture != 0)
|
if (mFBOTexture != 0)
|
||||||
glDeleteTextures(1, &mFBOTexture);
|
glDeleteTextures(1, &mFBOTexture);
|
||||||
if (mUnpinnedTextureBuffer != 0)
|
if (mUnpinnedTextureBuffer != 0)
|
||||||
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
glDeleteBuffers(1, &mUnpinnedTextureBuffer);
|
||||||
|
|
||||||
destroyShaderProgram();
|
destroyShaderProgram();
|
||||||
|
destroyDecodeShaderProgram();
|
||||||
if (mControlServer)
|
if (mControlServer)
|
||||||
mControlServer->Stop();
|
mControlServer->Stop();
|
||||||
|
|
||||||
@@ -421,18 +457,52 @@ error:
|
|||||||
|
|
||||||
void OpenGLComposite::paintGL()
|
void OpenGLComposite::paintGL()
|
||||||
{
|
{
|
||||||
|
if (!TryEnterCriticalSection(&pMutex))
|
||||||
|
{
|
||||||
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// The DeckLink API provides IDeckLinkGLScreenPreviewHelper as a convenient way to view the playout video frames
|
// The DeckLink API provides IDeckLinkGLScreenPreviewHelper as a convenient way to view the playout video frames
|
||||||
// in a window. However, it performs a copy from host memory to the GPU which is wasteful in this case since
|
// in a window. However, it performs a copy from host memory to the GPU which is wasteful in this case since
|
||||||
// we already have the rendered frame to be played out sitting in the GPU in the mIdFrameBuf frame buffer.
|
// we already have the rendered frame to be played out sitting in the GPU in the mIdFrameBuf frame buffer.
|
||||||
|
|
||||||
// Simply copy the off-screen frame buffer to on-screen frame buffer, scaling to the viewing window size.
|
// Copy the off-screen frame buffer to the on-screen frame buffer while preserving the
|
||||||
|
// incoming video aspect ratio. Any extra window area is cleared to black.
|
||||||
|
int destWidth = mViewWidth;
|
||||||
|
int destHeight = mViewHeight;
|
||||||
|
int destX = 0;
|
||||||
|
int destY = 0;
|
||||||
|
|
||||||
|
if (mFrameWidth > 0 && mFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0)
|
||||||
|
{
|
||||||
|
const double frameAspect = static_cast<double>(mFrameWidth) / static_cast<double>(mFrameHeight);
|
||||||
|
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
|
||||||
|
|
||||||
|
if (viewAspect > frameAspect)
|
||||||
|
{
|
||||||
|
destHeight = mViewHeight;
|
||||||
|
destWidth = static_cast<int>(destHeight * frameAspect + 0.5);
|
||||||
|
destX = (mViewWidth - destWidth) / 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destWidth = mViewWidth;
|
||||||
|
destHeight = static_cast<int>(destWidth / frameAspect + 0.5);
|
||||||
|
destY = (mViewHeight - destHeight) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mIdFrameBuf);
|
||||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||||
glViewport(0, 0, mViewWidth, mViewHeight);
|
glViewport(0, 0, mViewWidth, mViewHeight);
|
||||||
glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, 0, 0, mViewWidth, mViewHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glBlitFramebuffer(0, 0, mFrameWidth, mFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
|
||||||
SwapBuffers(hGLDC);
|
SwapBuffers(hGLDC);
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
LeaveCriticalSection(&pMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::resizeGL(WORD width, WORD height)
|
void OpenGLComposite::resizeGL(WORD width, WORD height)
|
||||||
@@ -470,6 +540,7 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
callbacks.updateParameter = [this](const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error) {
|
callbacks.updateParameter = [this](const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error) {
|
||||||
return UpdateParameterJson(shaderId, parameterId, valueJson, error);
|
return UpdateParameterJson(shaderId, parameterId, valueJson, error);
|
||||||
};
|
};
|
||||||
|
callbacks.resetParameters = [this](const std::string& shaderId, std::string& error) { return ResetShaderParameters(shaderId, error); };
|
||||||
callbacks.setBypass = [this](bool bypassEnabled, std::string& error) { return SetBypassEnabled(bypassEnabled, error); };
|
callbacks.setBypass = [this](bool bypassEnabled, std::string& error) { return SetBypassEnabled(bypassEnabled, error); };
|
||||||
callbacks.setMixAmount = [this](double mixAmount, std::string& error) { return SetMixAmount(mixAmount, error); };
|
callbacks.setMixAmount = [this](double mixAmount, std::string& error) { return SetMixAmount(mixAmount, error); };
|
||||||
callbacks.reloadShader = [this](std::string& error) {
|
callbacks.reloadShader = [this](std::string& error) {
|
||||||
@@ -490,6 +561,12 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
|
|
||||||
// Prepare the runtime shader program generated from the active shader package.
|
// Prepare the runtime shader program generated from the active shader package.
|
||||||
char compilerErrorMessage[1024];
|
char compilerErrorMessage[1024];
|
||||||
|
if (! compileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
if (! compileFragmentShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||||
{
|
{
|
||||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||||
@@ -520,15 +597,34 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth/2, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
glGenTextures(1, &mDecodedTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mFrameWidth, mFrameHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
|
||||||
// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
|
// Create Frame Buffer Object (FBO) to perform off-screen rendering of scene.
|
||||||
// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
|
// This allows the render to be done on a framebuffer with width and height exactly matching the video format.
|
||||||
|
glGenFramebuffers(1, &mDecodeFrameBuf);
|
||||||
glGenFramebuffers(1, &mIdFrameBuf);
|
glGenFramebuffers(1, &mIdFrameBuf);
|
||||||
glGenRenderbuffers(1, &mIdColorBuf);
|
glGenRenderbuffers(1, &mIdColorBuf);
|
||||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||||
glGenVertexArrays(1, &mFullscreenVAO);
|
glGenVertexArrays(1, &mFullscreenVAO);
|
||||||
glGenBuffers(1, &mGlobalParamsUBO);
|
glGenBuffers(1, &mGlobalParamsUBO);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
|
||||||
|
GLenum glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (glStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
MessageBox(NULL, _T("Cannot initialize decode framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
|
||||||
// Texture for FBO
|
// Texture for FBO
|
||||||
@@ -549,7 +645,7 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
// Attach the texture which stores the playback image
|
// Attach the texture which stores the playback image
|
||||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
|
||||||
|
|
||||||
GLenum glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
glStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
if (glStatus != GL_FRAMEBUFFER_COMPLETE)
|
if (glStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||||
{
|
{
|
||||||
MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
MessageBox(NULL, _T("Cannot initialize framebuffer."), _T("OpenGL initialization error."), MB_OK);
|
||||||
@@ -645,8 +741,18 @@ void OpenGLComposite::PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame,
|
|||||||
wglMakeCurrent( hGLDC, hGLRC );
|
wglMakeCurrent( hGLDC, hGLRC );
|
||||||
|
|
||||||
// Draw the effect output to the off-screen framebuffer.
|
// Draw the effect output to the off-screen framebuffer.
|
||||||
|
const auto renderStartTime = std::chrono::steady_clock::now();
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
renderEffect();
|
renderEffect();
|
||||||
|
const auto renderEndTime = std::chrono::steady_clock::now();
|
||||||
|
if (mRuntimeHost)
|
||||||
|
{
|
||||||
|
const double frameBudgetMilliseconds = mFrameTimescale != 0
|
||||||
|
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mFrameTimescale)
|
||||||
|
: 0.0;
|
||||||
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
||||||
|
mRuntimeHost->SetPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
|
||||||
|
}
|
||||||
if (mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
mRuntimeHost->AdvanceFrame();
|
mRuntimeHost->AdvanceFrame();
|
||||||
|
|
||||||
@@ -789,6 +895,58 @@ bool OpenGLComposite::ReloadShader()
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::compileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||||
|
{
|
||||||
|
GLsizei errorBufferSize = 0;
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
const char* vertexSource = kVertexShaderSource;
|
||||||
|
const char* fragmentSource = kDecodeFragmentShaderSource;
|
||||||
|
|
||||||
|
GLuint newVertexShader = glCreateShader(GL_VERTEX_SHADER);
|
||||||
|
glShaderSource(newVertexShader, 1, (const GLchar**)&vertexSource, NULL);
|
||||||
|
glCompileShader(newVertexShader);
|
||||||
|
glGetShaderiv(newVertexShader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetShaderInfoLog(newVertexShader, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint newFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
glShaderSource(newFragmentShader, 1, (const GLchar**)&fragmentSource, NULL);
|
||||||
|
glCompileShader(newFragmentShader);
|
||||||
|
glGetShaderiv(newFragmentShader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetShaderInfoLog(newFragmentShader, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
glDeleteShader(newFragmentShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint newProgram = glCreateProgram();
|
||||||
|
glAttachShader(newProgram, newVertexShader);
|
||||||
|
glAttachShader(newProgram, newFragmentShader);
|
||||||
|
glLinkProgram(newProgram);
|
||||||
|
glGetProgramiv(newProgram, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
glGetProgramInfoLog(newProgram, errorMessageSize, &errorBufferSize, errorMessage);
|
||||||
|
glDeleteProgram(newProgram);
|
||||||
|
glDeleteShader(newVertexShader);
|
||||||
|
glDeleteShader(newFragmentShader);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroyDecodeShaderProgram();
|
||||||
|
mDecodeProgram = newProgram;
|
||||||
|
mDecodeVertexShader = newVertexShader;
|
||||||
|
mDecodeFragmentShader = newFragmentShader;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void OpenGLComposite::destroyShaderProgram()
|
void OpenGLComposite::destroyShaderProgram()
|
||||||
{
|
{
|
||||||
if (mProgram != 0)
|
if (mProgram != 0)
|
||||||
@@ -810,13 +968,31 @@ void OpenGLComposite::destroyShaderProgram()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::destroyDecodeShaderProgram()
|
||||||
|
{
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
{
|
||||||
|
glDeleteProgram(mDecodeProgram);
|
||||||
|
mDecodeProgram = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDecodeFragmentShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mDecodeFragmentShader);
|
||||||
|
mDecodeFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mDecodeVertexShader != 0)
|
||||||
|
{
|
||||||
|
glDeleteShader(mDecodeVertexShader);
|
||||||
|
mDecodeVertexShader = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void OpenGLComposite::renderEffect()
|
void OpenGLComposite::renderEffect()
|
||||||
{
|
{
|
||||||
PollRuntimeChanges();
|
PollRuntimeChanges();
|
||||||
|
|
||||||
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
||||||
|
|
||||||
if (mHasNoInputSource)
|
if (mHasNoInputSource)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -828,8 +1004,13 @@ void OpenGLComposite::renderEffect()
|
|||||||
|
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glActiveTexture(GL_TEXTURE0 + kVideoTextureUnit);
|
renderDecodePass();
|
||||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||||
|
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
|
||||||
glBindVertexArray(mFullscreenVAO);
|
glBindVertexArray(mFullscreenVAO);
|
||||||
glUseProgram(mProgram);
|
glUseProgram(mProgram);
|
||||||
|
|
||||||
@@ -850,6 +1031,31 @@ void OpenGLComposite::renderEffect()
|
|||||||
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
VideoFrameTransfer::endTextureInUse(VideoFrameTransfer::CPUtoGPU);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLComposite::renderDecodePass()
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
||||||
|
glViewport(0, 0, mFrameWidth, mFrameHeight);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||||
|
glBindVertexArray(mFullscreenVAO);
|
||||||
|
glUseProgram(mDecodeProgram);
|
||||||
|
|
||||||
|
const GLint packedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uPackedVideoResolution");
|
||||||
|
const GLint decodedResolutionLocation = glGetUniformLocation(mDecodeProgram, "uDecodedVideoResolution");
|
||||||
|
if (packedResolutionLocation >= 0)
|
||||||
|
glUniform2f(packedResolutionLocation, static_cast<float>(mFrameWidth / 2), static_cast<float>(mFrameHeight));
|
||||||
|
if (decodedResolutionLocation >= 0)
|
||||||
|
glUniform2f(decodedResolutionLocation, static_cast<float>(mFrameWidth), static_cast<float>(mFrameHeight));
|
||||||
|
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
// Compile a fullscreen shader pass from the runtime Slang source into a core-profile
|
// Compile a fullscreen shader pass from the runtime Slang source into a core-profile
|
||||||
// GLSL program. The renderer owns the fullscreen pass and parameter UBO layout.
|
// GLSL program. The renderer owns the fullscreen pass and parameter UBO layout.
|
||||||
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
|
bool OpenGLComposite::compileFragmentShader(int errorMessageSize, char* errorMessage)
|
||||||
@@ -1068,6 +1274,15 @@ bool OpenGLComposite::UpdateParameterJson(const std::string& shaderId, const std
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::ResetShaderParameters(const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
if (!mRuntimeHost->ResetParameters(shaderId, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
broadcastRuntimeState();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::SetBypassEnabled(bool bypassEnabled, std::string& error)
|
bool OpenGLComposite::SetBypassEnabled(bool bypassEnabled, std::string& error)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost->SetBypass(bypassEnabled, error))
|
if (!mRuntimeHost->SetBypass(bypassEnabled, error))
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ public:
|
|||||||
std::string GetRuntimeStateJson() const;
|
std::string GetRuntimeStateJson() const;
|
||||||
bool SelectShader(const std::string& shaderId, std::string& error);
|
bool SelectShader(const std::string& shaderId, std::string& error);
|
||||||
bool UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
bool UpdateParameterJson(const std::string& shaderId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||||
|
bool ResetShaderParameters(const std::string& shaderId, std::string& error);
|
||||||
bool SetBypassEnabled(bool bypassEnabled, std::string& error);
|
bool SetBypassEnabled(bool bypassEnabled, std::string& error);
|
||||||
bool SetMixAmount(double mixAmount, std::string& error);
|
bool SetMixAmount(double mixAmount, std::string& error);
|
||||||
|
|
||||||
@@ -116,13 +117,18 @@ private:
|
|||||||
// OpenGL data
|
// OpenGL data
|
||||||
bool mFastTransferExtensionAvailable;
|
bool mFastTransferExtensionAvailable;
|
||||||
GLuint mCaptureTexture;
|
GLuint mCaptureTexture;
|
||||||
|
GLuint mDecodedTexture;
|
||||||
GLuint mFBOTexture;
|
GLuint mFBOTexture;
|
||||||
GLuint mUnpinnedTextureBuffer;
|
GLuint mUnpinnedTextureBuffer;
|
||||||
|
GLuint mDecodeFrameBuf;
|
||||||
GLuint mIdFrameBuf;
|
GLuint mIdFrameBuf;
|
||||||
GLuint mIdColorBuf;
|
GLuint mIdColorBuf;
|
||||||
GLuint mIdDepthBuf;
|
GLuint mIdDepthBuf;
|
||||||
GLuint mFullscreenVAO;
|
GLuint mFullscreenVAO;
|
||||||
GLuint mGlobalParamsUBO;
|
GLuint mGlobalParamsUBO;
|
||||||
|
GLuint mDecodeProgram;
|
||||||
|
GLuint mDecodeVertexShader;
|
||||||
|
GLuint mDecodeFragmentShader;
|
||||||
GLuint mProgram;
|
GLuint mProgram;
|
||||||
GLuint mVertexShader;
|
GLuint mVertexShader;
|
||||||
GLuint mFragmentShader;
|
GLuint mFragmentShader;
|
||||||
@@ -134,7 +140,10 @@ private:
|
|||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
bool compileFragmentShader(int errorMessageSize, char* errorMessage);
|
bool compileFragmentShader(int errorMessageSize, char* errorMessage);
|
||||||
|
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||||
void destroyShaderProgram();
|
void destroyShaderProgram();
|
||||||
|
void destroyDecodeShaderProgram();
|
||||||
|
void renderDecodePass();
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
bool PollRuntimeChanges();
|
bool PollRuntimeChanges();
|
||||||
void broadcastRuntimeState();
|
void broadcastRuntimeState();
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ RuntimeHost::RuntimeHost()
|
|||||||
mHasSignal(false),
|
mHasSignal(false),
|
||||||
mSignalWidth(0),
|
mSignalWidth(0),
|
||||||
mSignalHeight(0),
|
mSignalHeight(0),
|
||||||
|
mFrameBudgetMilliseconds(0.0),
|
||||||
|
mRenderMilliseconds(0.0),
|
||||||
|
mSmoothedRenderMilliseconds(0.0),
|
||||||
mServerPort(8080),
|
mServerPort(8080),
|
||||||
mAutoReloadEnabled(true),
|
mAutoReloadEnabled(true),
|
||||||
mMixAmount(1.0),
|
mMixAmount(1.0),
|
||||||
@@ -332,8 +335,25 @@ bool RuntimeHost::UpdateParameter(const std::string& shaderId, const std::string
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
mPersistentState.parameterValuesByShader[shaderId][parameterId] = normalized;
|
mPersistentState.parameterValuesByShader[shaderId][parameterId] = normalized;
|
||||||
if (shaderId == mActiveShaderId)
|
|
||||||
mReloadRequested = true;
|
return SavePersistentState(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::ResetParameters(const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|
||||||
|
auto shaderIt = mPackagesById.find(shaderId);
|
||||||
|
if (shaderIt == mPackagesById.end())
|
||||||
|
{
|
||||||
|
error = "Unknown shader id: " + shaderId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<std::string, ShaderParameterValue>& shaderValues = mPersistentState.parameterValuesByShader[shaderId];
|
||||||
|
shaderValues.clear();
|
||||||
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||||
|
shaderValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
|
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
@@ -376,6 +396,17 @@ void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned heigh
|
|||||||
mSignalModeName = modeName;
|
mSignalModeName = modeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
||||||
|
mRenderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||||
|
if (mSmoothedRenderMilliseconds <= 0.0)
|
||||||
|
mSmoothedRenderMilliseconds = mRenderMilliseconds;
|
||||||
|
else
|
||||||
|
mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
void RuntimeHost::AdvanceFrame()
|
void RuntimeHost::AdvanceFrame()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -944,65 +975,17 @@ std::string RuntimeHost::BuildWrapperSlangSource(const ShaderPackage& shaderPack
|
|||||||
source << "\t" << SlangTypeForParameter(definition.type).substr(strlen("uniform ")) << " " << definition.id << ";\n";
|
source << "\t" << SlangTypeForParameter(definition.type).substr(strlen("uniform ")) << " " << definition.id << ";\n";
|
||||||
source << "};\n\n";
|
source << "};\n\n";
|
||||||
source << "Sampler2D<float4> gVideoInput;\n\n";
|
source << "Sampler2D<float4> gVideoInput;\n\n";
|
||||||
source << "float4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n";
|
|
||||||
source << "{\n";
|
|
||||||
source << "\tY = (Y * 256.0 - 16.0) / 219.0;\n";
|
|
||||||
source << "\tCb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n";
|
|
||||||
source << "\tCr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n";
|
|
||||||
source << "\treturn float4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n";
|
|
||||||
source << "}\n\n";
|
|
||||||
source << "float4 bilinear(float4 W, float4 X, float4 Y, float4 Z, float2 weight)\n";
|
|
||||||
source << "{\n";
|
|
||||||
source << "\tfloat4 m0 = lerp(W, Z, weight.x);\n";
|
|
||||||
source << "\tfloat4 m1 = lerp(X, Y, weight.x);\n";
|
|
||||||
source << "\treturn lerp(m0, m1, weight.y);\n";
|
|
||||||
source << "}\n\n";
|
|
||||||
source << "void textureGatherYUV(Sampler2D<float4> textureSampler, float2 tc, out float4 W, out float4 X, out float4 Y, out float4 Z)\n";
|
|
||||||
source << "{\n";
|
|
||||||
source << "\tuint width = 0;\n";
|
|
||||||
source << "\tuint height = 0;\n";
|
|
||||||
source << "\ttextureSampler.GetDimensions(width, height);\n";
|
|
||||||
source << "\tint2 tx = int2(tc * float2(width, height));\n";
|
|
||||||
source << "\tint2 tmin = int2(0, 0);\n";
|
|
||||||
source << "\tint2 tmax = int2(int(width), int(height)) - int2(1, 1);\n";
|
|
||||||
source << "\tW = textureSampler.Load(int3(tx, 0));\n";
|
|
||||||
source << "\tX = textureSampler.Load(int3(clamp(tx + int2(0, 1), tmin, tmax), 0));\n";
|
|
||||||
source << "\tY = textureSampler.Load(int3(clamp(tx + int2(1, 1), tmin, tmax), 0));\n";
|
|
||||||
source << "\tZ = textureSampler.Load(int3(clamp(tx + int2(1, 0), tmin, tmax), 0));\n";
|
|
||||||
source << "}\n\n";
|
|
||||||
source << "float4 sampleVideo(float2 tc)\n";
|
source << "float4 sampleVideo(float2 tc)\n";
|
||||||
source << "{\n";
|
source << "{\n";
|
||||||
source << "\tfloat4 macro, macroU, macroR, macroUR;\n";
|
source << "\treturn gVideoInput.Sample(tc);\n";
|
||||||
source << "\ttextureGatherYUV(gVideoInput, tc, macro, macroU, macroUR, macroR);\n";
|
|
||||||
source << "\tuint width = 0;\n";
|
|
||||||
source << "\tuint height = 0;\n";
|
|
||||||
source << "\tgVideoInput.GetDimensions(width, height);\n";
|
|
||||||
source << "\tfloat2 off = frac(tc * float2(width, height));\n";
|
|
||||||
source << "\tfloat4 pixel, pixelR, pixelU, pixelUR;\n";
|
|
||||||
source << "\tif (off.x > 0.5)\n";
|
|
||||||
source << "\t{\n";
|
|
||||||
source << "\t\tpixel = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelR = rec709YCbCr2rgba(macroR.g, macroR.b, macroR.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelU = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelUR = rec709YCbCr2rgba(macroUR.g, macroUR.b, macroUR.r, 1.0);\n";
|
|
||||||
source << "\t}\n";
|
|
||||||
source << "\telse\n";
|
|
||||||
source << "\t{\n";
|
|
||||||
source << "\t\tpixel = rec709YCbCr2rgba(macro.g, macro.b, macro.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelR = rec709YCbCr2rgba(macro.a, macro.b, macro.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelU = rec709YCbCr2rgba(macroU.g, macroU.b, macroU.r, 1.0);\n";
|
|
||||||
source << "\t\tpixelUR = rec709YCbCr2rgba(macroU.a, macroU.b, macroU.r, 1.0);\n";
|
|
||||||
source << "\t}\n";
|
|
||||||
source << "\treturn bilinear(pixel, pixelU, pixelUR, pixelR, off);\n";
|
|
||||||
source << "}\n\n";
|
source << "}\n\n";
|
||||||
source << "#include \"" << shaderPackage.shaderPath.generic_string() << "\"\n\n";
|
source << "#include \"" << shaderPackage.shaderPath.generic_string() << "\"\n\n";
|
||||||
source << "[shader(\"fragment\")]\n";
|
source << "[shader(\"fragment\")]\n";
|
||||||
source << "float4 fragmentMain(FragmentInput input) : SV_Target\n";
|
source << "float4 fragmentMain(FragmentInput input) : SV_Target\n";
|
||||||
source << "{\n";
|
source << "{\n";
|
||||||
source << "\tShaderContext context;\n";
|
source << "\tShaderContext context;\n";
|
||||||
source << "\tfloat2 correctedUv = float2(input.texCoord.x, 1.0 - input.texCoord.y);\n";
|
source << "\tcontext.uv = input.texCoord;\n";
|
||||||
source << "\tcontext.uv = correctedUv;\n";
|
source << "\tcontext.sourceColor = sampleVideo(context.uv);\n";
|
||||||
source << "\tcontext.sourceColor = sampleVideo(correctedUv);\n";
|
|
||||||
source << "\tcontext.inputResolution = gInputResolution;\n";
|
source << "\tcontext.inputResolution = gInputResolution;\n";
|
||||||
source << "\tcontext.outputResolution = gOutputResolution;\n";
|
source << "\tcontext.outputResolution = gOutputResolution;\n";
|
||||||
source << "\tcontext.time = gTime;\n";
|
source << "\tcontext.time = gTime;\n";
|
||||||
@@ -1184,6 +1167,13 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
video.set("modeName", JsonValue(mSignalModeName));
|
video.set("modeName", JsonValue(mSignalModeName));
|
||||||
root.set("video", video);
|
root.set("video", video);
|
||||||
|
|
||||||
|
JsonValue performance = JsonValue::MakeObject();
|
||||||
|
performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds));
|
||||||
|
performance.set("renderMs", JsonValue(mRenderMilliseconds));
|
||||||
|
performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds));
|
||||||
|
performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0));
|
||||||
|
root.set("performance", performance);
|
||||||
|
|
||||||
JsonValue shaders = JsonValue::MakeArray();
|
JsonValue shaders = JsonValue::MakeArray();
|
||||||
for (const std::string& shaderId : mPackageOrder)
|
for (const std::string& shaderId : mPackageOrder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -88,11 +88,13 @@ public:
|
|||||||
|
|
||||||
bool SelectShader(const std::string& shaderId, std::string& error);
|
bool SelectShader(const std::string& shaderId, std::string& error);
|
||||||
bool UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
bool UpdateParameter(const std::string& shaderId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
||||||
|
bool ResetParameters(const std::string& shaderId, std::string& error);
|
||||||
bool SetBypass(bool bypassEnabled, std::string& error);
|
bool SetBypass(bool bypassEnabled, std::string& error);
|
||||||
bool SetMixAmount(double mixAmount, std::string& error);
|
bool SetMixAmount(double mixAmount, std::string& error);
|
||||||
|
|
||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
|
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
void AdvanceFrame();
|
void AdvanceFrame();
|
||||||
|
|
||||||
bool BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error);
|
bool BuildActiveFragmentShaderSource(std::string& fragmentShaderSource, std::string& error);
|
||||||
@@ -163,6 +165,9 @@ private:
|
|||||||
unsigned mSignalWidth;
|
unsigned mSignalWidth;
|
||||||
unsigned mSignalHeight;
|
unsigned mSignalHeight;
|
||||||
std::string mSignalModeName;
|
std::string mSignalModeName;
|
||||||
|
double mFrameBudgetMilliseconds;
|
||||||
|
double mRenderMilliseconds;
|
||||||
|
double mSmoothedRenderMilliseconds;
|
||||||
unsigned short mServerPort;
|
unsigned short mServerPort;
|
||||||
bool mAutoReloadEnabled;
|
bool mAutoReloadEnabled;
|
||||||
double mMixAmount;
|
double mMixAmount;
|
||||||
|
|||||||
8
shaders/black-and-white/shader.json
Normal file
8
shaders/black-and-white/shader.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"id": "black-and-white",
|
||||||
|
"name": "Black and White",
|
||||||
|
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
|
||||||
|
"category": "Built-in",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
5
shaders/black-and-white/shader.slang
Normal file
5
shaders/black-and-white/shader.slang
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float luma = dot(context.sourceColor.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||||
|
return float4(luma, luma, luma, context.sourceColor.a);
|
||||||
|
}
|
||||||
36
shaders/gaussian-blur/shader.json
Normal file
36
shaders/gaussian-blur/shader.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"id": "gaussian-blur",
|
||||||
|
"name": "Gaussian Blur",
|
||||||
|
"description": "Applies a simple Gaussian-style blur to the decoded video input.",
|
||||||
|
"category": "Built-in",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "radius",
|
||||||
|
"label": "Radius",
|
||||||
|
"type": "float",
|
||||||
|
"default": 2.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 8.0,
|
||||||
|
"step": 0.1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "strength",
|
||||||
|
"label": "Strength",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "samples",
|
||||||
|
"label": "Samples",
|
||||||
|
"type": "float",
|
||||||
|
"default": 2.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 25.0,
|
||||||
|
"step": 1.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
shaders/gaussian-blur/shader.slang
Normal file
35
shaders/gaussian-blur/shader.slang
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
|
||||||
|
float blurRadius = max(radius, 0.0);
|
||||||
|
float2 sampleStep = texel * blurRadius;
|
||||||
|
int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5);
|
||||||
|
|
||||||
|
float4 center = sampleVideo(context.uv);
|
||||||
|
float4 blur = float4(0.0, 0.0, 0.0, 0.0);
|
||||||
|
float totalWeight = 0.0;
|
||||||
|
|
||||||
|
for (int y = -sampleRadius; y <= sampleRadius; ++y)
|
||||||
|
{
|
||||||
|
for (int x = -sampleRadius; x <= sampleRadius; ++x)
|
||||||
|
{
|
||||||
|
float distanceSquared = float(x * x + y * y);
|
||||||
|
float sigma = max(float(sampleRadius) * 0.5, 0.5);
|
||||||
|
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
|
||||||
|
float2 offset = float2(float(x), float(y)) * sampleStep;
|
||||||
|
blur += sampleVideo(context.uv + offset) * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sampleRadius == 0)
|
||||||
|
{
|
||||||
|
blur = center;
|
||||||
|
totalWeight = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
blur /= max(totalWeight, 0.0001);
|
||||||
|
|
||||||
|
float mixValue = saturate(strength);
|
||||||
|
return lerp(center, blur, mixValue);
|
||||||
|
}
|
||||||
72
shaders/vhs/shader.json
Normal file
72
shaders/vhs/shader.json
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"id": "vhs",
|
||||||
|
"name": "VHS",
|
||||||
|
"description": "VHS with wiggle, smear, and YIQ-style color separation inspired by the Godot shader reference.",
|
||||||
|
"category": "Built-in",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "wiggle",
|
||||||
|
"label": "Wiggle",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.03,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.5,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wiggleSpeed",
|
||||||
|
"label": "Wiggle Speed",
|
||||||
|
"type": "float",
|
||||||
|
"default": 25.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 100.0,
|
||||||
|
"step": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "smear",
|
||||||
|
"label": "Smear",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 2.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "blurSamples",
|
||||||
|
"label": "Blur Samples",
|
||||||
|
"type": "float",
|
||||||
|
"default": 15.0,
|
||||||
|
"min": 3.0,
|
||||||
|
"max": 15.0,
|
||||||
|
"step": 1.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "vignetteAmount",
|
||||||
|
"label": "Vignette",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.18,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 0.6,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aberrationAmount",
|
||||||
|
"label": "Aberration",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.75,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 3.0,
|
||||||
|
"step": 0.05
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "halationAmount",
|
||||||
|
"label": "Halation",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.12,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 0.5,
|
||||||
|
"step": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
115
shaders/vhs/shader.slang
Normal file
115
shaders/vhs/shader.slang
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
float onOff(float a, float b, float c, float framecount)
|
||||||
|
{
|
||||||
|
return step(c, sin((framecount * 0.001) + a * cos((framecount * 0.001) * b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 jumpy(float2 uv, float framecount)
|
||||||
|
{
|
||||||
|
float2 look = uv;
|
||||||
|
float m = frac(framecount / 4.0);
|
||||||
|
float dy = look.y - m;
|
||||||
|
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;
|
||||||
|
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)));
|
||||||
|
look.y = frac(look.y - 0.01 * vShift);
|
||||||
|
return look;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 circle(float start, float points, float point)
|
||||||
|
{
|
||||||
|
float rad = 6.28318530718 * (1.0 / points) * (point + start);
|
||||||
|
return float2(-(.3 + rad), cos(rad));
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 rgb2yiq(float3 c)
|
||||||
|
{
|
||||||
|
return float3(
|
||||||
|
0.2989 * c.x + 0.5959 * c.y + 0.2115 * c.z,
|
||||||
|
0.5870 * c.x - 0.2744 * c.y - 0.5229 * c.z,
|
||||||
|
0.1140 * c.x - 0.3216 * c.y + 0.3114 * c.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 yiq2rgb(float3 c)
|
||||||
|
{
|
||||||
|
return float3(
|
||||||
|
1.0 * c.x + 1.0 * c.y + 1.0 * c.z,
|
||||||
|
0.956 * c.x - 0.2720 * c.y - 1.1060 * c.z,
|
||||||
|
0.6210 * c.x - 0.6474 * c.y + 1.7046 * c.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 blurVhs(float2 uv, float d, int sampleCount)
|
||||||
|
{
|
||||||
|
float3 sum = float3(0.0, 0.0, 0.0);
|
||||||
|
float weight = 1.0 / max(float(sampleCount), 1.0);
|
||||||
|
float start = 2.0 / max(float(sampleCount), 1.0);
|
||||||
|
float2 pixelOffset = float2(d, 0.0);
|
||||||
|
float2 scale = 0.66 * 8.0 * pixelOffset;
|
||||||
|
|
||||||
|
for (int i = 0; i < 15; ++i)
|
||||||
|
{
|
||||||
|
if (i >= sampleCount)
|
||||||
|
break;
|
||||||
|
|
||||||
|
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
|
||||||
|
sum += sampleVideo(frac(uv + offset)).rgb * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 uv = context.uv;
|
||||||
|
float framecount = frac(context.time * wiggleSpeed / 7.0) * 7.0;
|
||||||
|
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
|
||||||
|
|
||||||
|
float d = 0.1 - round(frac(context.time / 3.0)) * 0.1;
|
||||||
|
uv = jumpy(uv, framecount);
|
||||||
|
float s = 0.0001 * -d + 0.0001 * wiggle * sin(context.time * wiggleSpeed);
|
||||||
|
float e = min(0.30, pow(max(0.0, cos(uv.y * 4.0 + 0.3) - 0.75) * (s + 0.5), 3.0)) * 25.0;
|
||||||
|
float r = 250.0 * (2.0 * s);
|
||||||
|
uv.x += abs(r * pow(min(0.003, (-uv.y + (0.01 * frac(context.time / 5.0) * 5.0))) * 3.0, 2.0)) * wiggle;
|
||||||
|
|
||||||
|
d = 0.051 + abs(sin(s / 4.0));
|
||||||
|
float c = max(0.0001, 0.002 * d) * smear;
|
||||||
|
|
||||||
|
float3 yBlur = blurVhs(uv, c + c * uv.x, sampleCount);
|
||||||
|
float y = rgb2yiq(yBlur).r;
|
||||||
|
|
||||||
|
uv.x += 0.01 * d;
|
||||||
|
c *= 6.0;
|
||||||
|
float3 iBlur = blurVhs(uv, c, sampleCount);
|
||||||
|
float i = rgb2yiq(iBlur).g;
|
||||||
|
|
||||||
|
uv.x += 0.005 * d;
|
||||||
|
c *= 2.5;
|
||||||
|
float3 qBlur = blurVhs(uv, c, sampleCount);
|
||||||
|
float q = rgb2yiq(qBlur).b;
|
||||||
|
|
||||||
|
float3 color = yiq2rgb(float3(y, i, q)) - pow(s + e * 2.0, 3.0);
|
||||||
|
|
||||||
|
float2 centered = context.uv * 2.0 - 1.0;
|
||||||
|
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
||||||
|
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
|
||||||
|
float redAberration = sampleVideo(frac(context.uv + aberrationOffset)).r;
|
||||||
|
float blueAberration = sampleVideo(frac(context.uv - aberrationOffset)).b;
|
||||||
|
color.r = lerp(color.r, redAberration, 0.35);
|
||||||
|
color.b = lerp(color.b, blueAberration, 0.35);
|
||||||
|
|
||||||
|
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
|
||||||
|
float3 halationSource =
|
||||||
|
sampleVideo(frac(context.uv + halationOffset)).rgb * 0.4 +
|
||||||
|
sampleVideo(frac(context.uv - halationOffset)).rgb * 0.4 +
|
||||||
|
sampleVideo(frac(context.uv + halationOffset * 2.0)).rgb * 0.2;
|
||||||
|
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
|
||||||
|
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
|
||||||
|
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
|
||||||
|
|
||||||
|
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));
|
||||||
|
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);
|
||||||
|
|
||||||
|
return float4(saturate(color), 1.0);
|
||||||
|
}
|
||||||
45
shaders/video-cube/shader.json
Normal file
45
shaders/video-cube/shader.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"id": "video-cube",
|
||||||
|
"name": "Video Cube",
|
||||||
|
"description": "Maps the live video onto the faces of a rotating cube in screen space.",
|
||||||
|
"category": "Built-in",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "spinSpeed",
|
||||||
|
"label": "Spin Speed",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 4.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cubeScale",
|
||||||
|
"label": "Cube Scale",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.85,
|
||||||
|
"min": 0.3,
|
||||||
|
"max": 1.4,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "faceZoom",
|
||||||
|
"label": "Face Zoom",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.5,
|
||||||
|
"max": 2.0,
|
||||||
|
"step": 0.01
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "backgroundMix",
|
||||||
|
"label": "Background Mix",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.15,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
116
shaders/video-cube/shader.slang
Normal file
116
shaders/video-cube/shader.slang
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool intersectCube(float3 rayOrigin, float3 rayDirection, float halfExtent, out float hitDistance)
|
||||||
|
{
|
||||||
|
float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent);
|
||||||
|
float3 boxMax = float3(halfExtent, halfExtent, halfExtent);
|
||||||
|
|
||||||
|
float3 invDir = 1.0 / rayDirection;
|
||||||
|
float3 t0 = (boxMin - rayOrigin) * invDir;
|
||||||
|
float3 t1 = (boxMax - rayOrigin) * invDir;
|
||||||
|
|
||||||
|
float3 tMin3 = min(t0, t1);
|
||||||
|
float3 tMax3 = max(t0, t1);
|
||||||
|
|
||||||
|
float tMin = max(max(tMin3.x, tMin3.y), tMin3.z);
|
||||||
|
float tMax = min(min(tMax3.x, tMax3.y), tMax3.z);
|
||||||
|
|
||||||
|
if (tMax < max(tMin, 0.0))
|
||||||
|
{
|
||||||
|
hitDistance = 0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hitDistance = tMin > 0.0 ? tMin : tMax;
|
||||||
|
return hitDistance > 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float2 cubeFaceUv(float3 hitPoint, float halfExtent, float zoom)
|
||||||
|
{
|
||||||
|
float3 face = abs(hitPoint);
|
||||||
|
float2 uv = float2(0.5, 0.5);
|
||||||
|
float safeZoom = max(zoom, 0.001);
|
||||||
|
|
||||||
|
if (face.x >= face.y && face.x >= face.z)
|
||||||
|
{
|
||||||
|
uv = hitPoint.x > 0.0
|
||||||
|
? float2(-hitPoint.z, hitPoint.y)
|
||||||
|
: float2(hitPoint.z, hitPoint.y);
|
||||||
|
}
|
||||||
|
else if (face.y >= face.x && face.y >= face.z)
|
||||||
|
{
|
||||||
|
uv = hitPoint.y > 0.0
|
||||||
|
? float2(hitPoint.x, -hitPoint.z)
|
||||||
|
: float2(hitPoint.x, hitPoint.z);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uv = hitPoint.z > 0.0
|
||||||
|
? float2(hitPoint.x, hitPoint.y)
|
||||||
|
: float2(-hitPoint.x, hitPoint.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
uv = uv / (halfExtent * 2.0 * safeZoom) + 0.5;
|
||||||
|
return frac(uv);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 centeredUv = context.uv * 2.0 - 1.0;
|
||||||
|
float aspect = context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
||||||
|
centeredUv.x *= aspect;
|
||||||
|
|
||||||
|
float3 rayOrigin = float3(0.0, 0.0, 2.7);
|
||||||
|
float3 rayDirection = normalize(float3(centeredUv, -2.1));
|
||||||
|
|
||||||
|
float spin = context.time * spinSpeed;
|
||||||
|
float yaw = spin;
|
||||||
|
float pitch = spin * 0.61 + 0.35;
|
||||||
|
|
||||||
|
float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw);
|
||||||
|
float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw);
|
||||||
|
|
||||||
|
float halfExtent = max(cubeScale, 0.05);
|
||||||
|
float hitDistance = 0.0;
|
||||||
|
|
||||||
|
float3 background = lerp(float3(0.02, 0.02, 0.03), context.sourceColor.rgb, saturate(backgroundMix));
|
||||||
|
|
||||||
|
if (!intersectCube(localOrigin, localDirection, halfExtent, hitDistance))
|
||||||
|
return float4(background, 1.0);
|
||||||
|
|
||||||
|
float3 localHit = localOrigin + localDirection * hitDistance;
|
||||||
|
float2 faceUv = cubeFaceUv(localHit, halfExtent, faceZoom);
|
||||||
|
float4 faceColor = sampleVideo(faceUv);
|
||||||
|
|
||||||
|
float3 normal;
|
||||||
|
float3 face = abs(localHit);
|
||||||
|
if (face.x >= face.y && face.x >= face.z)
|
||||||
|
normal = float3(sign(localHit.x), 0.0, 0.0);
|
||||||
|
else if (face.y >= face.x && face.y >= face.z)
|
||||||
|
normal = float3(0.0, sign(localHit.y), 0.0);
|
||||||
|
else
|
||||||
|
normal = float3(0.0, 0.0, sign(localHit.z));
|
||||||
|
|
||||||
|
normal = rotateX(rotateY(normal, yaw), pitch);
|
||||||
|
|
||||||
|
float3 lightDir = normalize(float3(0.5, 0.8, 0.6));
|
||||||
|
float diffuse = saturate(dot(normal, lightDir)) * 0.75 + 0.25;
|
||||||
|
float edge = 1.0 - saturate(max(face.x, max(face.y, face.z)) - halfExtent + 0.03) / 0.03;
|
||||||
|
|
||||||
|
float3 shaded = faceColor.rgb * diffuse;
|
||||||
|
shaded = lerp(shaded, shaded + 0.12, edge * 0.35);
|
||||||
|
|
||||||
|
return float4(saturate(shaded), 1.0);
|
||||||
|
}
|
||||||
20
ui/app.js
20
ui/app.js
@@ -2,6 +2,7 @@ const shaderSelect = document.getElementById("shader-select");
|
|||||||
const mixSlider = document.getElementById("mix-slider");
|
const mixSlider = document.getElementById("mix-slider");
|
||||||
const bypassToggle = document.getElementById("bypass-toggle");
|
const bypassToggle = document.getElementById("bypass-toggle");
|
||||||
const reloadButton = document.getElementById("reload-button");
|
const reloadButton = document.getElementById("reload-button");
|
||||||
|
const resetParametersButton = document.getElementById("reset-parameters-button");
|
||||||
const runtimeStatus = document.getElementById("runtime-status");
|
const runtimeStatus = document.getElementById("runtime-status");
|
||||||
const videoStatus = document.getElementById("video-status");
|
const videoStatus = document.getElementById("video-status");
|
||||||
const compileStatus = document.getElementById("compile-status");
|
const compileStatus = document.getElementById("compile-status");
|
||||||
@@ -141,6 +142,11 @@ function renderState(state) {
|
|||||||
const shaders = state.shaders || [];
|
const shaders = state.shaders || [];
|
||||||
const activeShaderId = state.runtime.activeShaderId;
|
const activeShaderId = state.runtime.activeShaderId;
|
||||||
const activeShader = shaders.find((shader) => shader.id === activeShaderId) || shaders[0];
|
const activeShader = shaders.find((shader) => shader.id === activeShaderId) || shaders[0];
|
||||||
|
const performance = state.performance || {};
|
||||||
|
const frameBudgetMs = Number(performance.frameBudgetMs || 0);
|
||||||
|
const renderMs = Number(performance.renderMs || 0);
|
||||||
|
const smoothedRenderMs = Number(performance.smoothedRenderMs || 0);
|
||||||
|
const budgetUsedPercent = Number(performance.budgetUsedPercent || 0);
|
||||||
|
|
||||||
shaderSelect.innerHTML = "";
|
shaderSelect.innerHTML = "";
|
||||||
shaders.forEach((shader) => {
|
shaders.forEach((shader) => {
|
||||||
@@ -156,12 +162,17 @@ function renderState(state) {
|
|||||||
mixSlider.value = state.runtime.mixAmount ?? 1;
|
mixSlider.value = state.runtime.mixAmount ?? 1;
|
||||||
bypassToggle.checked = Boolean(state.runtime.bypass);
|
bypassToggle.checked = Boolean(state.runtime.bypass);
|
||||||
compileStatus.textContent = state.runtime.compileMessage || "No compiler output.";
|
compileStatus.textContent = state.runtime.compileMessage || "No compiler output.";
|
||||||
|
resetParametersButton.disabled = !activeShader || activeShader.parameters.length === 0;
|
||||||
|
|
||||||
createKv(runtimeStatus, [
|
createKv(runtimeStatus, [
|
||||||
["Active Shader", activeShader?.name || "None"],
|
["Active Shader", activeShader?.name || "None"],
|
||||||
["Auto Reload", state.app.autoReload ? "On" : "Off"],
|
["Auto Reload", state.app.autoReload ? "On" : "Off"],
|
||||||
["Control URL", `http://127.0.0.1:${state.app.serverPort}`],
|
["Control URL", `http://127.0.0.1:${state.app.serverPort}`],
|
||||||
["Compile Status", state.runtime.compileSucceeded ? "Ready" : "Error"],
|
["Compile Status", state.runtime.compileSucceeded ? "Ready" : "Error"],
|
||||||
|
["Render Time", `${renderMs.toFixed(2)} ms`],
|
||||||
|
["Smoothed Time", `${smoothedRenderMs.toFixed(2)} ms`],
|
||||||
|
["Frame Budget", `${frameBudgetMs.toFixed(2)} ms`],
|
||||||
|
["Budget Used", `${budgetUsedPercent.toFixed(1)}%`],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
createKv(videoStatus, [
|
createKv(videoStatus, [
|
||||||
@@ -209,4 +220,13 @@ reloadButton.addEventListener("click", () => {
|
|||||||
postJson("/api/reload", {});
|
postJson("/api/reload", {});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
resetParametersButton.addEventListener("click", () => {
|
||||||
|
const activeShaderId = appState?.runtime?.activeShaderId;
|
||||||
|
if (!activeShaderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postJson("/api/reset-parameters", { shaderId: activeShaderId });
|
||||||
|
});
|
||||||
|
|
||||||
loadInitialState().then(connectWebSocket);
|
loadInitialState().then(connectWebSocket);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel__header">
|
<div class="panel__header">
|
||||||
<h2>Parameters</h2>
|
<h2>Parameters</h2>
|
||||||
|
<button id="reset-parameters-button" type="button">Reset Parameters</button>
|
||||||
</div>
|
</div>
|
||||||
<form id="parameter-form" class="parameter-grid"></form>
|
<form id="parameter-form" class="parameter-grid"></form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -55,9 +55,18 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.panel__header {
|
.panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel__header button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-grid {
|
.status-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user