SceneKit shader modifier for geometry entry points works in iOS but not OS X

*爱你&永不变心* 提交于 2020-01-23 13:30:13

问题


I'm in the early stages of making a SceneKit shader modifier (for the geometry entry point) that displaces a plane's geometry according to a height-map texture. The plan is to use it for creating terrain.

In iOS (edit: the iOS Simulator) the shader works as it should, but does print this warning to the console:

SceneKit: error, modifier without code is invalid

When building for OS X however, the shader gets a fatal error, and the terrain geometry just displays as a pink rectangle.

This is the geometry shader modifier:

uniform sampler2D displacementMap;
const float intensity = 7.5;

# pragma body

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

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

And this is how the shader is connected to the geometry. terrainScene is the height map, it is placed in both the diffuse contents, and the custom displacementMap sampler value in the shader modifier:

    //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]

This is the beginning of the OS X error message:

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:

And the end of the error:

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

Does anyone know why it displays on iOS, but fails on OS X?


回答1:


I'd suggest it may not be a iOS vs OSX issue, but related to one device using OpenGL and the other Metal.

The code in the error message is Metal, indicating that SceneKit has translated your code from GLSL to Metal. In my experience this doesn't work 100% of the time. In your case the simplest solution may be to force the SCNView to use OpenGL by passing in options when it's created. I believe there's also a dropdown in Interface Builder for this too.

If you wish to keep using Metal, and there are reasons why you would, read on.

When a shader modifier fails to compile the entire contents of the shader is dumped to stdout, this really helps the debugging process. If you scroll up to line 343 you will find that SceneKit has included the float4 displace = displacementMap.sample(...); method before the vertex shader. Being defined outside a function means that it is a global variable, hence requiring the constant address space modifier, but this isn't what you wanted at all... In this instance the translation from GLSL to Metal hasn't worked as you expected.

I note you're missing the #pragma arguments directive, this should be included above the uniform definition (refer to "Writing a Shader Modifier Snippet"), it may help. In my experience I think you'll have a tough time passing a texture into a Metal based shader modifier. I tried, failed, and wrote a SCNProgram to do it instead. Suggest forcing OpenGL.




回答2:


I've worked out why it wasn't working in Metal

And why, as @lock very helpfully pointed out, the line float4 displace = displacementMap.sample(...) was being placed in the global space, outside of the vertex shader, even though it comes below the line # pragma body.

It's because of an additional space.

It should be #pragma body (no space after the hash).

If I delete that space, then it compiles without error in both metal and GLES. And the custom texture works too (one thing I didn't mention, is that the texture is actually a SpriteKit scene, because it needs to be changed dynamically).

I can't believe I've spent essentially 24 hours staring at this code that should work, all because I wrote # pragma instead of #pragma. Thanks to @lock for spotting that the lines from the body were actually being declared in the global scope.

EDIT:

As the solution to this question revolves around the ability of SceneKit to translate my GLES shader snippet into Metal, here are a few other things I've noticed:

  • Passing custom textures in to shader snippets translates fine to Metal. Swift code I used:

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

(where mat is the SCNMaterial)

  • Using #pragma arguments makes the GLES shader fail. This seems to be optional, so I just removed it.

  • I can't get the Metal translation to recognise a custom global function

The Metal translation always fails with a no previous prototype for function when I try to do this, even though SCNShadable class reference has custom global functions in its example snippet. So to maintain compatibility, I've had to "unwind" the functions in the snippet. For example, here is a surface shader modifier that calculates the normals (point normals weren't looking great, so I switched to pixel normals, in a surface shader modifier). This works in GLES but fails to translate to 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;

In the Metal translation, this fails with:

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

This is the unwound version. Removing the function makes the code a lot less DRY and a lot less pretty, but it work in both GLES and 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;
  • Some uniforms provided by SceneKit, such as u_diffuseTexture aren't accessible in the Metal translation of the shader snippet

I suppose that ultimately, the automatic translation of GLES shader modifiers to Metal is only going to get you so far, and that to properly do multi-target development with custom shader modifiers, you should provide a full Metal shader snippet in addition to a GLES one (and switch between them with precompiler #if blocks?). I know GLSL pretty well, I haven't got round to learning Metal SL yet.



来源:https://stackoverflow.com/questions/37762535/scenekit-shader-modifier-for-geometry-entry-points-works-in-ios-but-not-os-x

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