VisuTwin Canvas
C++ 3D Engine — Metal Backend
Loading...
Searching...
No Matches
metalLICPass.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025-2026 Arnis Lektauers
3//
4// Surface LIC post-processing pass implementation.
5// Follows the MetalTaaPass decomposition pattern.
6//
7#include "metalLICPass.h"
8
9#include "metalComposePass.h"
10#include "metalGraphicsDevice.h"
11#include "metalRenderPipeline.h"
12#include "metalTexture.h"
13#include "metalVertexBuffer.h"
21#include "spdlog/spdlog.h"
22
23namespace visutwin::canvas
24{
25 namespace
26 {
27 // ── Embedded Metal Shading Language source ───────────────────
28 //
29 // Image-space LIC fragment shader.
30 //
31 // For each pixel:
32 // 1. Read screen-space velocity from velocity texture (RG16Float)
33 // 2. Trace forward L steps through the velocity field
34 // 3. Trace backward L steps
35 // 4. Accumulate noise texture samples along the streamline
36 // 5. Normalize → grayscale LIC value
37 // 6. Apply contrast enhancement
38 //
39 // Velocity texture convention:
40 // RG = (vx, vy) in normalized texture coordinates per step
41 // (i.e., a velocity of (1, 0) moves 1 texel per integration step)
42 //
43 // Noise texture:
44 // R8Unorm, tiled across the screen. Animation phase offsets
45 // the noise sampling for animated LIC (OLIC).
46 //
47 constexpr const char* LIC_SOURCE = R"(
48#include <metal_stdlib>
49using namespace metal;
50
51struct LICVertexIn {
52 float3 position [[attribute(0)]];
53 float3 normal [[attribute(1)]];
54 float2 uv0 [[attribute(2)]];
55 float4 tangent [[attribute(3)]];
56 float2 uv1 [[attribute(4)]];
57};
58
59struct LICVarying {
60 float4 position [[position]];
61 float2 uv;
62};
63
64struct LICUniforms {
65 float2 textureSize; // Velocity texture dimensions (pixels)
66 float2 noiseSize; // Noise texture dimensions (pixels)
67 float stepSize; // Step size in normalized tex coords
68 float animationPhase; // Phase offset [0, 1] for animated LIC
69 int integrationSteps;// L: steps in each direction
70 float contrastLo; // Low end of contrast range
71 float contrastHi; // High end of contrast range
72 float minVelocity; // Stagnation threshold
73};
74
75vertex LICVarying licVertex(LICVertexIn in [[stage_in]])
76{
77 LICVarying out;
78 out.position = float4(in.position, 1.0);
79 out.uv = in.uv0;
80 return out;
81}
82
83fragment float4 licFragment(
84 LICVarying in [[stage_in]],
85 texture2d<float> velocityTexture [[texture(0)]],
86 texture2d<float> noiseTexture [[texture(1)]],
87 sampler linearSampler [[sampler(0)]],
88 constant LICUniforms& uniforms [[buffer(5)]])
89{
90 const float2 uv = clamp(in.uv, float2(0.0), float2(1.0));
91
92 // Read velocity at this pixel (in normalized texture coordinates)
93 const float2 vel = velocityTexture.sample(linearSampler, uv).rg;
94 const float speed = length(vel);
95
96 // Stagnation → neutral gray
97 if (speed < uniforms.minVelocity) {
98 return float4(0.5, 0.5, 0.5, 1.0);
99 }
100
101 // Normalized velocity direction
102 const float2 dir = vel / speed;
103 const float dt = uniforms.stepSize;
104
105 // Phase offset for animation (shifts noise sampling)
106 const float phaseX = uniforms.animationPhase * uniforms.noiseSize.x;
107
108 // Accumulate noise along the streamline
109 float sum = 0.0;
110 float weightSum = 0.0;
111
112 // Center sample
113 float2 noiseUV = uv * uniforms.textureSize / uniforms.noiseSize;
114 noiseUV.x += phaseX / uniforms.noiseSize.x;
115 float n = noiseTexture.sample(linearSampler, noiseUV).r;
116 sum += n;
117 weightSum += 1.0;
118
119 // Forward trace
120 float2 pos = uv;
121 for (int i = 0; i < uniforms.integrationSteps; ++i) {
122 float2 v = velocityTexture.sample(linearSampler, pos).rg;
123 float s = length(v);
124 if (s < uniforms.minVelocity) break;
125
126 // Normalize and step
127 float2 d = v / s;
128 pos += d * dt;
129
130 // Bounds check
131 if (pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0) break;
132
133 // Sample noise (tiled + animated)
134 noiseUV = pos * uniforms.textureSize / uniforms.noiseSize;
135 noiseUV.x += phaseX / uniforms.noiseSize.x;
136 n = noiseTexture.sample(linearSampler, noiseUV).r;
137 sum += n;
138 weightSum += 1.0;
139 }
140
141 // Backward trace
142 pos = uv;
143 for (int i = 0; i < uniforms.integrationSteps; ++i) {
144 float2 v = velocityTexture.sample(linearSampler, pos).rg;
145 float s = length(v);
146 if (s < uniforms.minVelocity) break;
147
148 float2 d = v / s;
149 pos -= d * dt;
150
151 if (pos.x < 0.0 || pos.x > 1.0 || pos.y < 0.0 || pos.y > 1.0) break;
152
153 noiseUV = pos * uniforms.textureSize / uniforms.noiseSize;
154 noiseUV.x += phaseX / uniforms.noiseSize.x;
155 n = noiseTexture.sample(linearSampler, noiseUV).r;
156 sum += n;
157 weightSum += 1.0;
158 }
159
160 // Normalize
161 float lic = (weightSum > 0.0) ? (sum / weightSum) : 0.5;
162
163 // Contrast enhancement: remap from [0,1] to [contrastLo, contrastHi]
164 // (simple linear stretch; full histogram equalization done in CPU pass)
165 lic = uniforms.contrastLo + lic * (uniforms.contrastHi - uniforms.contrastLo);
166 lic = clamp(lic, 0.0, 1.0);
167
168 return float4(lic, lic, lic, 1.0);
169}
170)";
171 } // anonymous namespace
172
174 : _device(device), _composePass(composePass)
175 {
176 }
177
179 {
180 if (_depthStencilState) {
181 _depthStencilState->release();
182 _depthStencilState = nullptr;
183 }
184 }
185
186 void MetalLICPass::ensureResources()
187 {
188 if (_shader && _composePass->vertexBuffer() && _composePass->vertexFormat() &&
189 _blendState && _depthState && _depthStencilState) {
190 return;
191 }
192
193 if (!_shader) {
194 ShaderDefinition definition;
195 definition.name = "SurfaceLICPass";
196 definition.vshader = "licVertex";
197 definition.fshader = "licFragment";
198 _shader = createShader(_device, definition, LIC_SOURCE);
199 }
200
201 if (!_blendState) {
202 _blendState = std::make_shared<BlendState>();
203 }
204 if (!_depthState) {
205 _depthState = std::make_shared<DepthState>();
206 }
207 if (!_depthStencilState && _device->raw()) {
208 auto* depthDesc = MTL::DepthStencilDescriptor::alloc()->init();
209 depthDesc->setDepthCompareFunction(MTL::CompareFunctionAlways);
210 depthDesc->setDepthWriteEnabled(false);
211 _depthStencilState = _device->raw()->newDepthStencilState(depthDesc);
212 depthDesc->release();
213 }
214 }
215
216 void MetalLICPass::execute(MTL::RenderCommandEncoder* encoder,
217 Texture* velocityTexture,
218 Texture* noiseTexture,
219 const int integrationSteps,
220 const float stepSize,
221 const float animationPhase,
222 const float contrastLo,
223 const float contrastHi,
224 MetalRenderPipeline* pipeline,
225 const std::shared_ptr<RenderTarget>& renderTarget,
226 const std::vector<std::shared_ptr<MetalBindGroupFormat>>& bindGroupFormats,
227 MTL::SamplerState* defaultSampler,
228 MTL::DepthStencilState* defaultDepthStencilState)
229 {
230 if (!encoder || !velocityTexture || !noiseTexture) {
231 return;
232 }
233
234 ensureResources();
235 if (!_shader || !_composePass->vertexBuffer() || !_composePass->vertexFormat() ||
236 !_blendState || !_depthState) {
237 spdlog::warn("[MetalLICPass] missing resources");
238 return;
239 }
240
241 Primitive primitive;
242 primitive.type = PRIMITIVE_TRIANGLES;
243 primitive.base = 0;
244 primitive.count = 3;
245 primitive.indexed = false;
246
247 auto pipelineState = pipeline->get(primitive, _composePass->vertexFormat(), nullptr, -1,
248 _shader, renderTarget, bindGroupFormats, _blendState, _depthState,
249 CullMode::CULLFACE_NONE, false, nullptr, nullptr);
250 if (!pipelineState) {
251 spdlog::warn("[MetalLICPass] failed to get pipeline state");
252 return;
253 }
254
255 auto* vb = dynamic_cast<MetalVertexBuffer*>(_composePass->vertexBuffer().get());
256 if (!vb || !vb->raw()) {
257 spdlog::warn("[MetalLICPass] missing vertex buffer");
258 return;
259 }
260
261 encoder->setRenderPipelineState(pipelineState);
262 encoder->setCullMode(MTL::CullModeNone);
263 encoder->setDepthStencilState(
264 _depthStencilState ? _depthStencilState : defaultDepthStencilState);
265 encoder->setVertexBuffer(vb->raw(), 0, 0);
266
267 // Bind textures
268 auto* velHw = dynamic_cast<gpu::MetalTexture*>(velocityTexture->impl());
269 auto* noiseHw = dynamic_cast<gpu::MetalTexture*>(noiseTexture->impl());
270
271 encoder->setFragmentTexture(velHw ? velHw->raw() : nullptr, 0);
272 encoder->setFragmentTexture(noiseHw ? noiseHw->raw() : nullptr, 1);
273 if (defaultSampler) {
274 encoder->setFragmentSamplerState(defaultSampler, 0);
275 }
276
277 // Pack uniforms
278 struct alignas(16) LICUniforms
279 {
280 simd::float2 textureSize;
281 simd::float2 noiseSize;
282 float stepSize;
283 float animationPhase;
284 int32_t integrationSteps;
285 float contrastLo;
286 float contrastHi;
287 float minVelocity;
288 } uniforms{};
289
290 uniforms.textureSize = simd::float2{
291 static_cast<float>(std::max(velocityTexture->width(), 1u)),
292 static_cast<float>(std::max(velocityTexture->height(), 1u))
293 };
294 uniforms.noiseSize = simd::float2{
295 static_cast<float>(std::max(noiseTexture->width(), 1u)),
296 static_cast<float>(std::max(noiseTexture->height(), 1u))
297 };
298 uniforms.stepSize = stepSize;
299 uniforms.animationPhase = animationPhase;
300 uniforms.integrationSteps = integrationSteps;
301 uniforms.contrastLo = contrastLo;
302 uniforms.contrastHi = contrastHi;
303 uniforms.minVelocity = 1.0e-6f;
304
305 encoder->setFragmentBytes(&uniforms, sizeof(LICUniforms), 5);
306
307 encoder->drawPrimitives(MTL::PrimitiveTypeTriangle,
308 static_cast<NS::UInteger>(0), static_cast<NS::UInteger>(3));
309 _device->recordDrawCall();
310 }
311}
std::shared_ptr< VertexFormat > vertexFormat() const
Shared vertex format (full-screen triangle, 14 floats per vertex).
std::shared_ptr< VertexBuffer > vertexBuffer() const
Shared vertex buffer (3-vertex full-screen triangle).
void execute(MTL::RenderCommandEncoder *encoder, Texture *velocityTexture, Texture *noiseTexture, int integrationSteps, float stepSize, float animationPhase, float contrastLo, float contrastHi, MetalRenderPipeline *pipeline, const std::shared_ptr< RenderTarget > &renderTarget, const std::vector< std::shared_ptr< MetalBindGroupFormat > > &bindGroupFormats, MTL::SamplerState *defaultSampler, MTL::DepthStencilState *defaultDepthStencilState)
MetalLICPass(MetalGraphicsDevice *device, MetalComposePass *composePass)
MTL::RenderPipelineState * get(const Primitive &primitive, const std::shared_ptr< VertexFormat > &vertexFormat0, const std::shared_ptr< VertexFormat > &vertexFormat1, int ibFormat, const std::shared_ptr< Shader > &shader, const std::shared_ptr< RenderTarget > &renderTarget, const std::vector< std::shared_ptr< MetalBindGroupFormat > > &bindGroupFormats, const std::shared_ptr< BlendState > &blendState, const std::shared_ptr< DepthState > &depthState, CullMode cullMode, bool stencilEnabled, const std::shared_ptr< StencilParameters > &stencilFront, const std::shared_ptr< StencilParameters > &stencilBack, const std::shared_ptr< VertexFormat > &instancingFormat=nullptr)
GPU texture resource supporting 2D, cubemap, volume, and array formats with mipmap management.
Definition texture.h:57
uint32_t width() const
Definition texture.h:63
uint32_t height() const
Definition texture.h:65
gpu::HardwareTexture * impl() const
Definition texture.h:101
std::shared_ptr< Shader > createShader(GraphicsDevice *graphicsDevice, const ShaderDefinition &definition, const std::string &sourceCode)
Definition shader.cpp:39
@ PRIMITIVE_TRIANGLES
Definition mesh.h:23
Describes how vertex and index data should be interpreted for a draw call.
Definition mesh.h:33
PrimitiveType type
Definition mesh.h:34