Compare commits
16 Commits
07a5c91427
...
v0.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 27bf2ae45c | |||
| 1ea44ba3ae | |||
| 0af9a72937 | |||
| d650cac857 | |||
| a0cc86f189 | |||
| f322abf79a | |||
| eede6938cb | |||
| ad24a20fdb | |||
| 5ae43513a7 | |||
| cc23e73d51 | |||
| f85abef237 | |||
| 596d370f43 | |||
| 87cb55b80b | |||
| f458eb0130 | |||
| 7d8f9a39d1 | |||
| 5b6e30ad13 |
@@ -64,8 +64,11 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
||||
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
||||
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
||||
"${APP_DIR}/gl/renderer/RenderTargetPool.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
|
||||
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
|
||||
|
||||
28
README.md
28
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
|
||||
|
||||
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
|
||||
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
@@ -62,6 +62,14 @@ npm run build
|
||||
|
||||
The native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development.
|
||||
|
||||
The control UI provides:
|
||||
|
||||
- A searchable shader library for adding layers.
|
||||
- Compact parameter rows with inline descriptions and OSC copy controls.
|
||||
- Stack save/recall presets.
|
||||
- Manual shader reload.
|
||||
- Screenshot capture from the final output render target.
|
||||
|
||||
## Package
|
||||
|
||||
Build the UI, build the native Release target, then install into a portable runtime folder:
|
||||
@@ -121,8 +129,9 @@ Current native test coverage includes:
|
||||
- JSON parsing and serialization.
|
||||
- Parameter normalization and preset filename safety.
|
||||
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
|
||||
- Video I/O format helpers, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
|
||||
- OSC packet parsing.
|
||||
- Slang validation for every available shader package.
|
||||
|
||||
## Runtime Configuration
|
||||
|
||||
@@ -182,7 +191,11 @@ http://127.0.0.1:<serverPort>/docs
|
||||
|
||||
Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
|
||||
|
||||
The control UI also has a Screenshot button. It queues a capture of the final output render target and writes a PNG under:
|
||||
The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, queues shader compilation, refreshes shader availability/errors, and keeps the previous working shader stack running if a changed shader fails to compile.
|
||||
|
||||
Each parameter row also includes a small **OSC** button. Clicking it copies that parameter's OSC route to the clipboard.
|
||||
|
||||
The control UI also has a **Screenshot** button. It queues a capture of the final output render target and writes a PNG under:
|
||||
|
||||
```text
|
||||
runtime/screenshots/
|
||||
@@ -206,10 +219,11 @@ Each shader package lives under:
|
||||
shaders/<id>/
|
||||
shader.json
|
||||
shader.slang
|
||||
optional-extra-pass.slang
|
||||
optional-font-or-texture-assets
|
||||
```
|
||||
|
||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license.
|
||||
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
|
||||
|
||||
## Generated Files
|
||||
|
||||
@@ -249,15 +263,13 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
|
||||
|
||||
- Audio.
|
||||
- Genlock.
|
||||
- Find a better UI library for React.
|
||||
- Logs.
|
||||
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
|
||||
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
|
||||
- Add WebView2
|
||||
- Add WebView2 for an embedded native control surface.
|
||||
- MSDF typography rasterisation
|
||||
- More shader-library organisation and filtering as the built-in library grows.
|
||||
- linear compositing?
|
||||
- Optional linear-light compositing mode.
|
||||
- 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
|
||||
- Mipmapping for shader-declared textures
|
||||
- Multipass for shaders at request
|
||||
|
||||
@@ -55,7 +55,7 @@ float4 shadeVideo(ShaderContext context)
|
||||
}
|
||||
```
|
||||
|
||||
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically.
|
||||
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. You can also use **Reload shaders** in the control UI to manually rescan the shader library.
|
||||
|
||||
## Guidance For Shaders
|
||||
|
||||
@@ -80,7 +80,7 @@ Important rules:
|
||||
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
|
||||
- If the source license is unclear or incompatible, do not add the shader package.
|
||||
|
||||
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector.
|
||||
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. CI also runs shader validation, so every available package in `shaders/` should compile successfully. Intentionally broken examples should stay visibly marked as broken rather than pretending to be production shaders.
|
||||
|
||||
## Manifest Fields
|
||||
|
||||
@@ -97,10 +97,13 @@ Optional fields:
|
||||
- `description`: display/help text for the shader library.
|
||||
- `category`: UI grouping label.
|
||||
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
|
||||
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
|
||||
- `textures`: texture assets to load and expose as samplers.
|
||||
- `fonts`: packaged font assets for live text parameters.
|
||||
- `temporal`: history-buffer requirements.
|
||||
|
||||
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
|
||||
|
||||
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
|
||||
- `entryPoint`
|
||||
@@ -110,6 +113,87 @@ Shader-visible identifiers must be valid Slang-style identifiers:
|
||||
|
||||
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
|
||||
|
||||
## Render Passes
|
||||
|
||||
Most shaders should omit `passes`. The runtime then creates one implicit pass:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "main",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "shadeVideo",
|
||||
"output": "layerOutput"
|
||||
}
|
||||
```
|
||||
|
||||
Advanced shaders may declare explicit passes. All passes may live in one `.slang` file by using different `entryPoint` values, or they may be split across multiple source files:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "blurX",
|
||||
"source": "blur-x.slang",
|
||||
"entryPoint": "blurHorizontal",
|
||||
"inputs": ["layerInput"],
|
||||
"output": "blurredX"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "final.slang",
|
||||
"entryPoint": "finish",
|
||||
"inputs": ["blurredX"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Pass fields:
|
||||
|
||||
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
|
||||
- `source`: required Slang source path relative to the package directory.
|
||||
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
|
||||
- `inputs`: optional list of named inputs. The first input is used as the pass input texture.
|
||||
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
|
||||
|
||||
Pass input names:
|
||||
|
||||
- `layerInput`: the input to this layer, before any of its passes run.
|
||||
- `previousPass`: the previous pass output in this layer. If there is no previous pass, this falls back to `layerInput`.
|
||||
- Any earlier pass `id` or `output` name from the same layer.
|
||||
|
||||
If `inputs` is omitted, the first pass samples `layerInput` and later passes sample `previousPass`.
|
||||
|
||||
Single-file multipass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "mask",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "makeMask",
|
||||
"output": "maskBuffer"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "finish",
|
||||
"inputs": ["maskBuffer"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Pass output names:
|
||||
|
||||
- `layerOutput`: the final visible output of this layer.
|
||||
- Any other name creates an intermediate 16-bit float render target that later passes may sample.
|
||||
|
||||
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
|
||||
|
||||
## Slang Entry Point
|
||||
|
||||
Your shader file must implement the manifest `entryPoint`.
|
||||
@@ -177,7 +261,7 @@ Fields:
|
||||
Color/precision notes:
|
||||
|
||||
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
|
||||
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback.
|
||||
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback. If external keying is enabled, output prefers 10-bit YUVA (`Ay10`) when supported so shader alpha can drive the key signal, then falls back to 8-bit BGRA.
|
||||
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
|
||||
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
|
||||
|
||||
@@ -547,6 +631,8 @@ When a shader compiles, the runtime writes generated files under `runtime/shader
|
||||
|
||||
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
|
||||
|
||||
For multipass shaders, these files reflect the most recently compiled pass. If a package has several passes, the reported compile error and pass name are usually more useful than assuming the cache contains the first pass.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
|
||||
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
|
||||
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
|
||||
<ClCompile Include="gl\renderer\RenderTargetPool.cpp" />
|
||||
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
|
||||
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
|
||||
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
|
||||
@@ -206,7 +207,9 @@
|
||||
<ClInclude Include="gl\OpenGLComposite.h" />
|
||||
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
|
||||
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
|
||||
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h" />
|
||||
<ClInclude Include="gl\renderer\OpenGLRenderer.h" />
|
||||
<ClInclude Include="gl\renderer\RenderTargetPool.h" />
|
||||
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h" />
|
||||
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h" />
|
||||
<ClInclude Include="gl\shader\ShaderBuildQueue.h" />
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\renderer\RenderTargetPool.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
@@ -92,9 +95,15 @@
|
||||
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\renderer\OpenGLRenderer.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\renderer\RenderTargetPool.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
|
||||
@@ -105,7 +105,8 @@ bool OpenGLComposite::InitVideoIO()
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
if (!mVideoIO->SelectPreferredFormats(videoModes, initFailureReason))
|
||||
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
|
||||
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
|
||||
goto error;
|
||||
|
||||
if (! CheckOpenGLExtensions())
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
|
||||
mRenderer(renderer)
|
||||
{
|
||||
@@ -43,26 +45,16 @@ void OpenGLRenderPass::Render(
|
||||
}
|
||||
else
|
||||
{
|
||||
GLuint sourceTexture = mRenderer.DecodedTexture();
|
||||
GLuint sourceFrameBuffer = mRenderer.DecodeFramebuffer();
|
||||
for (std::size_t index = 0; index < layerStates.size() && index < layerPrograms.size(); ++index)
|
||||
const std::vector<RenderPassDescriptor> passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
|
||||
for (const RenderPassDescriptor& pass : passes)
|
||||
{
|
||||
const std::size_t remaining = layerStates.size() - index;
|
||||
const bool writeToMain = (remaining % 2) == 1;
|
||||
RenderShaderProgram(
|
||||
sourceTexture,
|
||||
writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer(),
|
||||
layerPrograms[index],
|
||||
layerStates[index],
|
||||
RenderLayerPass(
|
||||
pass,
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
historyCap,
|
||||
updateTextBinding,
|
||||
updateGlobalParams);
|
||||
if (layerStates[index].temporalHistorySource == TemporalHistorySource::PreLayerInput)
|
||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(layerStates[index].layerId, sourceFrameBuffer, inputFrameWidth, inputFrameHeight);
|
||||
sourceTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
|
||||
sourceFrameBuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +89,158 @@ void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned input
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
std::vector<LayerProgram>& layerPrograms) const
|
||||
{
|
||||
// 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
|
||||
// per layer.
|
||||
std::vector<RenderPassDescriptor> passes;
|
||||
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
|
||||
std::size_t descriptorCount = 0;
|
||||
for (std::size_t index = 0; index < passCount; ++index)
|
||||
descriptorCount += layerPrograms[index].passes.size();
|
||||
passes.reserve(descriptorCount);
|
||||
|
||||
GLuint sourceTexture = mRenderer.DecodedTexture();
|
||||
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
|
||||
for (std::size_t index = 0; index < passCount; ++index)
|
||||
{
|
||||
const RuntimeRenderState& state = layerStates[index];
|
||||
LayerProgram& layerProgram = layerPrograms[index];
|
||||
if (layerProgram.passes.empty())
|
||||
continue;
|
||||
|
||||
// Preserve the original two-target layer ping-pong. Intermediate passes
|
||||
// inside this layer are routed through pooled temporary targets instead.
|
||||
const std::size_t remaining = layerStates.size() - index;
|
||||
const bool writeToMain = (remaining % 2) == 1;
|
||||
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
|
||||
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
|
||||
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
|
||||
|
||||
const GLuint layerInputTexture = sourceTexture;
|
||||
const GLuint layerInputFramebuffer = sourceFramebuffer;
|
||||
GLuint previousPassTexture = layerInputTexture;
|
||||
GLuint previousPassFramebuffer = layerInputFramebuffer;
|
||||
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
|
||||
std::size_t temporaryTargetIndex = 0;
|
||||
|
||||
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
|
||||
{
|
||||
PassProgram& passProgram = layerProgram.passes[passIndex];
|
||||
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
|
||||
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
|
||||
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
|
||||
|
||||
GLuint passSourceTexture = previousPassTexture;
|
||||
GLuint passSourceFramebuffer = previousPassFramebuffer;
|
||||
if (!passProgram.inputNames.empty())
|
||||
{
|
||||
// v1 multipass uses the first declared input as gVideoInput.
|
||||
// Later inputs are parsed for forward compatibility.
|
||||
const std::string& inputName = passProgram.inputNames.front();
|
||||
if (inputName == "layerInput")
|
||||
{
|
||||
passSourceTexture = layerInputTexture;
|
||||
passSourceFramebuffer = layerInputFramebuffer;
|
||||
}
|
||||
else if (inputName == "previousPass")
|
||||
{
|
||||
passSourceTexture = previousPassTexture;
|
||||
passSourceFramebuffer = previousPassFramebuffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto namedOutputIt = namedOutputs.find(inputName);
|
||||
if (namedOutputIt != namedOutputs.end())
|
||||
{
|
||||
passSourceTexture = namedOutputIt->second.first;
|
||||
passSourceFramebuffer = namedOutputIt->second.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint passDestinationTexture = layerOutputTexture;
|
||||
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
|
||||
RenderPassOutputTarget outputTarget = layerOutputTarget;
|
||||
if (!writesLayerOutput)
|
||||
{
|
||||
// Temporary targets are reserved when the shader stack is
|
||||
// committed, avoiding texture allocation during playback.
|
||||
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
|
||||
{
|
||||
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
|
||||
++temporaryTargetIndex;
|
||||
passDestinationTexture = temporaryTarget.texture;
|
||||
passDestinationFramebuffer = temporaryTarget.framebuffer;
|
||||
outputTarget = RenderPassOutputTarget::Temporary;
|
||||
}
|
||||
}
|
||||
|
||||
RenderPassDescriptor pass;
|
||||
pass.kind = RenderPassKind::LayerEffect;
|
||||
pass.outputTarget = outputTarget;
|
||||
pass.passIndex = passes.size();
|
||||
pass.passId = passProgram.passId;
|
||||
pass.layerId = state.layerId;
|
||||
pass.shaderId = state.shaderId;
|
||||
pass.sourceTexture = passSourceTexture;
|
||||
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
|
||||
pass.destinationTexture = passDestinationTexture;
|
||||
pass.destinationFramebuffer = passDestinationFramebuffer;
|
||||
pass.layerProgram = &layerProgram;
|
||||
pass.passProgram = &passProgram;
|
||||
pass.layerState = &state;
|
||||
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
||||
passes.push_back(pass);
|
||||
|
||||
// A later pass can reference either the explicit output name or the
|
||||
// pass id, which keeps small manifests pleasant to write.
|
||||
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
||||
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
||||
previousPassTexture = passDestinationTexture;
|
||||
previousPassFramebuffer = passDestinationFramebuffer;
|
||||
}
|
||||
|
||||
sourceTexture = layerOutputTexture;
|
||||
sourceFramebuffer = layerOutputFramebuffer;
|
||||
}
|
||||
|
||||
return passes;
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderLayerPass(
|
||||
const RenderPassDescriptor& pass,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams)
|
||||
{
|
||||
if (pass.passProgram == nullptr || pass.layerState == nullptr)
|
||||
return;
|
||||
|
||||
RenderShaderProgram(
|
||||
pass.sourceTexture,
|
||||
pass.destinationFramebuffer,
|
||||
*pass.passProgram,
|
||||
*pass.layerState,
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
historyCap,
|
||||
updateTextBinding,
|
||||
updateGlobalParams);
|
||||
|
||||
if (pass.capturePreLayerHistory)
|
||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderShaderProgram(
|
||||
GLuint sourceTexture,
|
||||
GLuint destinationFrameBuffer,
|
||||
LayerProgram& layerProgram,
|
||||
PassProgram& passProgram,
|
||||
const RuntimeRenderState& state,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
@@ -108,7 +248,7 @@ void OpenGLRenderPass::RenderShaderProgram(
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams)
|
||||
{
|
||||
for (LayerProgram::TextBinding& textBinding : layerProgram.textBindings)
|
||||
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
|
||||
{
|
||||
std::string textError;
|
||||
if (!updateTextBinding(state, textBinding, textError))
|
||||
@@ -118,52 +258,18 @@ void OpenGLRenderPass::RenderShaderProgram(
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, sourceTexture);
|
||||
mRenderer.TemporalHistory().BindSamplers(state, sourceTexture, historyCap);
|
||||
BindLayerTextureAssets(layerProgram);
|
||||
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
|
||||
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(layerProgram.program);
|
||||
glUseProgram(passProgram.program);
|
||||
// The UBO is shared by every pass in a layer; texture routing is what
|
||||
// changes from pass to pass.
|
||||
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
UnbindLayerTextureAssets(layerProgram, historyCap);
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::BindLayerTextureAssets(const LayerProgram& layerProgram)
|
||||
{
|
||||
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
|
||||
for (std::size_t index = 0; index < layerProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
|
||||
glBindTexture(GL_TEXTURE_2D, layerProgram.textureBindings[index].texture);
|
||||
}
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(layerProgram.textureBindings.size());
|
||||
for (std::size_t index = 0; index < layerProgram.textBindings.size(); ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + textTextureBase + static_cast<GLuint>(index));
|
||||
glBindTexture(GL_TEXTURE_2D, layerProgram.textBindings[index].texture);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap)
|
||||
{
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + historyCap + index);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
const GLuint shaderTextureBase = layerProgram.shaderTextureBase != 0 ? layerProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
|
||||
for (std::size_t index = 0; index < layerProgram.textureBindings.size() + layerProgram.textBindings.size(); ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + shaderTextureBase + static_cast<GLuint>(index));
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0 + kDecodedVideoTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RenderPassDescriptor.h"
|
||||
#include "ShaderTextureBindings.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
@@ -12,6 +14,7 @@ class OpenGLRenderPass
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
|
||||
|
||||
@@ -30,18 +33,27 @@ public:
|
||||
|
||||
private:
|
||||
void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat);
|
||||
std::vector<RenderPassDescriptor> BuildLayerPassDescriptors(
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
std::vector<LayerProgram>& layerPrograms) const;
|
||||
void RenderLayerPass(
|
||||
const RenderPassDescriptor& pass,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams);
|
||||
void RenderShaderProgram(
|
||||
GLuint sourceTexture,
|
||||
GLuint destinationFrameBuffer,
|
||||
LayerProgram& layerProgram,
|
||||
PassProgram& passProgram,
|
||||
const RuntimeRenderState& state,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams);
|
||||
void BindLayerTextureAssets(const LayerProgram& layerProgram);
|
||||
void UnbindLayerTextureAssets(const LayerProgram& layerProgram, unsigned historyCap);
|
||||
|
||||
OpenGLRenderer& mRenderer;
|
||||
ShaderTextureBindings mTextureBindings;
|
||||
};
|
||||
|
||||
@@ -34,8 +34,8 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
if (mOutputReady)
|
||||
mOutputReady();
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||
PackOutputForV210(state);
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
||||
PackOutputFor10Bit(state);
|
||||
glFlush();
|
||||
|
||||
const auto renderEndTime = std::chrono::steady_clock::now();
|
||||
@@ -50,7 +50,7 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::PackOutputForV210(const VideoIOState& state)
|
||||
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
|
||||
@@ -64,10 +64,13 @@ void OpenGLRenderPipeline::PackOutputForV210(const VideoIOState& state)
|
||||
|
||||
const GLint outputResolutionLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputVideoResolution");
|
||||
const GLint activeWordsLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uActiveV210Words");
|
||||
const GLint packFormatLocation = glGetUniformLocation(mRenderer.OutputPackProgram(), "uOutputPackFormat");
|
||||
if (outputResolutionLocation >= 0)
|
||||
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
||||
if (activeWordsLocation >= 0)
|
||||
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
|
||||
if (packFormatLocation >= 0)
|
||||
glUniform1i(packFormatLocation, state.outputPixelFormat == VideoIOPixelFormat::Yuva10 ? 2 : 1);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
@@ -79,7 +82,7 @@ void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOut
|
||||
{
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210)
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, outputFrame.bytes);
|
||||
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
|
||||
private:
|
||||
void PackOutputForV210(const VideoIOState& state);
|
||||
void PackOutputFor10Bit(const VideoIOState& state);
|
||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||
|
||||
OpenGLRenderer& mRenderer;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
enum class RenderPassKind
|
||||
{
|
||||
LayerEffect
|
||||
};
|
||||
|
||||
enum class RenderPassOutputTarget
|
||||
{
|
||||
Temporary,
|
||||
LayerTemp,
|
||||
Composite
|
||||
};
|
||||
|
||||
struct RenderPassDescriptor
|
||||
{
|
||||
RenderPassKind kind = RenderPassKind::LayerEffect;
|
||||
RenderPassOutputTarget outputTarget = RenderPassOutputTarget::Composite;
|
||||
std::size_t passIndex = 0;
|
||||
std::string passId;
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
GLuint sourceTexture = 0;
|
||||
GLuint sourceFramebuffer = 0;
|
||||
GLuint destinationTexture = 0;
|
||||
GLuint destinationFramebuffer = 0;
|
||||
OpenGLRenderer::LayerProgram* layerProgram = nullptr;
|
||||
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
||||
const RuntimeRenderState* layerState = nullptr;
|
||||
bool capturePreLayerHistory = false;
|
||||
};
|
||||
@@ -212,6 +212,29 @@ void TemporalHistoryBuffers::BindSamplers(const RuntimeRenderState& state, GLuin
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const
|
||||
{
|
||||
std::vector<GLuint> textures;
|
||||
textures.reserve(historyCap);
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
textures.push_back(ResolveTexture(sourceHistoryRing, fallbackTexture, index));
|
||||
return textures;
|
||||
}
|
||||
|
||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const
|
||||
{
|
||||
const Ring* temporalRing = nullptr;
|
||||
auto it = preLayerHistoryByLayerId.find(state.layerId);
|
||||
if (it != preLayerHistoryByLayerId.end())
|
||||
temporalRing = &it->second;
|
||||
|
||||
std::vector<GLuint> textures;
|
||||
textures.reserve(historyCap);
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
textures.push_back(temporalRing ? ResolveTexture(*temporalRing, fallbackTexture, index) : fallbackTexture);
|
||||
return textures;
|
||||
}
|
||||
|
||||
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
|
||||
{
|
||||
if (ring.filledCount == 0 || ring.slots.empty())
|
||||
|
||||
@@ -40,6 +40,8 @@ public:
|
||||
void PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap);
|
||||
std::vector<GLuint> ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const;
|
||||
std::vector<GLuint> ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const;
|
||||
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
||||
unsigned SourceAvailableCount() const;
|
||||
unsigned AvailableCountForLayer(const std::string& layerId) const;
|
||||
|
||||
@@ -12,15 +12,6 @@ namespace
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
}
|
||||
|
||||
void ConfigureDisplayFrameTexture(unsigned width, unsigned height)
|
||||
{
|
||||
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_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error)
|
||||
@@ -35,80 +26,32 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
|
||||
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenTextures(1, &mDecodedTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mDecodedTexture);
|
||||
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenTextures(1, &mLayerTempTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mLayerTempTexture);
|
||||
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &mDecodeFrameBuf);
|
||||
glGenFramebuffers(1, &mLayerTempFrameBuf);
|
||||
glGenFramebuffers(1, &mIdFrameBuf);
|
||||
glGenFramebuffers(1, &mOutputFrameBuf);
|
||||
glGenFramebuffers(1, &mOutputPackFrameBuf);
|
||||
glGenRenderbuffers(1, &mIdColorBuf);
|
||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||
glGenVertexArrays(1, &mFullscreenVAO);
|
||||
glGenBuffers(1, &mGlobalParamsUBO);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFrameBuf);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mDecodedTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize decode framebuffer.";
|
||||
if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerTempFrameBuf);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTempTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize layer framebuffer.";
|
||||
if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
|
||||
return false;
|
||||
if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mIdFrameBuf);
|
||||
glGenTextures(1, &mFBOTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mFBOTexture);
|
||||
ConfigureDisplayFrameTexture(inputFrameWidth, inputFrameHeight);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mFBOTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize framebuffer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &mOutputTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mOutputTexture);
|
||||
ConfigureDisplayFrameTexture(outputFrameWidth, outputFrameHeight);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mOutputFrameBuf);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize output framebuffer.";
|
||||
if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &mOutputPackTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mOutputPackTexture);
|
||||
ConfigureByteFrameTexture(outputPackTextureWidth, outputFrameHeight);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mOutputPackFrameBuf);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mOutputPackTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize output pack framebuffer.";
|
||||
if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
@@ -137,6 +80,11 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
|
||||
mOutputPackFragmentShader = fragmentShader;
|
||||
}
|
||||
|
||||
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
|
||||
{
|
||||
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
|
||||
}
|
||||
|
||||
void OpenGLRenderer::ResizeView(int width, int height)
|
||||
{
|
||||
mViewWidth = width;
|
||||
@@ -169,7 +117,7 @@ void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigne
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mOutputFrameBuf);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, OutputFramebuffer());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, mViewWidth, mViewHeight);
|
||||
@@ -186,50 +134,21 @@ void OpenGLRenderer::DestroyResources()
|
||||
glDeleteVertexArrays(1, &mFullscreenVAO);
|
||||
if (mGlobalParamsUBO != 0)
|
||||
glDeleteBuffers(1, &mGlobalParamsUBO);
|
||||
if (mDecodeFrameBuf != 0)
|
||||
glDeleteFramebuffers(1, &mDecodeFrameBuf);
|
||||
if (mLayerTempFrameBuf != 0)
|
||||
glDeleteFramebuffers(1, &mLayerTempFrameBuf);
|
||||
if (mIdFrameBuf != 0)
|
||||
glDeleteFramebuffers(1, &mIdFrameBuf);
|
||||
if (mOutputFrameBuf != 0)
|
||||
glDeleteFramebuffers(1, &mOutputFrameBuf);
|
||||
if (mOutputPackFrameBuf != 0)
|
||||
glDeleteFramebuffers(1, &mOutputPackFrameBuf);
|
||||
if (mIdColorBuf != 0)
|
||||
glDeleteRenderbuffers(1, &mIdColorBuf);
|
||||
if (mIdDepthBuf != 0)
|
||||
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
||||
if (mCaptureTexture != 0)
|
||||
glDeleteTextures(1, &mCaptureTexture);
|
||||
if (mDecodedTexture != 0)
|
||||
glDeleteTextures(1, &mDecodedTexture);
|
||||
if (mLayerTempTexture != 0)
|
||||
glDeleteTextures(1, &mLayerTempTexture);
|
||||
if (mFBOTexture != 0)
|
||||
glDeleteTextures(1, &mFBOTexture);
|
||||
if (mOutputTexture != 0)
|
||||
glDeleteTextures(1, &mOutputTexture);
|
||||
if (mOutputPackTexture != 0)
|
||||
glDeleteTextures(1, &mOutputPackTexture);
|
||||
if (mTextureUploadBuffer != 0)
|
||||
glDeleteBuffers(1, &mTextureUploadBuffer);
|
||||
mRenderTargets.Destroy();
|
||||
|
||||
mFullscreenVAO = 0;
|
||||
mGlobalParamsUBO = 0;
|
||||
mDecodeFrameBuf = 0;
|
||||
mLayerTempFrameBuf = 0;
|
||||
mIdFrameBuf = 0;
|
||||
mOutputFrameBuf = 0;
|
||||
mOutputPackFrameBuf = 0;
|
||||
mIdColorBuf = 0;
|
||||
mIdDepthBuf = 0;
|
||||
mCaptureTexture = 0;
|
||||
mDecodedTexture = 0;
|
||||
mLayerTempTexture = 0;
|
||||
mFBOTexture = 0;
|
||||
mOutputTexture = 0;
|
||||
mOutputPackTexture = 0;
|
||||
mTextureUploadBuffer = 0;
|
||||
mGlobalParamsUBOSize = 0;
|
||||
|
||||
@@ -241,43 +160,47 @@ void OpenGLRenderer::DestroyResources()
|
||||
|
||||
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
||||
{
|
||||
for (LayerProgram::TextureBinding& binding : layerProgram.textureBindings)
|
||||
for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
|
||||
{
|
||||
if (binding.texture != 0)
|
||||
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
if (binding.texture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
}
|
||||
}
|
||||
passProgram.textureBindings.clear();
|
||||
|
||||
for (LayerProgram::TextBinding& binding : passProgram.textBindings)
|
||||
{
|
||||
if (binding.texture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
}
|
||||
}
|
||||
passProgram.textBindings.clear();
|
||||
|
||||
if (passProgram.program != 0)
|
||||
{
|
||||
glDeleteProgram(passProgram.program);
|
||||
passProgram.program = 0;
|
||||
}
|
||||
|
||||
if (passProgram.fragmentShader != 0)
|
||||
{
|
||||
glDeleteShader(passProgram.fragmentShader);
|
||||
passProgram.fragmentShader = 0;
|
||||
}
|
||||
|
||||
if (passProgram.vertexShader != 0)
|
||||
{
|
||||
glDeleteShader(passProgram.vertexShader);
|
||||
passProgram.vertexShader = 0;
|
||||
}
|
||||
}
|
||||
layerProgram.textureBindings.clear();
|
||||
|
||||
for (LayerProgram::TextBinding& binding : layerProgram.textBindings)
|
||||
{
|
||||
if (binding.texture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
}
|
||||
}
|
||||
layerProgram.textBindings.clear();
|
||||
|
||||
if (layerProgram.program != 0)
|
||||
{
|
||||
glDeleteProgram(layerProgram.program);
|
||||
layerProgram.program = 0;
|
||||
}
|
||||
|
||||
if (layerProgram.fragmentShader != 0)
|
||||
{
|
||||
glDeleteShader(layerProgram.fragmentShader);
|
||||
layerProgram.fragmentShader = 0;
|
||||
}
|
||||
|
||||
if (layerProgram.vertexShader != 0)
|
||||
{
|
||||
glDeleteShader(layerProgram.vertexShader);
|
||||
layerProgram.vertexShader = 0;
|
||||
}
|
||||
layerProgram.passes.clear();
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroyLayerPrograms()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "RenderTargetPool.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "TemporalHistoryBuffers.h"
|
||||
|
||||
@@ -36,26 +37,35 @@ public:
|
||||
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
GLuint shaderTextureBase = 0;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
std::vector<TextureBinding> textureBindings;
|
||||
std::vector<TextBinding> textBindings;
|
||||
|
||||
struct PassProgram
|
||||
{
|
||||
std::string passId;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
GLuint shaderTextureBase = 0;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
std::vector<TextureBinding> textureBindings;
|
||||
std::vector<TextBinding> textBindings;
|
||||
};
|
||||
|
||||
std::vector<PassProgram> passes;
|
||||
};
|
||||
|
||||
GLuint CaptureTexture() const { return mCaptureTexture; }
|
||||
GLuint DecodedTexture() const { return mDecodedTexture; }
|
||||
GLuint LayerTempTexture() const { return mLayerTempTexture; }
|
||||
GLuint CompositeTexture() const { return mFBOTexture; }
|
||||
GLuint OutputTexture() const { return mOutputTexture; }
|
||||
GLuint OutputPackTexture() const { return mOutputPackTexture; }
|
||||
GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
|
||||
GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
|
||||
GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
|
||||
GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
|
||||
GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
|
||||
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
|
||||
GLuint DecodeFramebuffer() const { return mDecodeFrameBuf; }
|
||||
GLuint LayerTempFramebuffer() const { return mLayerTempFrameBuf; }
|
||||
GLuint CompositeFramebuffer() const { return mIdFrameBuf; }
|
||||
GLuint OutputFramebuffer() const { return mOutputFrameBuf; }
|
||||
GLuint OutputPackFramebuffer() const { return mOutputPackFrameBuf; }
|
||||
GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
|
||||
GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
|
||||
GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
|
||||
GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
|
||||
GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
|
||||
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
|
||||
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
||||
GLuint DecodeProgram() const { return mDecodeProgram; }
|
||||
@@ -65,6 +75,9 @@ public:
|
||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
||||
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
||||
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
|
||||
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
|
||||
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
||||
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
||||
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
||||
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||
@@ -80,17 +93,7 @@ public:
|
||||
|
||||
private:
|
||||
GLuint mCaptureTexture = 0;
|
||||
GLuint mDecodedTexture = 0;
|
||||
GLuint mLayerTempTexture = 0;
|
||||
GLuint mFBOTexture = 0;
|
||||
GLuint mOutputTexture = 0;
|
||||
GLuint mOutputPackTexture = 0;
|
||||
GLuint mTextureUploadBuffer = 0;
|
||||
GLuint mDecodeFrameBuf = 0;
|
||||
GLuint mLayerTempFrameBuf = 0;
|
||||
GLuint mIdFrameBuf = 0;
|
||||
GLuint mOutputFrameBuf = 0;
|
||||
GLuint mOutputPackFrameBuf = 0;
|
||||
GLuint mIdColorBuf = 0;
|
||||
GLuint mIdDepthBuf = 0;
|
||||
GLuint mFullscreenVAO = 0;
|
||||
@@ -105,5 +108,6 @@ private:
|
||||
int mViewWidth = 0;
|
||||
int mViewHeight = 0;
|
||||
std::vector<LayerProgram> mLayerPrograms;
|
||||
RenderTargetPool mRenderTargets;
|
||||
TemporalHistoryBuffers mTemporalHistory;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "RenderTargetPool.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace
|
||||
{
|
||||
void ConfigureRenderTargetTexture(
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType)
|
||||
{
|
||||
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, internalFormat, width, height, 0, pixelFormat, pixelType, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTargetPool::Create(
|
||||
RenderTargetId id,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
const char* errorPrefix,
|
||||
std::string& error)
|
||||
{
|
||||
RenderTarget& target = mTargets[TargetIndex(id)];
|
||||
if (target.texture != 0 || target.framebuffer != 0)
|
||||
{
|
||||
error = std::string(errorPrefix) + " render target was already initialized.";
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &target.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = std::string("Cannot initialize ") + errorPrefix + " framebuffer.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
target.width = width;
|
||||
target.height = height;
|
||||
target.internalFormat = internalFormat;
|
||||
target.pixelFormat = pixelFormat;
|
||||
target.pixelType = pixelType;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderTargetPool::ReserveTemporaryTargets(
|
||||
std::size_t count,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
std::string& error)
|
||||
{
|
||||
if (mTemporaryTargets.size() == count)
|
||||
return true;
|
||||
|
||||
DestroyTemporaryTargets();
|
||||
|
||||
mTemporaryTargets.resize(count);
|
||||
for (std::size_t index = 0; index < mTemporaryTargets.size(); ++index)
|
||||
{
|
||||
RenderTarget& target = mTemporaryTargets[index];
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &target.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize temporary render target.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.width = width;
|
||||
target.height = height;
|
||||
target.internalFormat = internalFormat;
|
||||
target.pixelFormat = pixelFormat;
|
||||
target.pixelType = pixelType;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderTargetPool::DestroyTemporaryTargets()
|
||||
{
|
||||
for (RenderTarget& target : mTemporaryTargets)
|
||||
{
|
||||
if (target.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &target.framebuffer);
|
||||
if (target.texture != 0)
|
||||
glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
mTemporaryTargets.clear();
|
||||
}
|
||||
|
||||
void RenderTargetPool::Destroy()
|
||||
{
|
||||
for (RenderTarget& target : mTargets)
|
||||
{
|
||||
if (target.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &target.framebuffer);
|
||||
if (target.texture != 0)
|
||||
glDeleteTextures(1, &target.texture);
|
||||
target = RenderTarget();
|
||||
}
|
||||
|
||||
DestroyTemporaryTargets();
|
||||
}
|
||||
|
||||
const RenderTarget& RenderTargetPool::Target(RenderTargetId id) const
|
||||
{
|
||||
return mTargets[TargetIndex(id)];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class RenderTargetId
|
||||
{
|
||||
Decoded,
|
||||
LayerTemp,
|
||||
Composite,
|
||||
Output,
|
||||
OutputPack,
|
||||
Count
|
||||
};
|
||||
|
||||
struct RenderTarget
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
GLenum internalFormat = GL_RGBA8;
|
||||
GLenum pixelFormat = GL_RGBA;
|
||||
GLenum pixelType = GL_UNSIGNED_BYTE;
|
||||
};
|
||||
|
||||
class RenderTargetPool
|
||||
{
|
||||
public:
|
||||
bool Create(
|
||||
RenderTargetId id,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
const char* errorPrefix,
|
||||
std::string& error);
|
||||
bool ReserveTemporaryTargets(
|
||||
std::size_t count,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
std::string& error);
|
||||
void DestroyTemporaryTargets();
|
||||
void Destroy();
|
||||
|
||||
GLuint Texture(RenderTargetId id) const { return Target(id).texture; }
|
||||
GLuint Framebuffer(RenderTargetId id) const { return Target(id).framebuffer; }
|
||||
const RenderTarget& Target(RenderTargetId id) const;
|
||||
const RenderTarget& TemporaryTarget(std::size_t index) const { return mTemporaryTargets[index]; }
|
||||
std::size_t TemporaryTargetCount() const { return mTemporaryTargets.size(); }
|
||||
|
||||
private:
|
||||
static std::size_t TargetIndex(RenderTargetId id) { return static_cast<std::size_t>(id); }
|
||||
|
||||
std::array<RenderTarget, static_cast<std::size_t>(RenderTargetId::Count)> mTargets;
|
||||
std::vector<RenderTarget> mTemporaryTargets;
|
||||
};
|
||||
@@ -90,12 +90,17 @@ const char* kOutputPackFragmentShaderSource =
|
||||
"layout(binding = 0) uniform sampler2D uOutputRgb;\n"
|
||||
"uniform vec2 uOutputVideoResolution;\n"
|
||||
"uniform float uActiveV210Words;\n"
|
||||
"uniform int uOutputPackFormat;\n"
|
||||
"in vec2 vTexCoord;\n"
|
||||
"layout(location = 0) out vec4 fragColor;\n"
|
||||
"vec3 rgbAt(int x, int y)\n"
|
||||
"vec4 rgbaAt(int x, int y)\n"
|
||||
"{\n"
|
||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0).rgb, vec3(0.0), vec3(1.0));\n"
|
||||
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0), vec4(0.0), vec4(1.0));\n"
|
||||
"}\n"
|
||||
"vec3 rgbAt(int x, int y)\n"
|
||||
"{\n"
|
||||
" return rgbaAt(x, y).rgb;\n"
|
||||
"}\n"
|
||||
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
|
||||
"{\n"
|
||||
@@ -112,9 +117,35 @@ const char* kOutputPackFragmentShaderSource =
|
||||
"{\n"
|
||||
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n"
|
||||
"}\n"
|
||||
"vec4 bigEndianWordToBytes(uint word)\n"
|
||||
"{\n"
|
||||
" return vec4(float((word >> 24) & 255u), float((word >> 16) & 255u), float((word >> 8) & 255u), float(word & 255u)) / 255.0;\n"
|
||||
"}\n"
|
||||
"vec4 packAy10Word(ivec2 outCoord)\n"
|
||||
"{\n"
|
||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" if (outCoord.x >= size.x)\n"
|
||||
" return vec4(0.0);\n"
|
||||
" int pixelBase = (outCoord.x / 2) * 2;\n"
|
||||
" int y = outCoord.y;\n"
|
||||
" vec4 rgba0 = rgbaAt(pixelBase + 0, y);\n"
|
||||
" vec4 rgba1 = rgbaAt(pixelBase + 1, y);\n"
|
||||
" vec3 c0 = rgbToLegalYcbcr10(rgba0.rgb);\n"
|
||||
" vec3 c1 = rgbToLegalYcbcr10(rgba1.rgb);\n"
|
||||
" float chroma = (outCoord.x & 1) == 0 ? round((c0.y + c1.y) * 0.5) : round((c0.z + c1.z) * 0.5);\n"
|
||||
" float alpha = round(clamp(((outCoord.x & 1) == 0 ? rgba0.a : rgba1.a), 0.0, 1.0) * 1023.0);\n"
|
||||
" float luma = (outCoord.x & 1) == 0 ? c0.x : c1.x;\n"
|
||||
" uint word = ((uint(luma) & 1023u) << 22) | ((uint(chroma) & 1023u) << 12) | ((uint(alpha) & 1023u) << 2);\n"
|
||||
" return bigEndianWordToBytes(word);\n"
|
||||
"}\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
|
||||
" if (uOutputPackFormat == 2)\n"
|
||||
" {\n"
|
||||
" fragColor = packAy10Word(outCoord);\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" if (float(outCoord.x) >= uActiveV210Words)\n"
|
||||
" {\n"
|
||||
" fragColor = vec4(0.0);\n"
|
||||
|
||||
@@ -13,6 +13,20 @@ void CopyErrorMessage(const std::string& message, int errorMessageSize, char* er
|
||||
|
||||
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::LayerProgram>& layerPrograms)
|
||||
{
|
||||
// Only one layer renders at a time, so the pool needs to cover the widest
|
||||
// layer, not the sum of every intermediate pass in the stack.
|
||||
std::size_t requiredTargets = 0;
|
||||
for (const OpenGLRenderer::LayerProgram& layerProgram : layerPrograms)
|
||||
{
|
||||
const std::size_t internalPasses = layerProgram.passes.size() > 0 ? layerProgram.passes.size() - 1 : 0;
|
||||
if (internalPasses > requiredTargets)
|
||||
requiredTargets = internalPasses;
|
||||
}
|
||||
return requiredTargets;
|
||||
}
|
||||
}
|
||||
|
||||
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
|
||||
@@ -39,6 +53,8 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initial startup still compiles synchronously; auto-reload uses the build
|
||||
// queue so Slang/file work stays off the playback path.
|
||||
std::vector<LayerProgram> newPrograms;
|
||||
newPrograms.reserve(layerStates.size());
|
||||
|
||||
@@ -54,6 +70,15 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
|
||||
newPrograms.push_back(layerProgram);
|
||||
}
|
||||
|
||||
std::string targetError;
|
||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
DestroyLayerPrograms();
|
||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||
mCommittedLayerStates = layerStates;
|
||||
@@ -85,13 +110,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
|
||||
return false;
|
||||
}
|
||||
|
||||
// The prepared build already contains GLSL text for each pass. This commit
|
||||
// step performs the short GL work on the render thread.
|
||||
std::vector<LayerProgram> newPrograms;
|
||||
newPrograms.reserve(preparedBuild.layers.size());
|
||||
|
||||
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
|
||||
{
|
||||
LayerProgram layerProgram;
|
||||
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.fragmentShaderSource, layerProgram, errorMessageSize, errorMessage))
|
||||
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.passes, layerProgram, errorMessageSize, errorMessage))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
@@ -100,6 +127,15 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
|
||||
newPrograms.push_back(layerProgram);
|
||||
}
|
||||
|
||||
std::string targetError;
|
||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
DestroyLayerPrograms();
|
||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||
mCommittedLayerStates = preparedBuild.layerStates;
|
||||
|
||||
@@ -120,7 +120,7 @@ PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned output
|
||||
{
|
||||
PreparedLayerShader layer;
|
||||
layer.state = state;
|
||||
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, layer.fragmentShaderSource, build.message))
|
||||
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
|
||||
{
|
||||
build.succeeded = false;
|
||||
return build;
|
||||
|
||||
@@ -14,7 +14,7 @@ class RuntimeHost;
|
||||
struct PreparedLayerShader
|
||||
{
|
||||
RuntimeRenderState state;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<ShaderPassBuildSource> passes;
|
||||
};
|
||||
|
||||
struct PreparedShaderBuild
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "GlShaderSources.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
@@ -27,125 +28,113 @@ ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHo
|
||||
|
||||
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<ShaderPassBuildSource> passSources;
|
||||
std::string loadError;
|
||||
|
||||
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError))
|
||||
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
|
||||
{
|
||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
return CompilePreparedLayerProgram(state, fragmentShaderSource, layerProgram, errorMessageSize, errorMessage);
|
||||
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
|
||||
}
|
||||
|
||||
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
GLsizei errorBufferSize = 0;
|
||||
GLint compileResult = GL_FALSE;
|
||||
GLint linkResult = GL_FALSE;
|
||||
std::string loadError;
|
||||
std::vector<LayerProgram::TextureBinding> textureBindings;
|
||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
||||
const char* fragmentSource = fragmentShaderSource.c_str();
|
||||
|
||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
||||
glCompileShader(newVertexShader.get());
|
||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
||||
glCompileShader(newFragmentShader.get());
|
||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlProgram newProgram(glCreateProgram());
|
||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
||||
glLinkProgram(newProgram.get());
|
||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
|
||||
{
|
||||
LayerProgram::TextureBinding textureBinding;
|
||||
textureBinding.samplerName = textureAsset.id;
|
||||
textureBinding.sourcePath = textureAsset.path;
|
||||
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
|
||||
{
|
||||
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
|
||||
{
|
||||
if (loadedTexture.texture != 0)
|
||||
glDeleteTextures(1, &loadedTexture.texture);
|
||||
}
|
||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
textureBindings.push_back(textureBinding);
|
||||
}
|
||||
|
||||
std::vector<LayerProgram::TextBinding> textBindings;
|
||||
mTextureBindings.CreateTextBindings(state, textBindings);
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
|
||||
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||
const GLuint shaderTextureBase = state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
||||
glUseProgram(newProgram.get());
|
||||
const GLint videoInputLocation = glGetUniformLocation(newProgram.get(), "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
|
||||
const GLint sourceSamplerLocation = glGetUniformLocation(newProgram.get(), sourceSamplerName.c_str());
|
||||
if (sourceSamplerLocation >= 0)
|
||||
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
|
||||
|
||||
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
|
||||
const GLint temporalSamplerLocation = glGetUniformLocation(newProgram.get(), temporalSamplerName.c_str());
|
||||
if (temporalSamplerLocation >= 0)
|
||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
||||
}
|
||||
for (std::size_t index = 0; index < textureBindings.size(); ++index)
|
||||
{
|
||||
const GLint textureSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textureBindings[index].samplerName);
|
||||
if (textureSamplerLocation >= 0)
|
||||
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(textureBindings.size());
|
||||
for (std::size_t index = 0; index < textBindings.size(); ++index)
|
||||
{
|
||||
const GLint textSamplerLocation = mTextureBindings.FindSamplerUniformLocation(newProgram.get(), textBindings[index].samplerName);
|
||||
if (textSamplerLocation >= 0)
|
||||
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
glUseProgram(0);
|
||||
|
||||
layerProgram.layerId = state.layerId;
|
||||
layerProgram.shaderId = state.shaderId;
|
||||
layerProgram.shaderTextureBase = shaderTextureBase;
|
||||
layerProgram.program = newProgram.release();
|
||||
layerProgram.vertexShader = newVertexShader.release();
|
||||
layerProgram.fragmentShader = newFragmentShader.release();
|
||||
layerProgram.textureBindings.swap(textureBindings);
|
||||
layerProgram.textBindings.swap(textBindings);
|
||||
layerProgram.passes.clear();
|
||||
|
||||
for (const auto& passSource : passSources)
|
||||
{
|
||||
GLint compileResult = GL_FALSE;
|
||||
GLint linkResult = GL_FALSE;
|
||||
const char* fragmentSource = passSource.fragmentShaderSource.c_str();
|
||||
|
||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
||||
glCompileShader(newVertexShader.get());
|
||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
||||
glCompileShader(newFragmentShader.get());
|
||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlProgram newProgram(glCreateProgram());
|
||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
||||
glLinkProgram(newProgram.get());
|
||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<LayerProgram::TextureBinding> textureBindings;
|
||||
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
|
||||
{
|
||||
LayerProgram::TextureBinding textureBinding;
|
||||
textureBinding.samplerName = textureAsset.id;
|
||||
textureBinding.sourcePath = textureAsset.path;
|
||||
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
|
||||
{
|
||||
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
|
||||
{
|
||||
if (loadedTexture.texture != 0)
|
||||
glDeleteTextures(1, &loadedTexture.texture);
|
||||
}
|
||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
textureBindings.push_back(textureBinding);
|
||||
}
|
||||
|
||||
std::vector<LayerProgram::TextBinding> textBindings;
|
||||
mTextureBindings.CreateTextBindings(state, textBindings);
|
||||
|
||||
PassProgram passProgram;
|
||||
passProgram.passId = passSource.passId;
|
||||
passProgram.inputNames = passSource.inputNames;
|
||||
passProgram.outputName = passSource.outputName;
|
||||
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
|
||||
passProgram.textureBindings.swap(textureBindings);
|
||||
passProgram.textBindings.swap(textBindings);
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
|
||||
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||
glUseProgram(newProgram.get());
|
||||
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
|
||||
glUseProgram(0);
|
||||
|
||||
passProgram.program = newProgram.release();
|
||||
passProgram.vertexShader = newVertexShader.release();
|
||||
passProgram.fragmentShader = newFragmentShader.release();
|
||||
layerProgram.passes.push_back(std::move(passProgram));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
#include "ShaderTextureBindings.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderProgramCompiler
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
|
||||
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
|
||||
|
||||
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
||||
|
||||
|
||||
@@ -102,3 +102,122 @@ GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const st
|
||||
return location;
|
||||
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
||||
}
|
||||
|
||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
{
|
||||
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
||||
{
|
||||
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
||||
|
||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
||||
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
|
||||
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
|
||||
if (sourceSamplerLocation >= 0)
|
||||
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
|
||||
|
||||
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
|
||||
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
|
||||
if (temporalSamplerLocation >= 0)
|
||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
||||
if (textureSamplerLocation >= 0)
|
||||
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
||||
{
|
||||
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
|
||||
if (textSamplerLocation >= 0)
|
||||
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
}
|
||||
|
||||
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const
|
||||
{
|
||||
RuntimeTextureBindingPlan plan;
|
||||
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
|
||||
|
||||
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"sourceHistory",
|
||||
"gSourceHistory" + std::to_string(index),
|
||||
sourceHistoryTextures[index],
|
||||
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
|
||||
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"temporalHistory",
|
||||
"gTemporalHistory" + std::to_string(index),
|
||||
temporalHistoryTextures[index],
|
||||
temporalBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
||||
plan.bindings.push_back({
|
||||
"shaderTexture",
|
||||
textureBinding.samplerName,
|
||||
textureBinding.texture,
|
||||
shaderTextureBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
||||
{
|
||||
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
|
||||
plan.bindings.push_back({
|
||||
"textTexture",
|
||||
textBinding.samplerName,
|
||||
textBinding.texture,
|
||||
textTextureBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
||||
{
|
||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, binding.texture);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
||||
{
|
||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,32 @@ class ShaderTextureBindings
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
|
||||
struct RuntimeTextureBinding
|
||||
{
|
||||
std::string semanticName;
|
||||
std::string samplerName;
|
||||
GLuint texture = 0;
|
||||
GLuint textureUnit = 0;
|
||||
};
|
||||
|
||||
struct RuntimeTextureBindingPlan
|
||||
{
|
||||
std::vector<RuntimeTextureBinding> bindings;
|
||||
};
|
||||
|
||||
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
||||
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
||||
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const;
|
||||
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||
void UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||
};
|
||||
|
||||
@@ -82,17 +82,6 @@ bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
for (const JsonValue& item : value.asArray())
|
||||
{
|
||||
if (item.isNumber())
|
||||
numbers.push_back(item.asNumber());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
|
||||
{
|
||||
return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
|
||||
@@ -629,6 +618,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
||||
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
|
||||
@@ -1300,7 +1292,7 @@ bool RuntimeHost::TryAdvanceFrame()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
|
||||
bool RuntimeHost::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1324,16 +1316,30 @@ bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std
|
||||
}
|
||||
|
||||
ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames);
|
||||
return compiler.BuildLayerFragmentShaderSource(shaderPackage, fragmentShaderSource, error);
|
||||
// Compile every declared pass while the caller remains backend-neutral.
|
||||
// The GL layer decides how the resulting pass sources are routed.
|
||||
passSources.clear();
|
||||
passSources.reserve(shaderPackage.passes.size());
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
ShaderPassBuildSource passSource;
|
||||
passSource.passId = pass.id;
|
||||
passSource.inputNames = pass.inputNames;
|
||||
passSource.outputName = pass.outputName;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error))
|
||||
return false;
|
||||
passSources.push_back(std::move(passSource));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what();
|
||||
error = std::string("RuntimeHost::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception.";
|
||||
error = "RuntimeHost::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1671,39 +1677,6 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
|
||||
{
|
||||
const std::string manifestText = ReadTextFile(manifestPath, error);
|
||||
if (manifestText.empty())
|
||||
return false;
|
||||
|
||||
JsonValue manifestJson;
|
||||
if (!ParseJson(manifestText, manifestJson, error))
|
||||
return false;
|
||||
if (!manifestJson.isObject())
|
||||
{
|
||||
error = "Shader manifest root must be an object: " + manifestPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(shaderPackage.shaderPath))
|
||||
{
|
||||
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
|
||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseTemporalSettings(manifestJson, shaderPackage, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
|
||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||
}
|
||||
|
||||
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
|
||||
{
|
||||
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
|
||||
@@ -1987,6 +1960,8 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
||||
JsonValue parameter = JsonValue::MakeObject();
|
||||
parameter.set("id", JsonValue(definition.id));
|
||||
parameter.set("label", JsonValue(definition.label));
|
||||
if (!definition.description.empty())
|
||||
parameter.set("description", JsonValue(definition.description));
|
||||
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
||||
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost
|
||||
@@ -50,7 +51,7 @@ public:
|
||||
void AdvanceFrame();
|
||||
bool TryAdvanceFrame();
|
||||
|
||||
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
|
||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
@@ -115,7 +116,6 @@ private:
|
||||
bool LoadPersistentState(std::string& error);
|
||||
bool SavePersistentState(std::string& error) const;
|
||||
bool ScanShaderPackages(std::string& error);
|
||||
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
|
||||
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
||||
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
|
||||
|
||||
@@ -661,3 +661,14 @@ std::string SerializeJson(const JsonValue& value, bool pretty)
|
||||
SerializeJsonImpl(value, output, pretty, 0);
|
||||
return output.str();
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
for (const JsonValue& item : value.asArray())
|
||||
{
|
||||
if (item.isNumber())
|
||||
numbers.push_back(item.asNumber());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
@@ -62,3 +62,4 @@ private:
|
||||
|
||||
bool ParseJson(const std::string& text, JsonValue& value, std::string& error);
|
||||
std::string SerializeJson(const JsonValue& value, bool pretty = false);
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value);
|
||||
|
||||
@@ -26,17 +26,6 @@ bool IsFiniteNumber(double value)
|
||||
return std::isfinite(value) != 0;
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
for (const JsonValue& item : value.asArray())
|
||||
{
|
||||
if (item.isNumber())
|
||||
numbers.push_back(item.asNumber());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
std::string NormalizeTextValue(const std::string& text, unsigned maxLength)
|
||||
{
|
||||
std::string normalized;
|
||||
|
||||
@@ -143,10 +143,10 @@ ShaderCompiler::ShaderCompiler(
|
||||
{
|
||||
}
|
||||
|
||||
bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const
|
||||
bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
|
||||
{
|
||||
std::string wrapperSource;
|
||||
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error))
|
||||
if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
|
||||
return false;
|
||||
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
|
||||
return false;
|
||||
@@ -167,7 +167,7 @@ bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderP
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const
|
||||
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
|
||||
{
|
||||
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
|
||||
wrapperSource = ReadTextFile(templatePath, error);
|
||||
@@ -183,8 +183,8 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ public:
|
||||
const std::filesystem::path& patchedGlslPath,
|
||||
unsigned maxTemporalHistoryFrames);
|
||||
|
||||
bool BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const;
|
||||
bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const;
|
||||
|
||||
private:
|
||||
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const;
|
||||
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const;
|
||||
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
|
||||
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
|
||||
bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const;
|
||||
|
||||
@@ -29,17 +29,6 @@ bool IsFiniteNumber(double value)
|
||||
return std::isfinite(value) != 0;
|
||||
}
|
||||
|
||||
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
|
||||
{
|
||||
std::vector<double> numbers;
|
||||
for (const JsonValue& item : value.asArray())
|
||||
{
|
||||
if (item.isNumber())
|
||||
numbers.push_back(item.asNumber());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
||||
{
|
||||
if (typeName == "float")
|
||||
@@ -250,6 +239,107 @@ bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPac
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* passesValue = nullptr;
|
||||
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!passesValue)
|
||||
{
|
||||
// Existing shader packages are treated as a single implicit pass, so
|
||||
// multipass support does not require manifest churn.
|
||||
ShaderPassDefinition pass;
|
||||
pass.id = "main";
|
||||
pass.entryPoint = shaderPackage.entryPoint;
|
||||
pass.sourcePath = shaderPackage.shaderPath;
|
||||
pass.outputName = "layerOutput";
|
||||
if (!std::filesystem::exists(pass.sourcePath))
|
||||
{
|
||||
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
|
||||
return false;
|
||||
}
|
||||
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
|
||||
shaderPackage.passes.push_back(pass);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (passesValue->asArray().empty())
|
||||
{
|
||||
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const JsonValue& passJson : passesValue->asArray())
|
||||
{
|
||||
if (!passJson.isObject())
|
||||
{
|
||||
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string passId;
|
||||
std::string sourcePath;
|
||||
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
|
||||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
|
||||
{
|
||||
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
|
||||
return false;
|
||||
|
||||
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
|
||||
{
|
||||
if (existingPass.id == passId)
|
||||
{
|
||||
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ShaderPassDefinition pass;
|
||||
pass.id = passId;
|
||||
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
|
||||
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
|
||||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
|
||||
return false;
|
||||
|
||||
const JsonValue* inputsValue = nullptr;
|
||||
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
|
||||
return false;
|
||||
if (inputsValue)
|
||||
{
|
||||
for (const JsonValue& inputValue : inputsValue->asArray())
|
||||
{
|
||||
if (!inputValue.isString())
|
||||
{
|
||||
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
pass.inputNames.push_back(inputValue.asString());
|
||||
}
|
||||
}
|
||||
|
||||
// Keep source validation in the registry. Bad pass declarations then
|
||||
// appear as unavailable shaders instead of failing at render time.
|
||||
if (!std::filesystem::exists(pass.sourcePath))
|
||||
{
|
||||
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
|
||||
return false;
|
||||
}
|
||||
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
|
||||
shaderPackage.passes.push_back(pass);
|
||||
}
|
||||
|
||||
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* texturesValue = nullptr;
|
||||
@@ -503,6 +593,9 @@ bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDef
|
||||
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
|
||||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
|
||||
@@ -666,13 +759,15 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
||||
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (!std::filesystem::exists(shaderPackage.shaderPath))
|
||||
{
|
||||
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
|
||||
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
|
||||
return false;
|
||||
}
|
||||
|
||||
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
|
||||
shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
|
||||
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
|
||||
}
|
||||
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
|
||||
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
|
||||
@@ -26,6 +26,7 @@ struct ShaderParameterDefinition
|
||||
{
|
||||
std::string id;
|
||||
std::string label;
|
||||
std::string description;
|
||||
ShaderParameterType type = ShaderParameterType::Float;
|
||||
std::vector<double> defaultNumbers;
|
||||
std::vector<double> minNumbers;
|
||||
@@ -76,6 +77,24 @@ struct ShaderFontAsset
|
||||
std::filesystem::file_time_type writeTime;
|
||||
};
|
||||
|
||||
struct ShaderPassDefinition
|
||||
{
|
||||
std::string id;
|
||||
std::string entryPoint;
|
||||
std::filesystem::path sourcePath;
|
||||
std::filesystem::file_time_type sourceWriteTime;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
};
|
||||
|
||||
struct ShaderPassBuildSource
|
||||
{
|
||||
std::string passId;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
};
|
||||
|
||||
struct ShaderPackage
|
||||
{
|
||||
std::string id;
|
||||
@@ -86,6 +105,7 @@ struct ShaderPackage
|
||||
std::filesystem::path directoryPath;
|
||||
std::filesystem::path shaderPath;
|
||||
std::filesystem::path manifestPath;
|
||||
std::vector<ShaderPassDefinition> passes;
|
||||
std::vector<ShaderParameterDefinition> parameters;
|
||||
std::vector<ShaderTextureAsset> textureAssets;
|
||||
std::vector<ShaderFontAsset> fontAssets;
|
||||
|
||||
@@ -54,6 +54,8 @@ const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
|
||||
{
|
||||
case VideoIOPixelFormat::V210:
|
||||
return "10-bit YUV v210";
|
||||
case VideoIOPixelFormat::Yuva10:
|
||||
return "10-bit YUVA Ay10";
|
||||
case VideoIOPixelFormat::Bgra8:
|
||||
return "8-bit BGRA";
|
||||
case VideoIOPixelFormat::Uyvy8:
|
||||
@@ -64,7 +66,7 @@ const char* VideoIOPixelFormatName(VideoIOPixelFormat format)
|
||||
|
||||
bool VideoIOPixelFormatIsTenBit(VideoIOPixelFormat format)
|
||||
{
|
||||
return format == VideoIOPixelFormat::V210;
|
||||
return format == VideoIOPixelFormat::V210 || format == VideoIOPixelFormat::Yuva10;
|
||||
}
|
||||
|
||||
VideoIOPixelFormat ChoosePreferredVideoIOFormat(bool tenBitSupported)
|
||||
@@ -80,6 +82,8 @@ unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format)
|
||||
return 2u;
|
||||
case VideoIOPixelFormat::Bgra8:
|
||||
return 4u;
|
||||
case VideoIOPixelFormat::Yuva10:
|
||||
return 4u;
|
||||
case VideoIOPixelFormat::V210:
|
||||
default:
|
||||
return 0u;
|
||||
@@ -90,6 +94,8 @@ unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth)
|
||||
{
|
||||
if (format == VideoIOPixelFormat::V210)
|
||||
return MinimumV210RowBytes(frameWidth);
|
||||
if (format == VideoIOPixelFormat::Yuva10)
|
||||
return MinimumYuva10RowBytes(frameWidth);
|
||||
return frameWidth * VideoIOBytesPerPixel(format);
|
||||
}
|
||||
|
||||
@@ -103,6 +109,11 @@ unsigned MinimumV210RowBytes(unsigned frameWidth)
|
||||
return ((frameWidth + 5u) / 6u) * 16u;
|
||||
}
|
||||
|
||||
unsigned MinimumYuva10RowBytes(unsigned frameWidth)
|
||||
{
|
||||
return ((frameWidth + 63u) / 64u) * 256u;
|
||||
}
|
||||
|
||||
unsigned ActiveV210WordsForWidth(unsigned frameWidth)
|
||||
{
|
||||
return ((frameWidth + 5u) / 6u) * 4u;
|
||||
|
||||
@@ -7,6 +7,7 @@ enum class VideoIOPixelFormat
|
||||
{
|
||||
Uyvy8,
|
||||
V210,
|
||||
Yuva10,
|
||||
Bgra8
|
||||
};
|
||||
|
||||
@@ -31,6 +32,7 @@ unsigned VideoIOBytesPerPixel(VideoIOPixelFormat format);
|
||||
unsigned VideoIORowBytes(VideoIOPixelFormat format, unsigned frameWidth);
|
||||
unsigned PackedTextureWidthFromRowBytes(unsigned rowBytes);
|
||||
unsigned MinimumV210RowBytes(unsigned frameWidth);
|
||||
unsigned MinimumYuva10RowBytes(unsigned frameWidth);
|
||||
unsigned ActiveV210WordsForWidth(unsigned frameWidth);
|
||||
V210CodeValues Rec709RgbToLegalV210(float red, float green, float blue);
|
||||
std::array<uint8_t, 16> PackV210Block(const V210SixPixelBlock& block);
|
||||
|
||||
@@ -95,7 +95,7 @@ public:
|
||||
virtual ~VideoIODevice() = default;
|
||||
virtual void ReleaseResources() = 0;
|
||||
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
||||
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
||||
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
||||
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
||||
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
||||
virtual bool Start() = 0;
|
||||
|
||||
@@ -223,7 +223,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error)
|
||||
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
||||
{
|
||||
if (!output)
|
||||
{
|
||||
@@ -239,8 +239,15 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoMo
|
||||
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
||||
|
||||
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
|
||||
mState.outputPixelFormat = outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8;
|
||||
if (!outputTenBitSupported)
|
||||
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
|
||||
mState.outputPixelFormat = outputAlphaRequired
|
||||
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
|
||||
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
|
||||
if (outputAlphaRequired && outputTenBitYuvaSupported)
|
||||
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
|
||||
else if (outputAlphaRequired)
|
||||
mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
|
||||
else if (!outputTenBitSupported)
|
||||
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
|
||||
|
||||
int deckLinkOutputRowBytes = 0;
|
||||
|
||||
@@ -22,7 +22,7 @@ public:
|
||||
|
||||
void ReleaseResources() override;
|
||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
|
||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, std::string& error) override;
|
||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
|
||||
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
||||
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
||||
bool Start() override;
|
||||
|
||||
@@ -6,6 +6,8 @@ BMDPixelFormat DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat format)
|
||||
{
|
||||
case VideoIOPixelFormat::V210:
|
||||
return bmdFormat10BitYUV;
|
||||
case VideoIOPixelFormat::Yuva10:
|
||||
return bmdFormat10BitYUVA;
|
||||
case VideoIOPixelFormat::Bgra8:
|
||||
return bmdFormat8BitBGRA;
|
||||
case VideoIOPixelFormat::Uyvy8:
|
||||
@@ -18,6 +20,8 @@ VideoIOPixelFormat VideoIOPixelFormatFromDeckLink(BMDPixelFormat format)
|
||||
{
|
||||
if (format == bmdFormat10BitYUV)
|
||||
return VideoIOPixelFormat::V210;
|
||||
if (format == bmdFormat10BitYUVA)
|
||||
return VideoIOPixelFormat::Yuva10;
|
||||
if (format == bmdFormat8BitBGRA)
|
||||
return VideoIOPixelFormat::Bgra8;
|
||||
return VideoIOPixelFormat::Uyvy8;
|
||||
|
||||
@@ -47,6 +47,8 @@ Matching is exact first. If that fails, names are compared in a simplified form
|
||||
|
||||
If multiple layers use the same shader package ID or display name, the first matching layer in the stack is controlled. Use the internal layer ID shown in the UI when you need to target one duplicate layer precisely.
|
||||
|
||||
In the control UI, each parameter row has a small **OSC** button. Clicking it copies that parameter's exact OSC address to the clipboard, which is the safest way to target controls with long names or duplicate shader layers.
|
||||
|
||||
## Values
|
||||
|
||||
The listener accepts these OSC argument types:
|
||||
@@ -65,7 +67,7 @@ Examples:
|
||||
/VideoShaderToys/fisheye-reproject/panDegrees 45.0
|
||||
/VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid"
|
||||
/VideoShaderToys/video-transform/pan 0.25 -0.5
|
||||
/VideoShaderToys/composition-guides/lineColor 1.0 0.8 0.1 1.0
|
||||
/VideoShaderToys/safe-area-guides/lineColor 1.0 0.8 0.1 1.0
|
||||
```
|
||||
|
||||
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
|
||||
|
||||
@@ -201,6 +201,7 @@ paths:
|
||||
post:
|
||||
tags: [Runtime]
|
||||
summary: Reload shaders
|
||||
description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
|
||||
operationId: reloadShaders
|
||||
requestBody:
|
||||
required: false
|
||||
@@ -465,6 +466,18 @@ components:
|
||||
type: number
|
||||
budgetUsedPercent:
|
||||
type: number
|
||||
completionIntervalMs:
|
||||
type: number
|
||||
smoothedCompletionIntervalMs:
|
||||
type: number
|
||||
maxCompletionIntervalMs:
|
||||
type: number
|
||||
lateFrameCount:
|
||||
type: number
|
||||
droppedFrameCount:
|
||||
type: number
|
||||
flushedFrameCount:
|
||||
type: number
|
||||
ShaderSummary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -476,6 +489,12 @@ components:
|
||||
type: string
|
||||
category:
|
||||
type: string
|
||||
available:
|
||||
type: boolean
|
||||
description: False when the shader package exists but failed manifest or compile validation.
|
||||
error:
|
||||
type: string
|
||||
description: Error text for unavailable shader packages.
|
||||
temporal:
|
||||
$ref: "#/components/schemas/TemporalState"
|
||||
TemporalState:
|
||||
@@ -514,9 +533,21 @@ components:
|
||||
type: string
|
||||
label:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
description: Short helper text shown under the parameter label in the control UI.
|
||||
type:
|
||||
type: string
|
||||
enum: [float, vec2, color, bool, enum, text, trigger]
|
||||
defaultValue:
|
||||
description: Default parameter value from the shader manifest.
|
||||
oneOf:
|
||||
- type: number
|
||||
- type: boolean
|
||||
- type: string
|
||||
- type: array
|
||||
items:
|
||||
type: number
|
||||
min:
|
||||
type: array
|
||||
items:
|
||||
@@ -533,6 +564,12 @@ components:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/ParameterOption"
|
||||
maxLength:
|
||||
type: number
|
||||
description: Maximum length for text parameters.
|
||||
font:
|
||||
type: string
|
||||
description: Font asset id used by text parameters, when declared.
|
||||
value:
|
||||
description: Current parameter value.
|
||||
oneOf:
|
||||
|
||||
@@ -14,9 +14,9 @@ Packaged documentation:
|
||||
|
||||
Generated files:
|
||||
|
||||
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer.
|
||||
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`.
|
||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
|
||||
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the most recently compiled shader pass.
|
||||
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc` for the most recently compiled pass.
|
||||
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path for the most recently compiled pass.
|
||||
- `runtime_state.json`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
|
||||
- `stack_presets/*.json`: user-saved layer stack presets.
|
||||
- `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.
|
||||
|
||||
@@ -11,11 +11,24 @@
|
||||
"type": "enum",
|
||||
"default": "x1_33",
|
||||
"options": [
|
||||
{ "value": "x1_3", "label": "1.3x" },
|
||||
{ "value": "x1_33", "label": "1.33x" },
|
||||
{ "value": "x1_5", "label": "1.5x" },
|
||||
{ "value": "x2_0", "label": "2x" }
|
||||
]
|
||||
{
|
||||
"value": "x1_3",
|
||||
"label": "1.3x"
|
||||
},
|
||||
{
|
||||
"value": "x1_33",
|
||||
"label": "1.33x"
|
||||
},
|
||||
{
|
||||
"value": "x1_5",
|
||||
"label": "1.5x"
|
||||
},
|
||||
{
|
||||
"value": "x2_0",
|
||||
"label": "2x"
|
||||
}
|
||||
],
|
||||
"description": "Horizontal stretch factor matching the anamorphic lens or adapter."
|
||||
},
|
||||
{
|
||||
"id": "framing",
|
||||
@@ -23,24 +36,50 @@
|
||||
"type": "enum",
|
||||
"default": "fit",
|
||||
"options": [
|
||||
{ "value": "fit", "label": "Fit" },
|
||||
{ "value": "fill", "label": "Fill" }
|
||||
]
|
||||
{
|
||||
"value": "fit",
|
||||
"label": "Fit"
|
||||
},
|
||||
{
|
||||
"value": "fill",
|
||||
"label": "Fill"
|
||||
}
|
||||
],
|
||||
"description": "Fit preserves the whole image; Fill crops to remove borders."
|
||||
},
|
||||
{
|
||||
"id": "pan",
|
||||
"label": "Pan",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-1.0, -1.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Reframes the desqueezed image after fit/fill scaling."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,37 +9,41 @@
|
||||
"id": "spinRotation",
|
||||
"label": "Spin Rotation",
|
||||
"type": "float",
|
||||
"default": -2.0,
|
||||
"min": -8.0,
|
||||
"max": 8.0,
|
||||
"step": 0.05
|
||||
"default": -2,
|
||||
"min": -8,
|
||||
"max": 8,
|
||||
"step": 0.05,
|
||||
"description": "Base rotation applied to the swirl field."
|
||||
},
|
||||
{
|
||||
"id": "spinSpeed",
|
||||
"label": "Spin Speed",
|
||||
"type": "float",
|
||||
"default": 7.0,
|
||||
"min": 0.0,
|
||||
"max": 20.0,
|
||||
"step": 0.1
|
||||
"default": 7,
|
||||
"min": 0,
|
||||
"max": 20,
|
||||
"step": 0.1,
|
||||
"description": "How quickly the swirl pattern rotates."
|
||||
},
|
||||
{
|
||||
"id": "spinAmount",
|
||||
"label": "Spin Amount",
|
||||
"type": "float",
|
||||
"default": 0.25,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Amount of radial twisting in the swirl."
|
||||
},
|
||||
{
|
||||
"id": "spinEase",
|
||||
"label": "Spin Ease",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Changes how strongly the twist falls off from the center."
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
@@ -47,59 +51,94 @@
|
||||
"type": "float",
|
||||
"default": 3.5,
|
||||
"min": 0.5,
|
||||
"max": 8.0,
|
||||
"step": 0.05
|
||||
"max": 8,
|
||||
"step": 0.05,
|
||||
"description": "Adjusts separation between dark and bright areas."
|
||||
},
|
||||
{
|
||||
"id": "lighting",
|
||||
"label": "Lighting",
|
||||
"type": "float",
|
||||
"default": 0.4,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Strength of the highlight/shadow modulation."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-1.0, -1.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the generated field in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "colour1",
|
||||
"label": "Colour 1",
|
||||
"type": "color",
|
||||
"default": [0.871, 0.267, 0.231, 1.0]
|
||||
"default": [
|
||||
0.871,
|
||||
0.267,
|
||||
0.231,
|
||||
1
|
||||
],
|
||||
"description": "Primary warm swirl color."
|
||||
},
|
||||
{
|
||||
"id": "colour2",
|
||||
"label": "Colour 2",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.42, 0.706, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0.42,
|
||||
0.706,
|
||||
1
|
||||
],
|
||||
"description": "Secondary cool swirl color."
|
||||
},
|
||||
{
|
||||
"id": "colour3",
|
||||
"label": "Colour 3",
|
||||
"type": "color",
|
||||
"default": [0.086, 0.137, 0.145, 1.0]
|
||||
"default": [
|
||||
0.086,
|
||||
0.137,
|
||||
0.145,
|
||||
1
|
||||
],
|
||||
"description": "Dark base color in the swirl."
|
||||
},
|
||||
{
|
||||
"id": "isRotate",
|
||||
"label": "Rotate Field",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Rotates the whole generated field over time."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"id": "badToggle",
|
||||
"label": "Bad Toggle",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Intentionally unsupported parameter type used to test shader error handling."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,55 +9,67 @@
|
||||
"id": "showThirds",
|
||||
"label": "Rule of Thirds",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Shows vertical and horizontal thirds lines."
|
||||
},
|
||||
{
|
||||
"id": "showCrosshair",
|
||||
"label": "Center Crosshair",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Shows a center crosshair for lens/framing alignment."
|
||||
},
|
||||
{
|
||||
"id": "lineColor",
|
||||
"label": "Line Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Color used for guide lines and marks."
|
||||
},
|
||||
{
|
||||
"id": "lineOpacity",
|
||||
"label": "Line Opacity",
|
||||
"type": "float",
|
||||
"default": 0.65,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the guide lines."
|
||||
},
|
||||
{
|
||||
"id": "lineThicknessPixels",
|
||||
"label": "Line Thickness",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"default": 2,
|
||||
"min": 0.5,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Guide line width in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "crosshairSizePixels",
|
||||
"label": "Crosshair Size",
|
||||
"type": "float",
|
||||
"default": 54.0,
|
||||
"min": 8.0,
|
||||
"max": 240.0,
|
||||
"step": 1.0
|
||||
"default": 54,
|
||||
"min": 8,
|
||||
"max": 240,
|
||||
"step": 1,
|
||||
"description": "Length of each crosshair arm in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "crosshairGapPixels",
|
||||
"label": "Crosshair Gap",
|
||||
"type": "float",
|
||||
"default": 10.0,
|
||||
"min": 0.0,
|
||||
"max": 80.0,
|
||||
"step": 1.0
|
||||
"default": 10,
|
||||
"min": 0,
|
||||
"max": 80,
|
||||
"step": 1,
|
||||
"description": "Empty gap around the exact frame center."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,36 +15,52 @@
|
||||
"label": "Amount",
|
||||
"type": "float",
|
||||
"default": 0.45,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the blocky temporal smear."
|
||||
},
|
||||
{
|
||||
"id": "blockCount",
|
||||
"label": "Block Count",
|
||||
"type": "vec2",
|
||||
"default": [32.0, 18.0],
|
||||
"min": [2.0, 2.0],
|
||||
"max": [160.0, 120.0],
|
||||
"step": [1.0, 1.0]
|
||||
"default": [
|
||||
32,
|
||||
18
|
||||
],
|
||||
"min": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"max": [
|
||||
160,
|
||||
120
|
||||
],
|
||||
"step": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Number of glitch blocks across X and Y."
|
||||
},
|
||||
{
|
||||
"id": "tearAmount",
|
||||
"label": "Tear",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Horizontal scanline tearing intensity."
|
||||
},
|
||||
{
|
||||
"id": "chromaShift",
|
||||
"label": "Chroma Shift",
|
||||
"type": "float",
|
||||
"default": 1.8,
|
||||
"min": 0.0,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"min": 0,
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Separate color-channel offset in pixels."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"default": 0.28,
|
||||
"min": 0.12,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Logo size relative to the frame."
|
||||
},
|
||||
{
|
||||
"id": "bounceSpeed",
|
||||
@@ -27,34 +28,38 @@
|
||||
"default": 0.22,
|
||||
"min": 0.02,
|
||||
"max": 0.8,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "How fast the logo moves between edge hits."
|
||||
},
|
||||
{
|
||||
"id": "edgePadding",
|
||||
"label": "Edge Padding",
|
||||
"type": "float",
|
||||
"default": 0.018,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.08,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Inset distance from the frame edges."
|
||||
},
|
||||
{
|
||||
"id": "glowAmount",
|
||||
"label": "Glow",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.75,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Adds a soft colored glow around the logo."
|
||||
},
|
||||
{
|
||||
"id": "baseAlpha",
|
||||
"label": "Alpha",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.05,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall opacity of the overlay."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "depth",
|
||||
@@ -20,65 +21,95 @@
|
||||
"type": "float",
|
||||
"default": 2.5,
|
||||
"min": 0.2,
|
||||
"max": 8.0,
|
||||
"step": 0.01
|
||||
"max": 8,
|
||||
"step": 0.01,
|
||||
"description": "Raymarch depth through the ether volume."
|
||||
},
|
||||
{
|
||||
"id": "density",
|
||||
"label": "Density",
|
||||
"type": "float",
|
||||
"default": 0.7,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Density of the volumetric strands."
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts the generated effect brightness."
|
||||
},
|
||||
{
|
||||
"id": "contrast",
|
||||
"label": "Contrast",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts separation between dark and bright areas."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "vec2",
|
||||
"default": [0.9, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.9,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the generated field in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "baseColor",
|
||||
"label": "Base Color",
|
||||
"type": "color",
|
||||
"default": [0.1, 0.3, 0.4, 1.0]
|
||||
"default": [
|
||||
0.1,
|
||||
0.3,
|
||||
0.4,
|
||||
1
|
||||
],
|
||||
"description": "Low-energy color used in the generated field."
|
||||
},
|
||||
{
|
||||
"id": "energyColor",
|
||||
"label": "Energy Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 0.5, 0.6, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
0.5,
|
||||
0.6,
|
||||
1
|
||||
],
|
||||
"description": "High-energy color used in the generated field."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,34 +9,38 @@
|
||||
"id": "blendAmount",
|
||||
"label": "Blend",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mix amount for the processed result."
|
||||
},
|
||||
{
|
||||
"id": "showLuma",
|
||||
"label": "Show Luma",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Shows grayscale luminance before applying false-color mapping."
|
||||
},
|
||||
{
|
||||
"id": "lift",
|
||||
"label": "Lift",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"default": 0,
|
||||
"min": -0.25,
|
||||
"max": 0.25,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Offsets luminance before false-color mapping."
|
||||
},
|
||||
{
|
||||
"id": "gain",
|
||||
"label": "Gain",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Scales luminance before false-color mapping."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,55 +9,85 @@
|
||||
"id": "lensFovDegrees",
|
||||
"label": "Lens FOV",
|
||||
"type": "float",
|
||||
"default": 190.0,
|
||||
"min": 1.0,
|
||||
"max": 220.0,
|
||||
"step": 0.1
|
||||
"default": 190,
|
||||
"min": 1,
|
||||
"max": 220,
|
||||
"step": 0.1,
|
||||
"description": "Actual fisheye lens field of view in degrees."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Optical Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
|
||||
},
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Fisheye Radius",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.8889],
|
||||
"min": [0.001, 0.001],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.8889
|
||||
],
|
||||
"min": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
||||
},
|
||||
{
|
||||
"id": "yawDegrees",
|
||||
"label": "Yaw",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the virtual view horizontally."
|
||||
},
|
||||
{
|
||||
"id": "pitchDegrees",
|
||||
"label": "Pitch",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the virtual view vertically."
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live roll rotation around the viewing axis."
|
||||
},
|
||||
{
|
||||
"id": "fisheyeModel",
|
||||
@@ -65,35 +95,56 @@
|
||||
"type": "enum",
|
||||
"default": "equidistant",
|
||||
"options": [
|
||||
{ "value": "equidistant", "label": "Equidistant" },
|
||||
{ "value": "equisolid", "label": "Equisolid" },
|
||||
{ "value": "stereographic", "label": "Stereographic" },
|
||||
{ "value": "orthographic", "label": "Orthographic" }
|
||||
]
|
||||
{
|
||||
"value": "equidistant",
|
||||
"label": "Equidistant"
|
||||
},
|
||||
{
|
||||
"value": "equisolid",
|
||||
"label": "Equisolid"
|
||||
},
|
||||
{
|
||||
"value": "stereographic",
|
||||
"label": "Stereographic"
|
||||
},
|
||||
{
|
||||
"value": "orthographic",
|
||||
"label": "Orthographic"
|
||||
}
|
||||
],
|
||||
"description": "Projection model used by the physical fisheye lens."
|
||||
},
|
||||
{
|
||||
"id": "edgeFill",
|
||||
"label": "Edge Fill",
|
||||
"type": "float",
|
||||
"default": 0.06,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.3,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Extends edge samples outward to cover small missing areas."
|
||||
},
|
||||
{
|
||||
"id": "edgeBlur",
|
||||
"label": "Edge Blur",
|
||||
"type": "float",
|
||||
"default": 0.018,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.12,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Softens the dilated edge fill."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,91 +9,125 @@
|
||||
"id": "lensFovDegrees",
|
||||
"label": "Lens FOV",
|
||||
"type": "float",
|
||||
"default": 190.0,
|
||||
"min": 1.0,
|
||||
"max": 220.0,
|
||||
"step": 0.1
|
||||
"default": 190,
|
||||
"min": 1,
|
||||
"max": 220,
|
||||
"step": 0.1,
|
||||
"description": "Actual fisheye lens field of view in degrees."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Optical Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
|
||||
},
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Fisheye Radius",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.885],
|
||||
"min": [0.001, 0.001],
|
||||
"max": [2.0, 2.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.5,
|
||||
0.885
|
||||
],
|
||||
"min": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"max": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
||||
},
|
||||
{
|
||||
"id": "virtualFovDegrees",
|
||||
"label": "Virtual FOV",
|
||||
"type": "float",
|
||||
"default": 75.0,
|
||||
"min": 1.0,
|
||||
"max": 175.0,
|
||||
"step": 0.1
|
||||
"default": 75,
|
||||
"min": 1,
|
||||
"max": 175,
|
||||
"step": 0.1,
|
||||
"description": "Field of view of the generated virtual camera."
|
||||
},
|
||||
{
|
||||
"id": "basePanDegrees",
|
||||
"label": "Base Pan",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Permanent horizontal alignment offset before live pan."
|
||||
},
|
||||
{
|
||||
"id": "baseTiltDegrees",
|
||||
"label": "Base Tilt",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Permanent vertical alignment offset before live tilt."
|
||||
},
|
||||
{
|
||||
"id": "baseRollDegrees",
|
||||
"label": "Base Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Permanent roll alignment offset before live roll."
|
||||
},
|
||||
{
|
||||
"id": "panDegrees",
|
||||
"label": "Pan",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live horizontal view rotation."
|
||||
},
|
||||
{
|
||||
"id": "tiltDegrees",
|
||||
"label": "Tilt",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -120.0,
|
||||
"max": 120.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -120,
|
||||
"max": 120,
|
||||
"step": 0.1,
|
||||
"description": "Live vertical view rotation."
|
||||
},
|
||||
{
|
||||
"id": "rollDegrees",
|
||||
"label": "Roll",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Live roll rotation around the viewing axis."
|
||||
},
|
||||
{
|
||||
"id": "fisheyeModel",
|
||||
@@ -101,11 +135,24 @@
|
||||
"type": "enum",
|
||||
"default": "equidistant",
|
||||
"options": [
|
||||
{ "value": "equidistant", "label": "Equidistant" },
|
||||
{ "value": "equisolid", "label": "Equisolid" },
|
||||
{ "value": "stereographic", "label": "Stereographic" },
|
||||
{ "value": "orthographic", "label": "Orthographic" }
|
||||
]
|
||||
{
|
||||
"value": "equidistant",
|
||||
"label": "Equidistant"
|
||||
},
|
||||
{
|
||||
"value": "equisolid",
|
||||
"label": "Equisolid"
|
||||
},
|
||||
{
|
||||
"value": "stereographic",
|
||||
"label": "Stereographic"
|
||||
},
|
||||
{
|
||||
"value": "orthographic",
|
||||
"label": "Orthographic"
|
||||
}
|
||||
],
|
||||
"description": "Projection model used by the physical fisheye lens."
|
||||
},
|
||||
{
|
||||
"id": "outputProjection",
|
||||
@@ -113,15 +160,28 @@
|
||||
"type": "enum",
|
||||
"default": "rectilinear",
|
||||
"options": [
|
||||
{ "value": "rectilinear", "label": "Rectilinear" },
|
||||
{ "value": "cylindrical", "label": "Cylindrical" }
|
||||
]
|
||||
{
|
||||
"value": "rectilinear",
|
||||
"label": "Rectilinear"
|
||||
},
|
||||
{
|
||||
"value": "cylindrical",
|
||||
"label": "Cylindrical"
|
||||
}
|
||||
],
|
||||
"description": "Chooses rectilinear perspective or cylindrical reprojection."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,36 +1,59 @@
|
||||
{
|
||||
"id": "gaussian-blur",
|
||||
"name": "Gaussian Blur",
|
||||
"description": "Applies a simple Gaussian-style blur to the decoded video input.",
|
||||
"description": "Applies a separable two-pass Gaussian-style blur to the decoded video input.",
|
||||
"category": "Transform",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "horizontal",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "blurHorizontal",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "blurHorizontal"
|
||||
},
|
||||
{
|
||||
"id": "vertical",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "blurVertical",
|
||||
"inputs": [
|
||||
"blurHorizontal"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "radius",
|
||||
"label": "Radius",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 8.0,
|
||||
"step": 0.1
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 0.1,
|
||||
"description": "Blur radius in pixels for each separable pass."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends between the original and blurred result."
|
||||
},
|
||||
{
|
||||
"id": "samples",
|
||||
"label": "Samples",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 0.0,
|
||||
"max": 25.0,
|
||||
"step": 1.0
|
||||
"default": 2,
|
||||
"min": 0,
|
||||
"max": 25,
|
||||
"step": 1,
|
||||
"description": "Number of taps per direction; higher values cost more GPU time."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float4 gaussianBlurDirection(ShaderContext context, float2 direction)
|
||||
{
|
||||
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
|
||||
float blurRadius = max(radius, 0.0);
|
||||
float2 sampleStep = texel * blurRadius;
|
||||
float blurRadius = max(radius, 0.0) * saturate(strength);
|
||||
float2 sampleStep = texel * blurRadius * direction;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
float distanceSquared = float(x * x);
|
||||
float sigma = max(float(sampleRadius) * 0.5, 0.5);
|
||||
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
|
||||
float2 offset = float(x) * sampleStep;
|
||||
blur += sampleVideo(context.uv + offset) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
if (sampleRadius == 0)
|
||||
@@ -29,7 +26,20 @@ float4 shadeVideo(ShaderContext context)
|
||||
}
|
||||
|
||||
blur /= max(totalWeight, 0.0001);
|
||||
|
||||
float mixValue = saturate(strength);
|
||||
return lerp(center, blur, mixValue);
|
||||
return blur;
|
||||
}
|
||||
|
||||
float4 blurHorizontal(ShaderContext context)
|
||||
{
|
||||
return gaussianBlurDirection(context, float2(1.0, 0.0));
|
||||
}
|
||||
|
||||
float4 blurVertical(ShaderContext context)
|
||||
{
|
||||
return gaussianBlurDirection(context, float2(0.0, 1.0));
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return blurVertical(context);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,65 @@
|
||||
"description": "Production-style green/blue screen keyer with matte refinement, despill, edge treatment, and debug views.",
|
||||
"category": "Keying",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "rawMatte",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "buildRawMatte",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "rawMatte"
|
||||
},
|
||||
{
|
||||
"id": "refinedMatte",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "refineMatte",
|
||||
"inputs": [
|
||||
"rawMatte"
|
||||
],
|
||||
"output": "refinedMatte"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "applyKey",
|
||||
"inputs": [
|
||||
"refinedMatte"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "screenColor",
|
||||
"label": "Screen Color",
|
||||
"type": "color",
|
||||
"default": [0.15, 0.85, 0.2, 1.0],
|
||||
"min": [0.0, 0.0, 0.0, 0.0],
|
||||
"max": [1.0, 1.0, 1.0, 1.0],
|
||||
"step": [0.01, 0.01, 0.01, 0.01]
|
||||
"default": [
|
||||
0.15,
|
||||
0.85,
|
||||
0.2,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Target screen color to remove; use green or blue depending on the backdrop."
|
||||
},
|
||||
{
|
||||
"id": "threshold",
|
||||
@@ -21,7 +71,8 @@
|
||||
"default": 0.24,
|
||||
"min": 0.01,
|
||||
"max": 0.8,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Higher values keep more foreground; lower values remove more screen."
|
||||
},
|
||||
{
|
||||
"id": "softness",
|
||||
@@ -30,142 +81,238 @@
|
||||
"default": 0.16,
|
||||
"min": 0.001,
|
||||
"max": 0.5,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Feathers the transition between foreground and keyed screen."
|
||||
},
|
||||
{
|
||||
"id": "screenBalance",
|
||||
"label": "Screen Balance",
|
||||
"type": "float",
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Balances chroma-distance keying against color-direction keying."
|
||||
},
|
||||
{
|
||||
"id": "screenPreBlur",
|
||||
"label": "Screen PreBlur",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 8.0,
|
||||
"step": 0.1
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 8,
|
||||
"step": 0.1,
|
||||
"description": "Blurs source color before matte generation to reduce noisy edges."
|
||||
},
|
||||
{
|
||||
"id": "erodeDilate",
|
||||
"label": "Erode/Dilate",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"default": 0,
|
||||
"min": -0.3,
|
||||
"max": 0.3,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Negative erodes the matte; positive expands it."
|
||||
},
|
||||
{
|
||||
"id": "matteBlur",
|
||||
"label": "Matte Blur",
|
||||
"type": "float",
|
||||
"default": 1.25,
|
||||
"min": 0.0,
|
||||
"max": 6.0,
|
||||
"step": 0.1
|
||||
"min": 0,
|
||||
"max": 6,
|
||||
"step": 0.1,
|
||||
"description": "Softens the generated matte after keying."
|
||||
},
|
||||
{
|
||||
"id": "matteGamma",
|
||||
"label": "Matte Gamma",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Shapes midtone opacity in the matte."
|
||||
},
|
||||
{
|
||||
"id": "matteContrast",
|
||||
"label": "Matte Contrast",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Increases or reduces matte separation around 50 percent alpha."
|
||||
},
|
||||
{
|
||||
"id": "blackCleanup",
|
||||
"label": "Black Cleanup",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Pushes semi-transparent dark matte areas toward transparent."
|
||||
},
|
||||
{
|
||||
"id": "whiteCleanup",
|
||||
"label": "White Cleanup",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Pushes semi-transparent light matte areas toward opaque."
|
||||
},
|
||||
{
|
||||
"id": "despill",
|
||||
"label": "Despill",
|
||||
"type": "float",
|
||||
"default": 0.45,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Removes screen-colored contamination from foreground edges."
|
||||
},
|
||||
{
|
||||
"id": "despillBias",
|
||||
"label": "Despill Bias",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"default": 0,
|
||||
"min": -0.5,
|
||||
"max": 0.5,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Offsets spill detection when foreground colors are close to the screen color."
|
||||
},
|
||||
{
|
||||
"id": "spillTint",
|
||||
"label": "Spill Tint",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0],
|
||||
"min": [0.0, 0.0, 0.0, 0.0],
|
||||
"max": [1.0, 1.0, 1.0, 1.0],
|
||||
"step": [0.01, 0.01, 0.01, 0.01]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Tint used when neutralizing spill."
|
||||
},
|
||||
{
|
||||
"id": "edgeRecover",
|
||||
"label": "Edge Recover",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Adds color recovery along semi-transparent matte edges."
|
||||
},
|
||||
{
|
||||
"id": "edgeColor",
|
||||
"label": "Edge Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0],
|
||||
"min": [0.0, 0.0, 0.0, 0.0],
|
||||
"max": [1.0, 1.0, 1.0, 1.0],
|
||||
"step": [0.01, 0.01, 0.01, 0.01]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01,
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Tint applied to recovered edge detail."
|
||||
},
|
||||
{
|
||||
"id": "clipBlack",
|
||||
"label": "Clip Black",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Matte values below this become transparent."
|
||||
},
|
||||
{
|
||||
"id": "clipWhite",
|
||||
"label": "Clip White",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.5,
|
||||
"max": 1.0,
|
||||
"step": 0.005
|
||||
"max": 1,
|
||||
"step": 0.005,
|
||||
"description": "Matte values above this become opaque."
|
||||
},
|
||||
{
|
||||
"id": "cropLeft",
|
||||
"label": "Crop Left",
|
||||
"description": "Trims the final matte from the left edge as a fraction of frame width.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropRight",
|
||||
"label": "Crop Right",
|
||||
"description": "Trims the final matte from the right edge as a fraction of frame width.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropTop",
|
||||
"label": "Crop Top",
|
||||
"description": "Trims the final matte from the top edge as a fraction of frame height.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "cropBottom",
|
||||
"label": "Crop Bottom",
|
||||
"description": "Trims the final matte from the bottom edge as a fraction of frame height.",
|
||||
"type": "float",
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.001
|
||||
},
|
||||
{
|
||||
"id": "viewMode",
|
||||
@@ -173,12 +320,28 @@
|
||||
"type": "enum",
|
||||
"default": "composite",
|
||||
"options": [
|
||||
{ "value": "composite", "label": "Composite" },
|
||||
{ "value": "matte", "label": "Matte" },
|
||||
{ "value": "spill", "label": "Spill" },
|
||||
{ "value": "despill", "label": "Despill" },
|
||||
{ "value": "status", "label": "Status" }
|
||||
]
|
||||
{
|
||||
"value": "composite",
|
||||
"label": "Composite"
|
||||
},
|
||||
{
|
||||
"value": "matte",
|
||||
"label": "Matte"
|
||||
},
|
||||
{
|
||||
"value": "spill",
|
||||
"label": "Spill"
|
||||
},
|
||||
{
|
||||
"value": "despill",
|
||||
"label": "Despill"
|
||||
},
|
||||
{
|
||||
"value": "status",
|
||||
"label": "Status"
|
||||
}
|
||||
],
|
||||
"description": "Debug output mode for inspecting matte, spill, and despill stages."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,12 +50,17 @@ float rawAlphaAt(float2 uv, ShaderContext context)
|
||||
return saturate(alpha);
|
||||
}
|
||||
|
||||
float refinedAlphaAt(float2 uv, ShaderContext context)
|
||||
float matteAlphaAt(float2 uv)
|
||||
{
|
||||
return saturate(sampleVideo(saturate(uv)).a);
|
||||
}
|
||||
|
||||
float refinedAlphaFromMatte(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float blur = max(matteBlur, 0.0);
|
||||
float aaRadius = max(blur, 0.65);
|
||||
float centerAlpha = rawAlphaAt(uv, context);
|
||||
float centerAlpha = matteAlphaAt(uv);
|
||||
float alpha = centerAlpha * 0.30;
|
||||
|
||||
if (aaRadius > 0.0001)
|
||||
@@ -64,51 +69,51 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
|
||||
float2 halfRadius = radius * 0.5;
|
||||
float alphaMin = centerAlpha;
|
||||
float alphaMax = centerAlpha;
|
||||
float sampleAlpha = rawAlphaAt(uv + float2(halfRadius.x, 0.0), context);
|
||||
float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv - float2(halfRadius.x, 0.0), context);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + float2(0.0, halfRadius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv - float2(0.0, halfRadius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
|
||||
alpha += sampleAlpha * 0.065;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + float2(radius.x, 0.0), context);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv - float2(radius.x, 0.0), context);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + float2(0.0, radius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv - float2(0.0, radius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
|
||||
alpha += sampleAlpha * 0.06;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + radius, context);
|
||||
sampleAlpha = matteAlphaAt(uv + radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv - radius, context);
|
||||
sampleAlpha = matteAlphaAt(uv - radius);
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + float2(radius.x, -radius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
sampleAlpha = rawAlphaAt(uv + float2(-radius.x, radius.y), context);
|
||||
sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
|
||||
alpha += sampleAlpha * 0.05;
|
||||
alphaMin = min(alphaMin, sampleAlpha);
|
||||
alphaMax = max(alphaMax, sampleAlpha);
|
||||
@@ -118,7 +123,7 @@ float refinedAlphaAt(float2 uv, ShaderContext context)
|
||||
}
|
||||
else
|
||||
{
|
||||
alpha = rawAlphaAt(uv, context);
|
||||
alpha = centerAlpha;
|
||||
}
|
||||
|
||||
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
|
||||
@@ -147,17 +152,43 @@ float3 despillColor(float3 color, float alpha)
|
||||
return saturate(neutralized);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float cropMaskAt(float2 uv, ShaderContext context)
|
||||
{
|
||||
float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
|
||||
float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x);
|
||||
float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float4 buildRawMatte(ShaderContext context)
|
||||
{
|
||||
float4 src = context.sourceColor;
|
||||
float3 color = saturate(src.rgb);
|
||||
float alpha = refinedAlphaAt(context.uv, context);
|
||||
float alpha = rawAlphaAt(context.uv, context);
|
||||
return float4(color, alpha);
|
||||
}
|
||||
|
||||
float4 refineMatte(ShaderContext context)
|
||||
{
|
||||
float4 raw = sampleVideo(context.uv);
|
||||
float alpha = refinedAlphaFromMatte(context.uv, context);
|
||||
return float4(saturate(raw.rgb), alpha);
|
||||
}
|
||||
|
||||
float4 applyKey(ShaderContext context)
|
||||
{
|
||||
float4 keyed = sampleVideo(context.uv);
|
||||
float3 color = saturate(keyed.rgb);
|
||||
float alpha = saturate(keyed.a);
|
||||
float spill = spillAmountForColor(color);
|
||||
float3 despilled = despillColor(color, alpha);
|
||||
float cropMask = cropMaskAt(context.uv, context);
|
||||
alpha *= cropMask;
|
||||
|
||||
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
|
||||
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
|
||||
alpha = saturate(lerp(alpha, rawAlphaAt(context.uv, context), edgeAmount * saturate(edgeRecover) * 0.35));
|
||||
|
||||
if (viewMode == 1)
|
||||
return float4(alpha, alpha, alpha, 1.0);
|
||||
@@ -167,10 +198,15 @@ float4 shadeVideo(ShaderContext context)
|
||||
return float4(despilled, 1.0);
|
||||
if (viewMode == 4)
|
||||
{
|
||||
float rawAlpha = rawAlphaAt(context.uv, context);
|
||||
float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
|
||||
return float4(rawAlpha, alpha, spill, 1.0);
|
||||
}
|
||||
|
||||
float3 premultiplied = saturate(despilled) * alpha;
|
||||
return float4(premultiplied, alpha);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return applyKey(context);
|
||||
}
|
||||
|
||||
@@ -9,46 +9,51 @@
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Overall size of the effect in the frame."
|
||||
},
|
||||
{
|
||||
"id": "raySteps",
|
||||
"label": "Ray Steps",
|
||||
"type": "float",
|
||||
"default": 77.0,
|
||||
"min": 8.0,
|
||||
"max": 77.0,
|
||||
"step": 1.0
|
||||
"default": 77,
|
||||
"min": 8,
|
||||
"max": 77,
|
||||
"step": 1,
|
||||
"description": "Raymarch iteration count; higher values increase detail and GPU cost."
|
||||
},
|
||||
{
|
||||
"id": "intensity",
|
||||
"label": "Intensity",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Overall brightness of the accumulated raymarched light."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,34 +9,59 @@
|
||||
"id": "lift",
|
||||
"label": "Lift",
|
||||
"type": "color",
|
||||
"default": [0.5, 0.5, 0.5, 1.0]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Adds color mostly to shadows."
|
||||
},
|
||||
{
|
||||
"id": "gamma",
|
||||
"label": "Gamma",
|
||||
"type": "color",
|
||||
"default": [0.5, 0.5, 0.5, 1.0]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Balances midtone color response."
|
||||
},
|
||||
{
|
||||
"id": "gain",
|
||||
"label": "Gain",
|
||||
"type": "color",
|
||||
"default": [0.5, 0.5, 0.5, 1.0]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Scales highlights and overall channel intensity."
|
||||
},
|
||||
{
|
||||
"id": "offset",
|
||||
"label": "Offset",
|
||||
"type": "color",
|
||||
"default": [0.5, 0.5, 0.5, 1.0]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5,
|
||||
0.5,
|
||||
1
|
||||
],
|
||||
"description": "Adds a uniform color offset after lift/gamma/gain."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the grade with the original image."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,43 +15,48 @@
|
||||
"id": "lutStrength",
|
||||
"label": "LUT Strength",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends between the original image and the LUT result."
|
||||
},
|
||||
{
|
||||
"id": "preExposure",
|
||||
"label": "Pre Exposure",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -4.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": -4,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Exposure offset applied before the LUT lookup."
|
||||
},
|
||||
{
|
||||
"id": "postContrast",
|
||||
"label": "Post Contrast",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Contrast adjustment applied after the LUT lookup."
|
||||
},
|
||||
{
|
||||
"id": "ditherAmount",
|
||||
"label": "Output Dither",
|
||||
"type": "float",
|
||||
"default": 0.5,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Adds subtle output dither to reduce visible banding."
|
||||
},
|
||||
{
|
||||
"id": "clampInput",
|
||||
"label": "Clamp Input",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Clamps colors to 0-1 before the LUT lookup."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
49
shaders/multipass-test/shader.json
Normal file
49
shaders/multipass-test/shader.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"id": "multipass-test",
|
||||
"name": "Multipass Test",
|
||||
"description": "Diagnostic two-pass shader that generates a mask in pass one, then samples that named intermediate in pass two.",
|
||||
"category": "Utility",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "mask",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "buildMask",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "generatedMask"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "applyMask",
|
||||
"inputs": [
|
||||
"generatedMask"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "intensity",
|
||||
"label": "Intensity",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the second-pass diagnostic overlay."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
"label": "Scale",
|
||||
"type": "float",
|
||||
"default": 10,
|
||||
"min": 2,
|
||||
"max": 32,
|
||||
"step": 1,
|
||||
"description": "Size of the generated test pattern."
|
||||
}
|
||||
]
|
||||
}
|
||||
37
shaders/multipass-test/shader.slang
Normal file
37
shaders/multipass-test/shader.slang
Normal file
@@ -0,0 +1,37 @@
|
||||
float ringMask(float2 uv)
|
||||
{
|
||||
float2 centered = uv * 2.0 - 1.0;
|
||||
float radius = length(centered);
|
||||
float ring = 1.0 - smoothstep(0.015, 0.035, abs(radius - 0.55));
|
||||
float cross = 1.0 - smoothstep(0.006, 0.018, min(abs(centered.x), abs(centered.y)));
|
||||
return saturate(max(ring, cross));
|
||||
}
|
||||
|
||||
float gridMask(float2 uv)
|
||||
{
|
||||
float2 cell = abs(frac(uv * max(scale, 1.0)) - 0.5);
|
||||
float line = 1.0 - smoothstep(0.455, 0.495, max(cell.x, cell.y));
|
||||
return saturate(line * 0.55);
|
||||
}
|
||||
|
||||
float4 buildMask(ShaderContext context)
|
||||
{
|
||||
float mask = saturate(max(ringMask(context.uv), gridMask(context.uv)));
|
||||
return float4(context.sourceColor.rgb, mask);
|
||||
}
|
||||
|
||||
float4 applyMask(ShaderContext context)
|
||||
{
|
||||
float4 generated = sampleVideo(context.uv);
|
||||
float mask = generated.a;
|
||||
float checker = step(0.5, frac((context.uv.x + context.uv.y) * max(scale, 1.0)));
|
||||
float3 testColor = lerp(float3(0.0, 0.75, 1.0), float3(1.0, 0.1, 0.85), checker);
|
||||
float3 base = generated.rgb;
|
||||
float3 color = lerp(base, testColor, mask * saturate(intensity));
|
||||
return float4(color, 1.0);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return applyMask(context);
|
||||
}
|
||||
@@ -9,25 +9,45 @@
|
||||
"id": "pixelCount",
|
||||
"label": "Pixel Count",
|
||||
"type": "vec2",
|
||||
"default": [96.0, 54.0],
|
||||
"min": [2.0, 2.0],
|
||||
"max": [1920.0, 1080.0],
|
||||
"step": [1.0, 1.0]
|
||||
"default": [
|
||||
96,
|
||||
54
|
||||
],
|
||||
"min": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"max": [
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"step": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Number of pixel blocks across X and Y."
|
||||
},
|
||||
{
|
||||
"id": "gridAmount",
|
||||
"label": "Grid",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Visibility of the block grid lines."
|
||||
},
|
||||
{
|
||||
"id": "gridColor",
|
||||
"label": "Grid Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used for the pixel grid."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,32 +5,58 @@
|
||||
"category": "Scopes & Guides",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{ "id": "showActionSafe", "label": "Action Safe", "type": "bool", "default": true },
|
||||
{ "id": "showTitleSafe", "label": "Title Safe", "type": "bool", "default": true },
|
||||
{ "id": "showCenter", "label": "Center Marks", "type": "bool", "default": true },
|
||||
{
|
||||
"id": "showActionSafe",
|
||||
"label": "Action Safe",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows the broadcast action-safe rectangle."
|
||||
},
|
||||
{
|
||||
"id": "showTitleSafe",
|
||||
"label": "Title Safe",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows the broadcast title-safe rectangle."
|
||||
},
|
||||
{
|
||||
"id": "showCenter",
|
||||
"label": "Center Marks",
|
||||
"type": "bool",
|
||||
"default": true,
|
||||
"description": "Shows center marks for alignment."
|
||||
},
|
||||
{
|
||||
"id": "lineColor",
|
||||
"label": "Line Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Color used for guide lines and marks."
|
||||
},
|
||||
{
|
||||
"id": "lineOpacity",
|
||||
"label": "Line Opacity",
|
||||
"type": "float",
|
||||
"default": 0.65,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the guide lines."
|
||||
},
|
||||
{
|
||||
"id": "lineThicknessPixels",
|
||||
"label": "Line Thickness",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"default": 2,
|
||||
"min": 0.5,
|
||||
"max": 12.0,
|
||||
"step": 0.1
|
||||
"max": 12,
|
||||
"step": 0.1,
|
||||
"description": "Guide line width in output pixels."
|
||||
},
|
||||
{
|
||||
"id": "aspectMode",
|
||||
@@ -38,20 +64,34 @@
|
||||
"type": "enum",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{ "value": "none", "label": "None" },
|
||||
{ "value": "239", "label": "2.39:1" },
|
||||
{ "value": "185", "label": "1.85:1" },
|
||||
{ "value": "square", "label": "1:1" }
|
||||
]
|
||||
{
|
||||
"value": "none",
|
||||
"label": "None"
|
||||
},
|
||||
{
|
||||
"value": "239",
|
||||
"label": "2.39:1"
|
||||
},
|
||||
{
|
||||
"value": "185",
|
||||
"label": "1.85:1"
|
||||
},
|
||||
{
|
||||
"value": "square",
|
||||
"label": "1:1"
|
||||
}
|
||||
],
|
||||
"description": "Adds an optional framing matte for common delivery ratios."
|
||||
},
|
||||
{
|
||||
"id": "matteOpacity",
|
||||
"label": "Matte Opacity",
|
||||
"type": "float",
|
||||
"default": 0.35,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the aspect-ratio matte outside the active image."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"id": "speed",
|
||||
"label": "Speed",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Animation speed multiplier; set to 0 to pause motion."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
@@ -21,16 +22,18 @@
|
||||
"default": 0.7,
|
||||
"min": 0.25,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Overall size of the effect in the frame."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Gravity",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Strength of the lensing/gravity distortion."
|
||||
},
|
||||
{
|
||||
"id": "ringRadius",
|
||||
@@ -39,7 +42,8 @@
|
||||
"default": 0.7,
|
||||
"min": 0.2,
|
||||
"max": 1.4,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Radius of the bright accretion ring."
|
||||
},
|
||||
{
|
||||
"id": "tightness",
|
||||
@@ -47,44 +51,61 @@
|
||||
"type": "float",
|
||||
"default": 1.35,
|
||||
"min": 0.5,
|
||||
"max": 3.0,
|
||||
"step": 0.01
|
||||
"max": 3,
|
||||
"step": 0.01,
|
||||
"description": "Concentration of the ring and spiral detail."
|
||||
},
|
||||
{
|
||||
"id": "brightness",
|
||||
"label": "Brightness",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Adjusts the generated effect brightness."
|
||||
},
|
||||
{
|
||||
"id": "colorShift",
|
||||
"label": "Color Shift",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": -2.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": -2,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Cycles the generated color palette."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Center",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-1.0, -1.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the black hole center in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
"id": "fillColor",
|
||||
"label": "Fill",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Frame fill color; alpha is preserved for key-capable outputs."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,33 +15,42 @@
|
||||
"label": "Echo Amount",
|
||||
"type": "float",
|
||||
"default": 0.55,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall visibility of the temporal echoes."
|
||||
},
|
||||
{
|
||||
"id": "decay",
|
||||
"label": "Decay",
|
||||
"type": "float",
|
||||
"default": 0.72,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "How quickly older temporal echoes fade away."
|
||||
},
|
||||
{
|
||||
"id": "frameStride",
|
||||
"label": "Frame Stride",
|
||||
"type": "float",
|
||||
"default": 2.0,
|
||||
"min": 1.0,
|
||||
"max": 6.0,
|
||||
"step": 1.0
|
||||
"default": 2,
|
||||
"min": 1,
|
||||
"max": 6,
|
||||
"step": 1,
|
||||
"description": "Number of frames skipped between each echo sample."
|
||||
},
|
||||
{
|
||||
"id": "echoTint",
|
||||
"label": "Echo Tint",
|
||||
"type": "color",
|
||||
"default": [0.65, 0.85, 1.0, 1.0]
|
||||
"default": [
|
||||
0.65,
|
||||
0.85,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Tint applied to older echo frames."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -15,18 +15,20 @@
|
||||
"label": "Current Mix",
|
||||
"type": "float",
|
||||
"default": 0.72,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Contribution of the current frame in the temporal blend."
|
||||
},
|
||||
{
|
||||
"id": "trailMix",
|
||||
"label": "Trail Mix",
|
||||
"type": "float",
|
||||
"default": 0.28,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Contribution of older frames in the temporal blend."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,19 +14,21 @@
|
||||
"id": "holdFrames",
|
||||
"label": "Hold Frames",
|
||||
"type": "float",
|
||||
"default": 3.0,
|
||||
"min": 0.0,
|
||||
"max": 7.0,
|
||||
"step": 0.1
|
||||
"default": 3,
|
||||
"min": 0,
|
||||
"max": 7,
|
||||
"step": 0.1,
|
||||
"description": "How many previous frames to hold for the low-FPS effect."
|
||||
},
|
||||
{
|
||||
"id": "blendAmount",
|
||||
"label": "Blend",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mix amount for the processed result."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -17,16 +17,30 @@
|
||||
"type": "text",
|
||||
"default": "VIDEO SHADER",
|
||||
"font": "roboto",
|
||||
"maxLength": 64
|
||||
"maxLength": 64,
|
||||
"description": "Text string rendered into the SDF text texture."
|
||||
},
|
||||
{
|
||||
"id": "position",
|
||||
"label": "Position",
|
||||
"type": "vec2",
|
||||
"default": [0.08, 0.12],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0.08,
|
||||
0.12
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Normalized placement of the text block in the frame."
|
||||
},
|
||||
{
|
||||
"id": "scale",
|
||||
@@ -35,37 +49,52 @@
|
||||
"default": 0.42,
|
||||
"min": 0.1,
|
||||
"max": 3,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Text size multiplier."
|
||||
},
|
||||
{
|
||||
"id": "fillColor",
|
||||
"label": "Fill",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Main text fill color and alpha."
|
||||
},
|
||||
{
|
||||
"id": "outlineColor",
|
||||
"label": "Outline",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 0.8]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0.8
|
||||
],
|
||||
"description": "Text outline color and alpha."
|
||||
},
|
||||
{
|
||||
"id": "outlineWidth",
|
||||
"label": "Outline Width",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Width of the SDF outline around the text."
|
||||
},
|
||||
{
|
||||
"id": "softness",
|
||||
"label": "Softness",
|
||||
"type": "float",
|
||||
"default": 0.04,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.3,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Smoothness of the SDF text edge."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,25 +8,40 @@
|
||||
{
|
||||
"id": "drop",
|
||||
"label": "Drop",
|
||||
"type": "trigger"
|
||||
"type": "trigger",
|
||||
"description": "Momentary trigger that starts a new ripple from the selected center."
|
||||
},
|
||||
{
|
||||
"id": "center",
|
||||
"label": "Center",
|
||||
"type": "vec2",
|
||||
"default": [0.5, 0.5],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.01, 0.01]
|
||||
"default": [
|
||||
0.5,
|
||||
0.5
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Origin of the triggered ripple in normalized coordinates."
|
||||
},
|
||||
{
|
||||
"id": "strength",
|
||||
"label": "Strength",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.3,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Amount of UV distortion caused by the ripple."
|
||||
},
|
||||
{
|
||||
"id": "speed",
|
||||
@@ -34,8 +49,9 @@
|
||||
"type": "float",
|
||||
"default": 0.3,
|
||||
"min": 0.3,
|
||||
"max": 5.0,
|
||||
"step": 0.01
|
||||
"max": 5,
|
||||
"step": 0.01,
|
||||
"description": "How long the ripple takes to expand and fade."
|
||||
},
|
||||
{
|
||||
"id": "width",
|
||||
@@ -44,7 +60,8 @@
|
||||
"default": 0.09,
|
||||
"min": 0.01,
|
||||
"max": 0.25,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Thickness of the travelling ripple ring."
|
||||
},
|
||||
{
|
||||
"id": "damping",
|
||||
@@ -52,8 +69,9 @@
|
||||
"type": "float",
|
||||
"default": 0.25,
|
||||
"min": 0.05,
|
||||
"max": 3.0,
|
||||
"step": 0.05
|
||||
"max": 3,
|
||||
"step": 0.05,
|
||||
"description": "How quickly the ripple fades as it expands."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"id": "showLocalTime",
|
||||
"label": "Show Local Time",
|
||||
"type": "bool",
|
||||
"default": true
|
||||
"default": true,
|
||||
"description": "Uses the PC UTC offset for local time; disable for pure UTC."
|
||||
},
|
||||
{
|
||||
"id": "clockScale",
|
||||
@@ -18,19 +19,32 @@
|
||||
"default": 0.7,
|
||||
"min": 0.25,
|
||||
"max": 0.95,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Size of the clock face relative to the frame."
|
||||
},
|
||||
{
|
||||
"id": "faceColor",
|
||||
"label": "Face Color",
|
||||
"type": "color",
|
||||
"default": [0.03, 0.04, 0.05, 0.82]
|
||||
"default": [
|
||||
0.03,
|
||||
0.04,
|
||||
0.05,
|
||||
0.82
|
||||
],
|
||||
"description": "Clock face fill color and alpha."
|
||||
},
|
||||
{
|
||||
"id": "accentColor",
|
||||
"label": "Accent Color",
|
||||
"type": "color",
|
||||
"default": [0.1, 0.62, 0.86, 1.0]
|
||||
"default": [
|
||||
0.1,
|
||||
0.62,
|
||||
0.86,
|
||||
1
|
||||
],
|
||||
"description": "Accent color for the seconds hand and glow."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -4,123 +4,156 @@
|
||||
"description": "VHS with wiggle, smear, and YIQ-style color separation inspired by nostalgic analog references.",
|
||||
"category": "Glitch",
|
||||
"entryPoint": "shadeVideo",
|
||||
"passes": [
|
||||
{
|
||||
"id": "tapeSmear",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "buildTapeSmear",
|
||||
"inputs": [
|
||||
"layerInput"
|
||||
],
|
||||
"output": "tapeSmear"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "finishVhs",
|
||||
"inputs": [
|
||||
"tapeSmear"
|
||||
],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"id": "wiggle",
|
||||
"label": "Wiggle",
|
||||
"type": "float",
|
||||
"default": 0.03,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Horizontal tape wobble amount."
|
||||
},
|
||||
{
|
||||
"id": "wiggleSpeed",
|
||||
"label": "Wiggle Speed",
|
||||
"type": "float",
|
||||
"default": 25.0,
|
||||
"min": 0.0,
|
||||
"max": 100.0,
|
||||
"step": 1.0
|
||||
"default": 25,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1,
|
||||
"description": "Speed of the tape wobble modulation."
|
||||
},
|
||||
{
|
||||
"id": "smear",
|
||||
"label": "Smear",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Horizontal color/luma smear strength."
|
||||
},
|
||||
{
|
||||
"id": "blurSamples",
|
||||
"label": "Blur Samples",
|
||||
"type": "float",
|
||||
"default": 15.0,
|
||||
"min": 3.0,
|
||||
"max": 15.0,
|
||||
"step": 1.0
|
||||
"default": 15,
|
||||
"min": 3,
|
||||
"max": 15,
|
||||
"step": 1,
|
||||
"description": "Number of smear samples; higher values are smoother but heavier."
|
||||
},
|
||||
{
|
||||
"id": "vignetteAmount",
|
||||
"label": "Vignette",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.6,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Darkens and softens the frame edges."
|
||||
},
|
||||
{
|
||||
"id": "aberrationAmount",
|
||||
"label": "Aberration",
|
||||
"type": "float",
|
||||
"default": 0.75,
|
||||
"min": 0.0,
|
||||
"max": 3.0,
|
||||
"step": 0.05
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"step": 0.05,
|
||||
"description": "Color-channel separation amount."
|
||||
},
|
||||
{
|
||||
"id": "halationAmount",
|
||||
"label": "Halation",
|
||||
"type": "float",
|
||||
"default": 0.12,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Red/orange glow around bright areas."
|
||||
},
|
||||
{
|
||||
"id": "bloomAmount",
|
||||
"label": "Bloom",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.6,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Soft glow strength from bright video regions."
|
||||
},
|
||||
{
|
||||
"id": "fadeAmount",
|
||||
"label": "Fade",
|
||||
"type": "float",
|
||||
"default": 0.22,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.75,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Washed-out tape fade amount."
|
||||
},
|
||||
{
|
||||
"id": "noiseAmount",
|
||||
"label": "Noise",
|
||||
"type": "float",
|
||||
"default": 0.055,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Fine grain/noise intensity."
|
||||
},
|
||||
{
|
||||
"id": "staticAmount",
|
||||
"label": "Analog Static",
|
||||
"type": "float",
|
||||
"default": 0.045,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.25,
|
||||
"step": 0.005
|
||||
"step": 0.005,
|
||||
"description": "Random bright static intensity."
|
||||
},
|
||||
{
|
||||
"id": "staticLines",
|
||||
"label": "Static Lines",
|
||||
"type": "float",
|
||||
"default": 0.65,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 1.5,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Horizontal static line visibility."
|
||||
},
|
||||
{
|
||||
"id": "noiseSize",
|
||||
"label": "Noise Size",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.25,
|
||||
"max": 6.0,
|
||||
"step": 0.05
|
||||
"max": 6,
|
||||
"step": 0.05,
|
||||
"description": "Scale of the generated noise pattern."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -158,10 +158,15 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
|
||||
return sum;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
float distortedTapeTime(ShaderContext context)
|
||||
{
|
||||
return context.time + context.startupRandom * 113.0;
|
||||
}
|
||||
|
||||
float4 buildTapeSmear(ShaderContext context)
|
||||
{
|
||||
float2 uv = context.uv;
|
||||
float time = context.time + context.startupRandom * 113.0;
|
||||
float time = distortedTapeTime(context);
|
||||
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
|
||||
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
|
||||
|
||||
@@ -189,6 +194,13 @@ float4 shadeVideo(ShaderContext context)
|
||||
float q = rgb2yiq(qBlur).b;
|
||||
|
||||
float3 color = yiq2rgb(float3(y, i, q)) - pow(s + e * 2.0, 3.0);
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
float4 finishVhs(ShaderContext context)
|
||||
{
|
||||
float time = distortedTapeTime(context);
|
||||
float3 color = sampleVideo(context.uv).rgb;
|
||||
|
||||
float2 centered = context.uv * 2.0 - 1.0;
|
||||
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
|
||||
@@ -238,3 +250,8 @@ float4 shadeVideo(ShaderContext context)
|
||||
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
return finishVhs(context);
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"id": "spinSpeed",
|
||||
"label": "Spin Speed",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01
|
||||
"default": 1,
|
||||
"min": 0,
|
||||
"max": 4,
|
||||
"step": 0.01,
|
||||
"description": "Rotation speed of the cube."
|
||||
},
|
||||
{
|
||||
"id": "cubeScale",
|
||||
@@ -21,25 +22,28 @@
|
||||
"default": 0.85,
|
||||
"min": 0.3,
|
||||
"max": 1.4,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Size of the cube in the frame."
|
||||
},
|
||||
{
|
||||
"id": "faceZoom",
|
||||
"label": "Face Zoom",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.5,
|
||||
"max": 2.0,
|
||||
"step": 0.01
|
||||
"max": 2,
|
||||
"step": 0.01,
|
||||
"description": "Zoom applied to the video on each cube face."
|
||||
},
|
||||
{
|
||||
"id": "backgroundMix",
|
||||
"label": "Background Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Mixes the original video behind the generated cube."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,28 +9,43 @@
|
||||
"id": "zoom",
|
||||
"label": "Zoom",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.1,
|
||||
"max": 8.0,
|
||||
"step": 0.01
|
||||
"max": 8,
|
||||
"step": 0.01,
|
||||
"description": "Scales the source image before edge handling is applied."
|
||||
},
|
||||
{
|
||||
"id": "pan",
|
||||
"label": "Pan",
|
||||
"type": "vec2",
|
||||
"default": [0.0, 0.0],
|
||||
"min": [-1.0, -1.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.001, 0.001]
|
||||
"default": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"min": [
|
||||
-1,
|
||||
-1
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.001,
|
||||
0.001
|
||||
],
|
||||
"description": "Moves the source image in normalized frame units."
|
||||
},
|
||||
{
|
||||
"id": "rotationDegrees",
|
||||
"label": "Rotation",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": -180.0,
|
||||
"max": 180.0,
|
||||
"step": 0.1
|
||||
"default": 0,
|
||||
"min": -180,
|
||||
"max": 180,
|
||||
"step": 0.1,
|
||||
"description": "Rotates the source image around the frame center."
|
||||
},
|
||||
{
|
||||
"id": "edgeMode",
|
||||
@@ -38,17 +53,36 @@
|
||||
"type": "enum",
|
||||
"default": "black",
|
||||
"options": [
|
||||
{ "value": "black", "label": "Black" },
|
||||
{ "value": "clamp", "label": "Clamp" },
|
||||
{ "value": "wrap", "label": "Wrap" },
|
||||
{ "value": "mirror", "label": "Mirror" }
|
||||
]
|
||||
{
|
||||
"value": "black",
|
||||
"label": "Black"
|
||||
},
|
||||
{
|
||||
"value": "clamp",
|
||||
"label": "Clamp"
|
||||
},
|
||||
{
|
||||
"value": "wrap",
|
||||
"label": "Wrap"
|
||||
},
|
||||
{
|
||||
"value": "mirror",
|
||||
"label": "Mirror"
|
||||
}
|
||||
],
|
||||
"description": "Chooses how samples outside the source frame are filled."
|
||||
},
|
||||
{
|
||||
"id": "outsideColor",
|
||||
"label": "Outside Color",
|
||||
"type": "color",
|
||||
"default": [0.0, 0.0, 0.0, 1.0]
|
||||
"default": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
],
|
||||
"description": "Color used where the remapped image samples outside the source frame."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,44 +33,61 @@
|
||||
"type": "float",
|
||||
"default": 0.4,
|
||||
"min": 0.1,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Size of the waveform panel."
|
||||
},
|
||||
{
|
||||
"id": "overlayPosition",
|
||||
"label": "Overlay Position",
|
||||
"type": "vec2",
|
||||
"default": [0.24, 0.76],
|
||||
"min": [0.0, 0.0],
|
||||
"max": [1.0, 1.0],
|
||||
"step": [0.01, 0.01]
|
||||
"default": [
|
||||
0.24,
|
||||
0.76
|
||||
],
|
||||
"min": [
|
||||
0,
|
||||
0
|
||||
],
|
||||
"max": [
|
||||
1,
|
||||
1
|
||||
],
|
||||
"step": [
|
||||
0.01,
|
||||
0.01
|
||||
],
|
||||
"description": "Normalized position of the waveform panel."
|
||||
},
|
||||
{
|
||||
"id": "overlayPadding",
|
||||
"label": "Overlay Padding",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.25,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Padding inside the waveform panel."
|
||||
},
|
||||
{
|
||||
"id": "waveformOpacity",
|
||||
"label": "Waveform Opacity",
|
||||
"type": "float",
|
||||
"default": 0.75,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the waveform trace."
|
||||
},
|
||||
{
|
||||
"id": "backgroundOpacity",
|
||||
"label": "Background",
|
||||
"type": "float",
|
||||
"default": 0.75,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the waveform background panel."
|
||||
},
|
||||
{
|
||||
"id": "lineThickness",
|
||||
@@ -78,50 +95,61 @@
|
||||
"type": "float",
|
||||
"default": 1.5,
|
||||
"min": 0.5,
|
||||
"max": 10.0,
|
||||
"step": 0.1
|
||||
"max": 10,
|
||||
"step": 0.1,
|
||||
"description": "Thickness of the waveform trace in pixels."
|
||||
},
|
||||
{
|
||||
"id": "gridOpacity",
|
||||
"label": "Grid Opacity",
|
||||
"type": "float",
|
||||
"default": 1,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Opacity of the waveform grid and labels."
|
||||
},
|
||||
{
|
||||
"id": "waveformSamples",
|
||||
"label": "Waveform Samples",
|
||||
"type": "float",
|
||||
"default": 64.0,
|
||||
"min": 8.0,
|
||||
"max": 96.0,
|
||||
"step": 1.0
|
||||
"default": 64,
|
||||
"min": 8,
|
||||
"max": 96,
|
||||
"step": 1,
|
||||
"description": "Number of vertical samples used to build the waveform."
|
||||
},
|
||||
{
|
||||
"id": "waveformGain",
|
||||
"label": "Waveform Gain",
|
||||
"type": "float",
|
||||
"default": 12.0,
|
||||
"min": 1.0,
|
||||
"max": 32.0,
|
||||
"step": 0.5
|
||||
"default": 12,
|
||||
"min": 1,
|
||||
"max": 32,
|
||||
"step": 0.5,
|
||||
"description": "Brightness/intensity of waveform hits."
|
||||
},
|
||||
{
|
||||
"id": "waveformNoiseReduction",
|
||||
"label": "Noise Reduction",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.6,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Suppresses faint waveform speckle."
|
||||
},
|
||||
{
|
||||
"id": "waveformColor",
|
||||
"label": "Waveform Color",
|
||||
"type": "color",
|
||||
"default": [1.0, 1.0, 1.0, 1.0]
|
||||
"default": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"description": "Color used for the waveform trace."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,11 @@
|
||||
"id": "patchCount",
|
||||
"label": "Patch Count",
|
||||
"type": "float",
|
||||
"default": 15.0,
|
||||
"min": 2.0,
|
||||
"max": 21.0,
|
||||
"step": 1.0
|
||||
"default": 15,
|
||||
"min": 2,
|
||||
"max": 21,
|
||||
"step": 1,
|
||||
"description": "Number of exposure patches in the chart."
|
||||
},
|
||||
{
|
||||
"id": "baseLevel",
|
||||
@@ -21,25 +22,28 @@
|
||||
"default": 0.00006103515625,
|
||||
"min": 0.000001,
|
||||
"max": 0.01,
|
||||
"step": 0.000001
|
||||
"step": 0.000001,
|
||||
"description": "Brightness of the darkest patch before tone mapping."
|
||||
},
|
||||
{
|
||||
"id": "peakLevel",
|
||||
"label": "Peak Level",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"default": 1,
|
||||
"min": 0.01,
|
||||
"max": 1.0,
|
||||
"step": 0.001
|
||||
"max": 1,
|
||||
"step": 0.001,
|
||||
"description": "Brightness limit for the brightest patch."
|
||||
},
|
||||
{
|
||||
"id": "gammaEncode",
|
||||
"label": "Display Gamma",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 1.0,
|
||||
"default": 1,
|
||||
"min": 1,
|
||||
"max": 2.6,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Gamma value used when Tone Curve is Display Gamma."
|
||||
},
|
||||
{
|
||||
"id": "toneCurve",
|
||||
@@ -59,7 +63,8 @@
|
||||
"value": "rec709",
|
||||
"label": "Rec.709"
|
||||
}
|
||||
]
|
||||
],
|
||||
"description": "Output encoding used for the chart patches."
|
||||
},
|
||||
{
|
||||
"id": "chartScale",
|
||||
@@ -67,56 +72,63 @@
|
||||
"type": "float",
|
||||
"default": 0.86,
|
||||
"min": 0.25,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Overall size of the chart in frame."
|
||||
},
|
||||
{
|
||||
"id": "gapSize",
|
||||
"label": "Gap Size",
|
||||
"type": "float",
|
||||
"default": 0.18,
|
||||
"min": 0.0,
|
||||
"min": 0,
|
||||
"max": 0.45,
|
||||
"step": 0.01
|
||||
"step": 0.01,
|
||||
"description": "Spacing between exposure patches."
|
||||
},
|
||||
{
|
||||
"id": "vertical",
|
||||
"label": "Vertical",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Stacks patches vertically instead of horizontally."
|
||||
},
|
||||
{
|
||||
"id": "reverseOrder",
|
||||
"label": "Reverse Order",
|
||||
"type": "bool",
|
||||
"default": false
|
||||
"default": false,
|
||||
"description": "Reverses the dark-to-bright patch order."
|
||||
},
|
||||
{
|
||||
"id": "backgroundLevel",
|
||||
"label": "Background",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.001
|
||||
"step": 0.001,
|
||||
"description": "Background brightness behind the chart."
|
||||
},
|
||||
{
|
||||
"id": "borderLevel",
|
||||
"label": "Border",
|
||||
"type": "float",
|
||||
"default": 0.08,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.001
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.001,
|
||||
"description": "Brightness of patch borders."
|
||||
},
|
||||
{
|
||||
"id": "sourceMix",
|
||||
"label": "Source Mix",
|
||||
"type": "float",
|
||||
"default": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01
|
||||
"default": 0,
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01,
|
||||
"description": "Blends the generated effect with the incoming video."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ void TestValidManifest()
|
||||
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
|
||||
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
|
||||
"parameters": [
|
||||
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 },
|
||||
{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
|
||||
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
|
||||
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
|
||||
{ "value": "soft", "label": "Soft" },
|
||||
@@ -78,8 +78,37 @@ void TestValidManifest()
|
||||
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
|
||||
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
|
||||
Expect(package.parameters.size() == 4, "parameters parse");
|
||||
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
|
||||
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
|
||||
Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
|
||||
Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
void TestExplicitPassManifest()
|
||||
{
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
WriteShaderPackage(root, "multi", R"({
|
||||
"id": "multi-pass",
|
||||
"name": "Multi Pass",
|
||||
"passes": [
|
||||
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
|
||||
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
|
||||
],
|
||||
"parameters": []
|
||||
})");
|
||||
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||
WriteFile(root / "multi" / "final.slang", "float4 finish(float2 uv) { return float4(uv, 1.0, 1.0); }\n");
|
||||
|
||||
ShaderPackageRegistry registry(4);
|
||||
ShaderPackage package;
|
||||
std::string error;
|
||||
Expect(registry.ParseManifest(root / "multi" / "shader.json", package, error), "explicit pass manifest parses");
|
||||
Expect(package.passes.size() == 2, "explicit passes parse");
|
||||
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
|
||||
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
|
||||
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
@@ -231,6 +260,7 @@ void TestInvalidPackageDoesNotFailScan()
|
||||
int main()
|
||||
{
|
||||
TestValidManifest();
|
||||
TestExplicitPassManifest();
|
||||
TestMissingFontAsset();
|
||||
TestInvalidManifest();
|
||||
TestInvalidTemporalSettings();
|
||||
|
||||
@@ -81,15 +81,19 @@ int main()
|
||||
if (packageIt == packagesById.end())
|
||||
continue;
|
||||
|
||||
std::string fragmentShaderSource;
|
||||
std::string compileError;
|
||||
if (!compiler.BuildLayerFragmentShaderSource(packageIt->second, fragmentShaderSource, compileError))
|
||||
const ShaderPackage& shaderPackage = packageIt->second;
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
Fail("Shader package '" + packageId + "' failed Slang validation: " + compileError);
|
||||
continue;
|
||||
std::string fragmentShaderSource;
|
||||
std::string compileError;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, compileError))
|
||||
{
|
||||
Fail("Shader package '" + packageId + "' pass '" + pass.id + "' failed Slang validation: " + compileError);
|
||||
continue;
|
||||
}
|
||||
if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
|
||||
Fail("Shader package '" + packageId + "' pass '" + pass.id + "' generated GLSL without the expected patched GLSL version header.");
|
||||
}
|
||||
if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
|
||||
Fail("Shader package '" + packageId + "' generated GLSL without the expected patched GLSL version header.");
|
||||
}
|
||||
|
||||
std::error_code removeError;
|
||||
|
||||
@@ -31,7 +31,7 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SelectPreferredFormats(const VideoFormatSelection&, std::string&) override
|
||||
bool SelectPreferredFormats(const VideoFormatSelection&, bool, std::string&) override
|
||||
{
|
||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
mState.outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
@@ -120,7 +120,7 @@ int main()
|
||||
bool outputSeen = false;
|
||||
|
||||
Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds");
|
||||
Expect(device.SelectPreferredFormats(selection, error), "fake format selection succeeds");
|
||||
Expect(device.SelectPreferredFormats(selection, false, error), "fake format selection succeeds");
|
||||
Expect(device.ConfigureInput([&](const VideoIOFrame& frame) {
|
||||
inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||
}, selection.input, error), "fake input config succeeds");
|
||||
|
||||
@@ -22,6 +22,7 @@ void TestPreferredFormatSelection()
|
||||
Expect(ChoosePreferredVideoIOFormat(true) == VideoIOPixelFormat::V210, "10-bit is preferred when supported");
|
||||
Expect(ChoosePreferredVideoIOFormat(false) == VideoIOPixelFormat::Uyvy8, "8-bit is used as fallback");
|
||||
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::V210) == bmdFormat10BitYUV, "v210 maps to DeckLink 10-bit YUV");
|
||||
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Yuva10) == bmdFormat10BitYUVA, "Ay10 maps to DeckLink 10-bit YUVA");
|
||||
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Uyvy8) == bmdFormat8BitYUV, "UYVY maps to DeckLink 8-bit YUV");
|
||||
Expect(DeckLinkPixelFormatForVideoIO(VideoIOPixelFormat::Bgra8) == bmdFormat8BitBGRA, "BGRA maps to DeckLink 8-bit BGRA");
|
||||
}
|
||||
@@ -31,11 +32,15 @@ void TestRowByteHelpers()
|
||||
Expect(MinimumV210RowBytes(1920) == 5120, "1920-wide v210 active row bytes");
|
||||
Expect(MinimumV210RowBytes(1280) == 3424, "1280-wide v210 active row bytes rounds up to six-pixel group");
|
||||
Expect(MinimumV210RowBytes(3840) == 10240, "3840-wide v210 active row bytes");
|
||||
Expect(MinimumYuva10RowBytes(1920) == 7680, "1920-wide Ay10 row bytes");
|
||||
Expect(MinimumYuva10RowBytes(1919) == 7680, "Ay10 row bytes round up to 64-pixel boundary");
|
||||
Expect(MinimumYuva10RowBytes(3840) == 15360, "3840-wide Ay10 row bytes");
|
||||
Expect(PackedTextureWidthFromRowBytes(5120) == 1280, "packed texture width is row bytes divided into RGBA byte texels");
|
||||
Expect(ActiveV210WordsForWidth(1920) == 1280, "active v210 words match 1920 width");
|
||||
Expect(VideoIORowBytes(VideoIOPixelFormat::Uyvy8, 1920) == 3840, "UYVY row bytes");
|
||||
Expect(VideoIORowBytes(VideoIOPixelFormat::Bgra8, 1920) == 7680, "BGRA row bytes");
|
||||
Expect(VideoIORowBytes(VideoIOPixelFormat::V210, 1920) == 5120, "v210 row bytes");
|
||||
Expect(VideoIORowBytes(VideoIOPixelFormat::Yuva10, 1920) == 7680, "Ay10 row bytes");
|
||||
}
|
||||
|
||||
void TestV210PackUnpack()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Wheel from "@uiw/react-color-wheel";
|
||||
import { hsvaToRgba, rgbaToHsva } from "@uiw/color-convert";
|
||||
import { Copy, RotateCcw, Zap } from "lucide-react";
|
||||
import { RotateCcw, Zap } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useThrottledParameterValue } from "../hooks/useThrottledParameterValue";
|
||||
import { ParameterValueDisplay } from "./ParameterValueDisplay";
|
||||
|
||||
function valuesMatch(left, right) {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
@@ -21,7 +21,10 @@ function ParameterHeader({ layer, parameter, onReset, resetDisabled }) {
|
||||
|
||||
return (
|
||||
<div className="parameter__header">
|
||||
<label>{parameter.label}</label>
|
||||
<div className="parameter__title">
|
||||
<label>{parameter.label}</label>
|
||||
{parameter.description ? <p title={parameter.description}>{parameter.description}</p> : null}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="parameter__osc"
|
||||
@@ -29,8 +32,7 @@ function ParameterHeader({ layer, parameter, onReset, resetDisabled }) {
|
||||
aria-label={`Copy OSC route ${oscRoute}`}
|
||||
onClick={copyRoute}
|
||||
>
|
||||
<span>{oscRoute}</span>
|
||||
<Copy size={13} strokeWidth={1.75} aria-hidden="true" />
|
||||
<span aria-hidden="true">OSC</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -88,12 +90,12 @@ function hsvaToColorValue(hsva, alpha) {
|
||||
}
|
||||
|
||||
export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const colorControlRef = useRef(null);
|
||||
const {
|
||||
appliedValue,
|
||||
beginInteraction,
|
||||
draftValue,
|
||||
endInteraction,
|
||||
isPending,
|
||||
scheduleSendValue,
|
||||
sendValue,
|
||||
} = useThrottledParameterValue(parameter, onParameterChange);
|
||||
@@ -105,6 +107,32 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
sendValue(defaultValue);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!colorPickerOpen) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handlePointerDown(event) {
|
||||
if (!colorControlRef.current || !colorControlRef.current.contains(event.target)) {
|
||||
setColorPickerOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "Escape") {
|
||||
setColorPickerOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [colorPickerOpen]);
|
||||
|
||||
const header = (
|
||||
<ParameterHeader
|
||||
layer={layer}
|
||||
@@ -118,36 +146,37 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<div className="parameter__pair">
|
||||
<input
|
||||
type="range"
|
||||
min={parameter.min?.[0] ?? 0}
|
||||
max={parameter.max?.[0] ?? 1}
|
||||
step={parameter.step?.[0] ?? 0.01}
|
||||
value={draftValue}
|
||||
onMouseDown={beginInteraction}
|
||||
onPointerDown={beginInteraction}
|
||||
onTouchStart={beginInteraction}
|
||||
onChange={(event) => scheduleSendValue(Number(event.target.value))}
|
||||
onMouseUp={endInteraction}
|
||||
onTouchEnd={endInteraction}
|
||||
onPointerUp={endInteraction}
|
||||
onKeyDown={beginInteraction}
|
||||
onKeyUp={endInteraction}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={parameter.min?.[0] ?? ""}
|
||||
max={parameter.max?.[0] ?? ""}
|
||||
step={parameter.step?.[0] ?? 0.01}
|
||||
value={draftValue}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(Number(event.target.value))}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<div className="parameter__control">
|
||||
<div className="parameter__pair">
|
||||
<input
|
||||
type="range"
|
||||
min={parameter.min?.[0] ?? 0}
|
||||
max={parameter.max?.[0] ?? 1}
|
||||
step={parameter.step?.[0] ?? 0.01}
|
||||
value={draftValue}
|
||||
onMouseDown={beginInteraction}
|
||||
onPointerDown={beginInteraction}
|
||||
onTouchStart={beginInteraction}
|
||||
onChange={(event) => scheduleSendValue(Number(event.target.value))}
|
||||
onMouseUp={endInteraction}
|
||||
onTouchEnd={endInteraction}
|
||||
onPointerUp={endInteraction}
|
||||
onKeyDown={beginInteraction}
|
||||
onKeyUp={endInteraction}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={parameter.min?.[0] ?? ""}
|
||||
max={parameter.max?.[0] ?? ""}
|
||||
step={parameter.step?.[0] ?? 0.01}
|
||||
value={draftValue}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(Number(event.target.value))}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -161,26 +190,28 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<div className="parameter__pair">
|
||||
{Array.from({ length: 2 }, (_, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="number"
|
||||
min={parameter.min?.[index] ?? ""}
|
||||
max={parameter.max?.[index] ?? ""}
|
||||
step={parameter.step?.[index] ?? 0.01}
|
||||
value={values[index]}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => {
|
||||
const next = [...values];
|
||||
next[index] = Number(event.target.value);
|
||||
sendValue(next);
|
||||
}}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
))}
|
||||
<div className="parameter__control">
|
||||
<div className="parameter__pair parameter__pair--vec2">
|
||||
{Array.from({ length: 2 }, (_, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="number"
|
||||
aria-label={`${parameter.label} ${index === 0 ? "X" : "Y"}`}
|
||||
min={parameter.min?.[index] ?? ""}
|
||||
max={parameter.max?.[index] ?? ""}
|
||||
step={parameter.step?.[index] ?? 0.01}
|
||||
value={values[index]}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => {
|
||||
const next = [...values];
|
||||
next[index] = Number(event.target.value);
|
||||
sendValue(next);
|
||||
}}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -193,69 +224,84 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
const hsva = colorValueToHsva(values);
|
||||
const wheelHsva = { ...hsva, v: 100 };
|
||||
const sendHsva = (nextHsva) => scheduleSendValue(hsvaToColorValue(nextHsva, values[3]));
|
||||
const updateColorComponent = (index, nextValue) => {
|
||||
const next = [...values];
|
||||
next[index] = Number(nextValue);
|
||||
sendValue(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="parameter">
|
||||
<section className="parameter parameter--color">
|
||||
{header}
|
||||
<div className="parameter__wheel-row">
|
||||
<div className="parameter__color-stack">
|
||||
<div
|
||||
className="parameter__wheel"
|
||||
onPointerDown={beginInteraction}
|
||||
onPointerUp={endInteraction}
|
||||
onPointerCancel={endInteraction}
|
||||
onBlur={endInteraction}
|
||||
<div className="parameter__control" ref={colorControlRef}>
|
||||
<div className="parameter__color-compact">
|
||||
<button
|
||||
type="button"
|
||||
className="parameter__swatch-button"
|
||||
title={`Open ${parameter.label} color picker`}
|
||||
aria-label={`Open ${parameter.label} color picker`}
|
||||
onClick={() => setColorPickerOpen((open) => !open)}
|
||||
>
|
||||
<Wheel
|
||||
color={wheelHsva}
|
||||
width={196}
|
||||
height={196}
|
||||
onChange={(color) => sendHsva({ ...color.hsva, v: hsva.v })}
|
||||
/>
|
||||
<span className="parameter__swatch" style={{ background: colorValueToHex(values) }} aria-hidden="true" />
|
||||
</button>
|
||||
<div className="parameter__rgba-grid">
|
||||
{["R", "G", "B", "A"].map((label, index) => (
|
||||
<label key={label} className="parameter__rgba-field">
|
||||
<span>{label}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={parameter.min?.[index] ?? 0}
|
||||
max={parameter.max?.[index] ?? 1}
|
||||
step={parameter.step?.[index] ?? 0.01}
|
||||
value={values[index]}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => updateColorComponent(index, event.target.value)}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="parameter__value-slider">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={Math.round(hsva.v)}
|
||||
aria-label={`${parameter.label} value`}
|
||||
onMouseDown={beginInteraction}
|
||||
</div>
|
||||
{colorPickerOpen ? (
|
||||
<div className="parameter__color-popover">
|
||||
<div
|
||||
className="parameter__wheel"
|
||||
onPointerDown={beginInteraction}
|
||||
onTouchStart={beginInteraction}
|
||||
onChange={(event) => sendHsva({ ...hsva, v: Number(event.target.value) })}
|
||||
onMouseUp={endInteraction}
|
||||
onTouchEnd={endInteraction}
|
||||
onPointerUp={endInteraction}
|
||||
onKeyDown={beginInteraction}
|
||||
onKeyUp={endInteraction}
|
||||
onPointerCancel={endInteraction}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="parameter__color-bottom">
|
||||
<label className="parameter__alpha">
|
||||
<span>Alpha</span>
|
||||
<input
|
||||
type="number"
|
||||
min={parameter.min?.[3] ?? 0}
|
||||
max={parameter.max?.[3] ?? 1}
|
||||
step={parameter.step?.[3] ?? 0.01}
|
||||
value={values[3]}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => {
|
||||
const next = [...values];
|
||||
next[3] = Number(event.target.value);
|
||||
sendValue(next);
|
||||
}}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</label>
|
||||
<div className="parameter__swatch" style={{ background: colorValueToHex(values) }} aria-hidden="true" />
|
||||
</div>
|
||||
>
|
||||
<Wheel
|
||||
color={wheelHsva}
|
||||
width={196}
|
||||
height={196}
|
||||
onChange={(color) => sendHsva({ ...color.hsva, v: hsva.v })}
|
||||
/>
|
||||
</div>
|
||||
<label className="parameter__value-slider">
|
||||
<span>Value</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={Math.round(hsva.v)}
|
||||
aria-label={`${parameter.label} value`}
|
||||
onMouseDown={beginInteraction}
|
||||
onPointerDown={beginInteraction}
|
||||
onTouchStart={beginInteraction}
|
||||
onChange={(event) => sendHsva({ ...hsva, v: Number(event.target.value) })}
|
||||
onMouseUp={endInteraction}
|
||||
onTouchEnd={endInteraction}
|
||||
onPointerUp={endInteraction}
|
||||
onKeyDown={beginInteraction}
|
||||
onKeyUp={endInteraction}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -264,17 +310,18 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<label className="toggle toggle--field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(draftValue)}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.checked)}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<span>{draftValue ? "Enabled" : "Disabled"}</span>
|
||||
</label>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
<div className="parameter__control">
|
||||
<label className="toggle toggle--field">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(draftValue)}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.checked)}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<span>{draftValue ? "Enabled" : "Disabled"}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -283,19 +330,20 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<select
|
||||
value={draftValue}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.value)}
|
||||
onBlur={endInteraction}
|
||||
>
|
||||
{parameter.options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
<div className="parameter__control">
|
||||
<select
|
||||
value={draftValue}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.value)}
|
||||
onBlur={endInteraction}
|
||||
>
|
||||
{parameter.options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -304,16 +352,17 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<input
|
||||
type="text"
|
||||
maxLength={parameter.maxLength ?? 64}
|
||||
placeholder={parameter.defaultValue ? `Default: ${parameter.defaultValue}` : ""}
|
||||
value={draftValue ?? ""}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.value)}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
<div className="parameter__control">
|
||||
<input
|
||||
type="text"
|
||||
maxLength={parameter.maxLength ?? 64}
|
||||
placeholder={parameter.defaultValue ? `Default: ${parameter.defaultValue}` : ""}
|
||||
value={draftValue ?? ""}
|
||||
onFocus={beginInteraction}
|
||||
onChange={(event) => sendValue(event.target.value)}
|
||||
onBlur={endInteraction}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -323,15 +372,16 @@ export function ParameterField({ layer, parameter, onParameterChange }) {
|
||||
return (
|
||||
<section className="parameter">
|
||||
{header}
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon parameter__trigger"
|
||||
onClick={() => sendValue(triggerCount + 1)}
|
||||
>
|
||||
<Zap size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Trigger</span>
|
||||
</button>
|
||||
<ParameterValueDisplay parameterType={parameter.type} value={appliedValue} pending={isPending} />
|
||||
<div className="parameter__control">
|
||||
<button
|
||||
type="button"
|
||||
className="button-with-icon parameter__trigger"
|
||||
onClick={() => sendValue(triggerCount + 1)}
|
||||
>
|
||||
<Zap size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Trigger</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
function formatNumber(value, digits = 3) {
|
||||
return Number(value ?? 0).toFixed(digits);
|
||||
}
|
||||
|
||||
export function formatParameterValue(parameterType, value) {
|
||||
if (parameterType === "float") {
|
||||
return formatNumber(value);
|
||||
}
|
||||
if (parameterType === "vec2" || parameterType === "color") {
|
||||
return (value ?? []).map((item) => formatNumber(item)).join(", ");
|
||||
}
|
||||
if (parameterType === "bool") {
|
||||
return value ? "Enabled" : "Disabled";
|
||||
}
|
||||
if (parameterType === "trigger") {
|
||||
return `Triggered ${Number(value ?? 0)} time${Number(value ?? 0) === 1 ? "" : "s"}`;
|
||||
}
|
||||
return `${value ?? ""}`;
|
||||
}
|
||||
|
||||
export function ParameterValueDisplay({ parameterType, value, pending }) {
|
||||
const valueText = formatParameterValue(parameterType, value);
|
||||
return (
|
||||
<div className={`parameter__value${pending ? " parameter__value--pending" : ""}`}>
|
||||
{pending ? `Applied: ${valueText}` : valueText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export function StackPresetToolbar({
|
||||
onClick={() => postJson("/api/reload", {})}
|
||||
>
|
||||
<RefreshCw size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||
<span>Reload shader</span>
|
||||
<span>Reload shaders</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -700,8 +700,6 @@ pre {
|
||||
.shader-picker__selected,
|
||||
.shader-picker__meta,
|
||||
.shader-picker__empty,
|
||||
.parameter__value,
|
||||
.parameter__alpha,
|
||||
.parameter__osc,
|
||||
.parameter__reset {
|
||||
color: var(--app-muted);
|
||||
@@ -876,31 +874,59 @@ pre {
|
||||
}
|
||||
|
||||
.parameter-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(17.5rem, 1fr));
|
||||
gap: 0.625rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 32rem), 1fr));
|
||||
gap: 0.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.parameter {
|
||||
padding: 0.75rem;
|
||||
position: relative;
|
||||
grid-template-columns: minmax(16rem, 0.9fr) minmax(18rem, 1.35fr);
|
||||
gap: 0.625rem;
|
||||
align-items: center;
|
||||
padding: 0.55rem 0.65rem;
|
||||
border: 1px solid var(--app-border);
|
||||
background: #141a23;
|
||||
}
|
||||
|
||||
.parameter__header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr) auto;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.parameter__title {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.parameter__title label {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parameter__title p {
|
||||
margin: 0.12rem 0 0;
|
||||
overflow: hidden;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.parameter__osc,
|
||||
.parameter__reset {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.375rem;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
min-height: 24px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
@@ -908,18 +934,16 @@ pre {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.parameter__reset {
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
min-width: 24px;
|
||||
color: var(--app-muted);
|
||||
.parameter__osc {
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.parameter__osc span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.parameter__reset {
|
||||
color: var(--app-muted);
|
||||
}
|
||||
|
||||
.parameter__osc svg,
|
||||
@@ -927,33 +951,89 @@ pre {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.parameter__value--pending {
|
||||
color: var(--app-warning);
|
||||
.parameter__control {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.parameter__pair {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(5.125rem, 1fr));
|
||||
grid-template-columns: minmax(8rem, 1fr) minmax(5.25rem, 7rem);
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.parameter__pair--vec2 {
|
||||
grid-template-columns: repeat(2, minmax(5.25rem, 1fr));
|
||||
}
|
||||
|
||||
.parameter__pair input[type="range"] {
|
||||
min-width: 7.5rem;
|
||||
}
|
||||
|
||||
.parameter__wheel-row {
|
||||
.parameter__color-compact {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 196px);
|
||||
gap: 0.625rem;
|
||||
align-items: start;
|
||||
justify-content: center;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 0.5rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.parameter__color-stack {
|
||||
.parameter__swatch-button {
|
||||
width: 42px;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
min-height: 42px;
|
||||
padding: 0.25rem;
|
||||
border-color: var(--app-border);
|
||||
background: var(--app-surface-2);
|
||||
}
|
||||
|
||||
.parameter__swatch-button:hover:not(:disabled) {
|
||||
background: var(--app-surface-2);
|
||||
border-color: rgba(26, 156, 219, 0.55);
|
||||
}
|
||||
|
||||
.parameter__swatch {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border-radius: var(--app-radius-sm);
|
||||
}
|
||||
|
||||
.parameter__rgba-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(3.5rem, 1fr));
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.parameter__rgba-field {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.parameter__rgba-field input {
|
||||
min-height: 32px;
|
||||
padding: 0.35rem 0.45rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.parameter__color-popover {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: calc(100% + 0.45rem);
|
||||
left: 0;
|
||||
display: grid;
|
||||
gap: 0.625rem;
|
||||
width: 196px;
|
||||
width: 224px;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-radius);
|
||||
background: #10151d;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.parameter__wheel {
|
||||
@@ -973,24 +1053,13 @@ pre {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.parameter__color-bottom {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(4.5rem, 0.85fr);
|
||||
gap: 0.625rem;
|
||||
align-items: end;
|
||||
width: 196px;
|
||||
}
|
||||
|
||||
.parameter__swatch {
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--app-border);
|
||||
border-radius: var(--app-radius-sm);
|
||||
}
|
||||
|
||||
.parameter__value-slider {
|
||||
display: block;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
width: 196px;
|
||||
color: var(--app-muted);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.parameter__value-slider input[type="range"] {
|
||||
@@ -1041,18 +1110,8 @@ pre {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.65), 0 1px 4px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.parameter__value-slider strong {
|
||||
text-align: right;
|
||||
min-height: 1rem;
|
||||
color: var(--app-text);
|
||||
font-size: 0.74rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.parameter__alpha {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
font-weight: 600;
|
||||
.parameter__trigger {
|
||||
min-width: 7rem;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
@@ -1141,8 +1200,8 @@ pre {
|
||||
.summary-grid,
|
||||
.kv-rows,
|
||||
.parameter-grid,
|
||||
.parameter__header,
|
||||
.parameter__wheel-row {
|
||||
.parameter,
|
||||
.parameter__header {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1154,12 +1213,20 @@ pre {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.parameter__swatch {
|
||||
grid-column: auto;
|
||||
.parameter__pair,
|
||||
.parameter__color-compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.parameter__color-stack,
|
||||
.parameter__color-bottom,
|
||||
.parameter__rgba-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.parameter__swatch-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.parameter__color-popover,
|
||||
.parameter__value-slider,
|
||||
.parameter__value-slider input[type="range"] {
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user