为啥金属着色器渐变作为 SCNProgram 应用于 SceneKit 节点比作为 MTKView 更轻?

Posted

技术标签:

【中文标题】为啥金属着色器渐变作为 SCNProgram 应用于 SceneKit 节点比作为 MTKView 更轻?【英文标题】:why is metal shader gradient lighter as a SCNProgram applied to a SceneKit Node than it is as a MTKView?为什么金属着色器渐变作为 SCNProgram 应用于 SceneKit 节点比作为 MTKView 更轻? 【发布时间】:2017-10-17 10:23:13 【问题描述】:

我有一个渐变,由金属片段着色器生成,我已将其应用于由平面几何定义的 SCNNode。

看起来像这样:

当我使用应用于 Xcode 操场中渲染的 MTKView 的相同着色器时,颜色更深。是什么导致 Scenekit 版本中的颜色变浅?

这里是 Metal 着色器和 GameViewController。

着色器:

#include <metal_stdlib>
using namespace metal;
#include <SceneKit/scn_metal>

struct myPlaneNodeBuffer 
    float4x4 modelTransform;
    float4x4 modelViewTransform;
    float4x4 normalTransform;
    float4x4 modelViewProjectionTransform;
    float2x3 boundingBox;
;

typedef struct 
    float3 position [[ attribute(SCNVertexSemanticPosition) ]];
    float2 texCoords [[ attribute(SCNVertexSemanticTexcoord0) ]];
 VertexInput;

struct SimpleVertexWithUV

    float4 position [[position]];
    float2 uv;
;


vertex SimpleVertexWithUV gradientVertex(VertexInput in [[ stage_in ]],
                         constant SCNSceneBuffer& scn_frame [[buffer(0)]],
                         constant myPlaneNodeBuffer& scn_node [[buffer(1)]])

    SimpleVertexWithUV vert;

    vert.position = scn_node.modelViewProjectionTransform * float4(in.position, 1.0);

    int width = abs(scn_node.boundingBox[0].x) + abs(scn_node.boundingBox[1].x);
    int height = abs(scn_node.boundingBox[0].y) + abs(scn_node.boundingBox[1].y);

    float2 resolution = float2(width,height);

    vert.uv = vert.position.xy * 0.5 / resolution;

    vert.uv = 0.5 - vert.uv;

    return vert;


fragment float4 gradientFragment(SimpleVertexWithUV in [[stage_in]],
                           constant myPlaneNodeBuffer& scn_node [[buffer(1)]])

    float4 fragColor;
    float3 color = mix(float3(1.0, 0.6, 0.1), float3(0.5, 0.8, 1.0), sqrt(1-in.uv.y));
    fragColor = float4(color,1);
    return(fragColor);

游戏视图控制器:

import SceneKit
import QuartzCore


class GameViewController: NSViewController 

@IBOutlet weak var gameView: GameView!

override func awakeFromNib()
    super.awakeFromNib()

    // create a new scene
    let scene = SCNScene()

    // create and add a camera to the scene
    let cameraNode = SCNNode()
    cameraNode.camera = SCNCamera()
    scene.rootNode.addChildNode(cameraNode)

    // place the camera
    cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)

    // turn off default lighting
    self.gameView!.autoenablesDefaultLighting = false

    // set the scene to the view
    self.gameView!.scene = scene

    // allows the user to manipulate the camera
    self.gameView!.allowsCameraControl = true

    // show statistics such as fps and timing information
    self.gameView!.showsStatistics = true

    // configure the view
    self.gameView!.backgroundColor = NSColor.black

    var geometry:SCNGeometry

    geometry = SCNPlane(width:10, height:10)
    let geometryNode = SCNNode(geometry: geometry)

    let program = SCNProgram()
    program.fragmentFunctionName = "gradientFragment"
    program.vertexFunctionName = "gradientVertex"

    let gradientMaterial = SCNMaterial()
    gradientMaterial.program = program
    geometry.materials = [gradientMaterial]


    scene.rootNode.addChildNode(geometryNode)

    


【问题讨论】:

【参考方案1】:

正如 WWDC 2016 的 Advances in SceneKit Rendering 会议所述,SceneKit 现在默认在线性空间中进行渲染,这需要从照明方程中获得准确的结果。

您看到的不同之处在于,在 MetalKit 案例中,您在 sRGB 颜色空间中提供颜色分量(红色、绿色和蓝色值),而在 SceneKit 案例中,您在线性中提供完全相同的分量sRGB 色彩空间。

由您决定哪个结果是您想要的结果。要么你想要线性空间中的渐变(如果你正在插入一些数据,这就是你想要的)或伽玛空间(这是绘图应用程序使用的)。

如果您想要伽马空间中的渐变,则需要将颜色分量转换为线性,因为这是 SceneKit 的工作原理。从Metal Shading Language Specification 获取转换公式,这里有一个解决方案:

static float srgbToLinear(float c) 
    if (c <= 0.04045)
        return c / 12.92;
    else
        return powr((c + 0.055) / 1.055, 2.4);


fragment float4 gradientFragment(SimpleVertexWithUV in [[stage_in]],
                                 constant myPlaneNodeBuffer& scn_node [[buffer(1)]])

    float3 color = mix(float3(1.0, 0.6, 0.1), float3(0.5, 0.8, 1.0), sqrt(1 - in.uv.y));

    color.r = srgbToLinear(color.r);
    color.g = srgbToLinear(color.g);
    color.b = srgbToLinear(color.b);

    float4 fragColor = float4(color, 1);
    return(fragColor);

【讨论】:

谢谢!这正是我需要知道的。【参考方案2】:

在了解了这个问题的根本原因后,我对该主题进行了更多研究,并找到了另一种解决方案。伽玛空间渲染可以通过设置强制应用程序范围 在应用程序的 plist 中 SCNDisableLinearSpaceRendering 为 TRUE。

【讨论】:

【参考方案3】:

我不确定,但在我看来,您对节点大小的计算已关闭,导致您的 .uv 关闭,具体取决于节点的位置。

你有:

int width = abs(scn_node.boundingBox[0].x) + abs(scn_node.boundingBox[1].x);
int height = abs(scn_node.boundingBox[0].y) + abs(scn_node.boundingBox[1].y);

我认为应该是:

int width = abs(scn_node.boundingBox[0].x - scn_node.boundingBox[1].x);
int height = abs(scn_node.boundingBox[0].y - scn_node.boundingBox[1].y);

您想要两个极端之间的绝对差,而不是总和。随着节点向右和向下移动,总和会变大​​,因为它实际上包含了位置。

说了这么多,in.texCoords 中不是已经为您提供了所需的 (u, v) 吗?

【讨论】:

是的,你在这两个方面都是对的。我已经改用 in.texCoords 作为正确的 uv

以上是关于为啥金属着色器渐变作为 SCNProgram 应用于 SceneKit 节点比作为 MTKView 更轻?的主要内容,如果未能解决你的问题,请参考以下文章

使用金属顶点和片段着色器将 MTLTexture 传递给 SCNProgram

SCNProgram - 如何将“mat4”类型的制服传递给自定义着色器?

渐变着色器 Z 仅在场景视图中战斗

ARKit,用于 ARSCNView 的金属着色器

ARKit,用于ARSCNView的金属着色器

为什么iOS 8上的SceneKit无法编译我的自定义着色器?