why is metal shader gradient lighter as a SCNProgram applied to a SceneKit Node than it is as a MTKView?

喜欢而已 提交于 2020-01-10 02:04:10

问题


I have a gradient, generated by a Metal fragment shader that I've applied to a SCNNode defined by a plane geometry.

It looks like this:

When I use the same shader applied to a MTKView rendered in an Xcode playground, the colors are darker. What is causing the colors to be lighter in the Scenekit version?

Here is the Metal shader and the GameViewController.

Shader:

#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);
}

Game view controller:

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:


As explained in the Advances in SceneKit Rendering session from WWDC 2016, SceneKit now defaults to rendering in linear space which is required to have accurate results from lighting equations.

The difference you see comes from the fact that in the MetalKit case you are providing color components (red, green and blue values) in the sRGB color space, while in the SceneKit case you are providing the exact same components in the linear sRGB color space.

It's up to you to decide which result is the one you want. Either you want a gradient in linear space (that's what you want if you are interpolating some data) or in gamma space (that's what drawing apps use).

If you want a gradient in gamma space, you'll need to convert the color components to be linear because that's what SceneKit works with. Taking the conversion formulas from the Metal Shading Language Specification, here's a solution:

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:


After learning the root cause of this problem, I did a bit more research on the topic and found another solution. Gamma space rendering can be forced application wide by setting SCNDisableLinearSpaceRendering to TRUE in the application's plist.




回答3:


I'm not sure, but it looks to me like your calculation of the size of the node is off, leading your .uv to be off, depending on the position of the node.

You have:

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);

I would think that should be:

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);

You want the absolute difference between the two extremes, not the sum. The sum gets larger as the node moves right and down, because it effectively includes the position.

All of that said, isn't the desired (u, v) already provided to you in in.texCoords?



来源:https://stackoverflow.com/questions/44033605/why-is-metal-shader-gradient-lighter-as-a-scnprogram-applied-to-a-scenekit-node

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!