几何入口点的SceneKit着色器修改器适用于iOS但不适用于OS X

时间:2016-06-11 10:15:29

标签: ios macos shader scenekit heightmap

我正处于制作SceneKit着色器修改器(用于几何入口点)的早期阶段,该修改器根据高度贴图纹理替换平面的几何体。计划是用它来创建地形。

在iOS(编辑:iOS模拟器)中,着色器可以正常工作,但会将此警告打印到控制台:

  

SceneKit:错误,没有代码的修饰符无效

How it looks on iOS

然而,当为OS X构建时,着色器会出现致命错误,而地形几何体只会显示为粉红色矩形。

这是几何着色器修改器:

uniform sampler2D displacementMap;
const float intensity = 7.5;

# pragma body

vec4 displace = texture2D(displacementMap, _geometry.texcoords[0]);

_geometry.position.z += displace.r * intensity;

这就是着色器连接到几何体的方式。 terrainScene是高度贴图,它放置在漫反射内容中,以及着色器修改器中的自定义displacementMap取样器值:

    //displacement shader

    let mat = SCNMaterial()
    mat.diffuse.contents = terrainScene

    var displacementShader: String?
    guard let path = NSBundle.mainBundle().pathForResource("DisplaceGeometry", ofType: "shader") else {return}

    do {
        displacementShader = try String(contentsOfFile: path,  encoding: NSUTF8StringEncoding)
    } catch {
        return
    }
    mat.shaderModifiers = [SCNShaderModifierEntryPointGeometry: displacementShader!]
    let noiseProperty = SCNMaterialProperty(contents: terrainScene!)
    mat.setValue(noiseProperty, forKey: "displacementMap")

    //geometry
    let plane = SCNPlane(width: 80, height: 80)
    plane.widthSegmentCount = 40
    plane.heightSegmentCount = 40
    plane.materials = [mat]

这是OS X错误消息的开头:

SceneKit: error, modifier without code is invalid
2016-06-11 10:58:32.054 EndlessTerrainOSX[16923:5292387] SceneKit: error, Invalid shader modifier : no code provided
2016-06-11 10:58:32.640 EndlessTerrainOSX[16923:5292387] FATAL ERROR : failed loading compiling shader:

错误结束:

UserInfo={NSLocalizedDescription=Compilation failed: 
<program source>:343:8: error: global variables must have a constant address space qualifier
float4 displace = displacementMap.sample(displacementMapSampler, _geometry.texcoords[0]);
       ^
<program source>:343:19: error: use of undeclared identifier 'displacementMap'
float4 displace = displacementMap.sample(displacementMapSampler, _geometry.texcoords[0]);
                  ^
<program source>:343:42: error: use of undeclared identifier 'displacementMapSampler'
float4 displace = displacementMap.sample(displacementMapSampler, _geometry.texcoords[0]);
                                         ^
<program source>:344:1: error: unknown type name '_geometry'
_geometry.position.z += displace.r * intensity;
^
<program source>:344:10: error: cannot use dot operator on a type
_geometry.position.z += displace.r * intensity;
         ^
}
2016-06-11 10:58:32.646 EndlessTerrainOSX[16923:5292387] Shaders without a vertex function are not allowed

有谁知道它在iOS上显示的原因,但在OS X上失败了?

2 个答案:

答案 0 :(得分:3)

我建议它可能不是iOS vs OSX问题,但与使用OpenGL和其他Metal的设备有关。

错误消息中的代码是Metal,表示SceneKit已将您的代码从GLSL转换为Metal。根据我的经验,这在100%的时间都不起作用。在您的情况下,最简单的解决方案可能是强制SCNView使用passing in options when it's created的OpenGL。我相信Interface Builder也有一个下拉列表。

如果您希望继续使用Metal,并且有理由,请继续阅读。

当着色器修改器无法编译时,着色器的全部内容被转储到stdout,这确实有助于调试过程。如果向上滚动到第343行,您会发现SceneKit在顶点着色器之前包含了float4 displace = displacementMap.sample(...);方法。在函数外定义意味着它是一个全局变量,因此需要constant地址空间修饰符,但这根本不是你想要的......在这种情况下,从GLSL到Metal的转换没有按照你的预期工作。

我注意到你错过了#pragma arguments指令,这应该包含在统一定义之上(参考"Writing a Shader Modifier Snippet"),它可能有所帮助。根据我的经验,我认为你很难将纹理传递到基于金属的着色器修改器。我尝试过,失败了,写了一个SCNProgram代替它。建议强制OpenGL。

答案 1 :(得分:2)

我已经弄清楚为什么它不适用于Metal

为什么,正如@lock非常有用地指出的那样,行float4 displace = displacementMap.sample(...)被放置在顶点着色器之外的全局空间中,即使它位于行# pragma body之下。

这是因为有额外的空间。

它应该是#pragma body(哈希后没有空格)。

如果我删除了那个空格,那么它会在金属和GLES中无错误地编译。自定义纹理也有效(我没有提到的一点是,纹理实际上是一个SpriteKit场景,因为它需要动态更改)。

我无法相信我花了大约24小时盯着应该运行的代码,因为我写了# pragma而不是#pragma。感谢@lock发现身体的线条实际上是在全球范围内声明的。

编辑:

由于这个问题的解决方案围绕着SceneKit将我的GLES着色器片段转换为Metal的能力,以下是我注意到的其他一些事情:

  • 将自定义纹理传递到着色器片段可以很好地转换为Metal。我使用的Swift代码:

    let terrainGenomes = SCNMaterialProperty(contents: "art.scnassets/TerrainGenomes.jpg")
    mat.setValue(terrainGenomes, forKey: "terrainGenomes")
    

(其中mat是SCNMaterial)

  • 使用#pragma arguments会使GLES着色器失败。这似乎是可选的,所以我把它删除了。

  • 我无法让Metal翻译识别自定义全局函数

当我尝试执行此操作时,金属翻译始终以no previous prototype for function失败,即使SCNShadable class reference在其示例代码段中具有自定义全局函数。因此,为了保持兼容性,我必须“解开”代码段中的功能。例如,这里是一个表面着色器修改器,用于计算法线(点法线看起来不太好,所以我切换到像素法线,在表面着色器修改器中)。这适用于GLES但无法转换为Metal:

uniform sampler2D displacementMap;
uniform float intensity;

vec3 getNewPos(vec2 tc) {
  vec4 displace = texture2D(displacementMap, tc);
  return vec3((tc.x - 0.5) * 80., (tc.y - 0.5) * -80., displace.r * intensity);
}

#pragma body

vec3 newPos = getNewPos(_surface.diffuseTexcoord);
vec3 neighbour1 = getNewPos(_surface.diffuseTexcoord + vec2(0.015, 0.));
vec3 neighbour2 = getNewPos(_surface.diffuseTexcoord + vec2(0., 0.015));

vec4 norm = u_modelViewTransform * vec4(normalize(cross(neighbour2 - newPos, neighbour1 - newPos)), 0.);
_surface.normal = norm.xyz;

在金属翻译中,这失败了:

<program source>:344:8: warning: no previous prototype for function 'getNewPos'
float3 getNewPos(float2 tc) {
       ^

这是解开的版本。删除该函数使得代码干得更少,而且不那么漂亮,但它在GLES和Metal中都有效:

uniform sampler2D displacementMap;

uniform float intensity;

#pragma body

vec4 pixel = texture2D(displacementMap, _surface.diffuseTexcoord);
vec3 newPos = vec3((_surface.diffuseTexcoord.x - 0.5) * 80., (_surface.diffuseTexcoord.y - 0.5) * -80., pixel.r * intensity);
vec2 tc1 = _surface.diffuseTexcoord + vec2(0.015, 0.);
vec2 tc2 = _surface.diffuseTexcoord + vec2(0., 0.015);
vec3 neighbour1 = vec3((tc1.x - 0.5) * 80., (tc1.y - 0.5) * -80., texture2D(displacementMap, tc1).r * intensity);
vec3 neighbour2 = vec3((tc2.x - 0.5) * 80., (tc2.y - 0.5) * -80., texture2D(displacementMap, tc2).r * intensity);

vec4 norm = u_modelViewTransform * vec4(normalize(cross(neighbour2 - newPos, neighbour1 - newPos)), 0.);
_surface.normal = norm.xyz;
  • 在Shader片段的金属翻译中无法访问SceneKit提供的某些制服,例如u_diffuseTexture

我想最终,将GLES着色器修改器自动转换为Metal只会让你到目前为止,并且为了使用自定义着色器修改器正确地进行多目标开发,你应该另外提供一个完整的金属着色器片段到一个GLES(并使用预编译器#if块在它们之间切换?)。我很了解GLSL,我还没有学习Metal SL。