问题
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