You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 20:40:47