SceneKit节点毛玻璃效果实现:Metal Shader纹理输入解惑
在SceneKit节点实现毛玻璃效果的问题
我想在SceneKit节点上实现一种毛玻璃效果,该效果会接收一张纹理作为输入,输出其模糊后的版本。我计划创建一个SCNProgram并将其应用到节点的材质上,但在用Metal着色器复现该效果时遇到了困惑——我不清楚该给片元函数传入什么纹理,而我的需求是这个输入纹理就是节点未使用该着色器时的原始纹理。
以下是我编写的Metal着色器代码:
#include <metal_stdlib> using namespace metal; #include <SceneKit/scn_metal> struct NodeBuffer { float4x4 modelTransform; float4x4 modelViewProjectionTransform; float4x4 modelViewTransform; float4x4 normalTransform; float2x3 boundingBox; }; struct MyNodeBuffer { float4x4 modelTransform; float4x4 inverseModelTransform; float4x4 modelViewTransform; float4x4 inverseModelViewTransform; float4x4 normalTransform; float4x4 modelViewProjectionTransform; float4x4 inverseModelViewProjectionTransform; }; struct VertexInput { float3 position [[attribute(SCNVertexSemanticPosition)]]; float2 uv [[attribute(SCNVertexSemanticTexcoord0)]]; }; struct VertexOut { float4 position [[position]]; float2 uv; }; vertex VertexOut vertexFunction(VertexInput in [[ stage_in ]], constant NodeBuffer& scn_node [[buffer(1)]]) { VertexOut out; out.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0); out.uv = in.uv; return out; }; float stepfun(float x) { return (sign(x) + 1.0) / 2.0; } float square(float2 pos) { return (stepfun(pos.x + 1.0) * stepfun(1.0 - pos.x)) * (stepfun(pos.y + 1.0) * stepfun(1.0 - pos.y)); }; constexpr sampler textureSampler(coord::normalized, filter::linear, address::repeat); float2 dist(float2 pos, texture2d<float,access::sample> texture [[ texture(0) ]]) { float2 offset = pos; return pos + square((offset - 0.5) * 3.0) * texture.sample(textureSampler, (offset - 0.5) * 5.0).xy * 0.05; } fragment float4 fragmentFunction(VertexOut out [[ stage_in ]], texture2d<float, access::sample> texture [[ texture(0) ]], device float3 *resolution [[ buffer(0) ]]) { float2 uv = out.uv / resolution[0].xy; float4 tex = texture.sample(textureSampler, dist(uv, texture)); tex.a = 0.5; return tex; }
解决方案
1. 修正着色器代码问题
原代码存在几个关键问题:
- UV计算错误:
out.uv已经是归一化坐标,不需要除以分辨率;分辨率参数应该传入纹理的实际尺寸而非float3*类型。 - 扰动逻辑与原效果偏差:调整采样逻辑,让扰动更贴近目标毛玻璃效果。
修正后的Metal着色器代码:
#include <metal_stdlib> using namespace metal; #include <SceneKit/scn_metal> struct NodeBuffer { float4x4 modelViewProjectionTransform [[attribute(SCNNodeModelViewProjectionTransform)]]; }; struct VertexInput { float3 position [[attribute(SCNVertexSemanticPosition)]]; float2 uv [[attribute(SCNVertexSemanticTexcoord0)]]; }; struct VertexOut { float4 position [[position]]; float2 uv; }; vertex VertexOut vertexFunction(VertexInput in [[stage_in]], constant NodeBuffer& scnNode [[buffer(1)]]) { VertexOut out; out.position = scnNode.modelViewProjectionTransform * float4(in.position, 1.0); out.uv = in.uv; return out; } float stepfun(float x) { return (sign(x) + 1.0) / 2.0; } float square(float2 pos) { return stepfun(pos.x + 1.0) * stepfun(1.0 - pos.x) * stepfun(pos.y + 1.0) * stepfun(1.0 - pos.y); } constexpr sampler texSampler(coord::normalized, filter::linear, address::repeat); // 模拟扰动模糊逻辑 float4 getDistortedSample(texture2d<float, access::sample> tex, float2 uv, float2 texSize) { float2 offset = uv - 0.5; float2 perturb = square(offset * 3.0) * tex.sample(texSampler, offset * 5.0).xy * 0.05; // 多次采样实现模糊效果 float4 color = float4(0.0); const int samples = 4; for (int i = 0; i < samples; i++) { float2 shift = perturb * (float(i)/float(samples) - 0.5); color += tex.sample(texSampler, uv + shift); } return color / float(samples); } fragment float4 fragmentFunction(VertexOut out [[stage_in]], texture2d<float, access::sample> originalTex [[texture(0)]], constant float2& texSize [[buffer(0)]]) { float4 blurredColor = getDistortedSample(originalTex, out.uv, texSize); blurredColor.a = 1.0; // 按需调整透明度 return blurredColor; }
2. 绑定原始纹理并设置SCNProgram
你需要传入的就是节点材质原本的diffuse纹理,通过SceneKit的属性绑定让着色器访问到它。以下是Swift代码示例:
import SceneKit // 1. 加载修正后的着色器代码 let vertexShader = """ // 上面修正后的顶点着色器代码 """ let fragmentShader = """ // 上面修正后的片元着色器代码 """ // 2. 创建SCNProgram let glassProgram = SCNProgram() glassProgram.vertexShader = vertexShader glassProgram.fragmentShader = fragmentShader // 3. 绑定纹理和缓冲区属性 // 将texture(0)对应材质的diffuse纹理 glassProgram.setSemantic(SCNGeometrySource.Semantic.diffuse.rawValue, for: .texture, options: [SCNProgram.SemanticOption.slot: 0]) // 绑定纹理尺寸缓冲区(slot 0) glassProgram.addUniform(withName: "texSize", type: .float2, count: 1) // 4. 配置节点材质 let material = SCNMaterial() material.program = glassProgram // 设置原始纹理(比如你原本要给节点用的纹理) guard let originalTexture = UIImage(named: "yourOriginalTexture.png") else { return } material.diffuse.contents = originalTexture // 将纹理尺寸传入缓冲区 let texSize = SIMD2<Float>(Float(originalTexture.size.width), Float(originalTexture.size.height)) material.setValue(texSize, forKey: "texSize") // 5. 将材质应用到节点 yourNode.geometry?.materials = [material]
3. 关键说明
- 原始纹理就是你节点原本要使用的diffuse纹理,通过
SCNProgram的语义绑定,着色器的texture(0)就能直接访问到它。 - 修正后的着色器通过多次带扰动的采样实现毛玻璃模糊,更贴近目标效果。
- 纹理尺寸用于辅助计算扰动幅度,确保不同分辨率的纹理都能得到一致的效果。
内容的提问来源于stack exchange,提问作者Bael
相关产品推荐
相关产品推荐

