如何基于顶点数据为自定义SCNGeometry定义材质,使其渲染效果与标准SCNNode一致?
Let's walk through each of your problems step by step, with explanations and fixes tailored to your Playground code:
1. Why were custom point primitives black initially?
The root cause here is missing normal data for your custom geometry.
When using the .physicallyBased lighting model, SceneKit relies entirely on surface normals to calculate how light interacts with geometry. Built-in primitives like SCNSphere automatically generate normals under the hood, so they render correctly. But your custom point geometry didn't include a normal source—without normals, SceneKit can't determine the orientation of each point relative to the light, so it defaults to rendering black (no light contribution at all).
2. Why do points show color but no shadows after adding normals?
There are two key factors at play here:
- Point primitives don't support shadows: SceneKit's
.pointprimitive type renders as screen-space sprites, not true 3D geometry. They will never cast shadows onto other objects, nor will they receive shadows from other lights/objects. If you need shadow support, replace your point primitives with tinySCNSpherenodes instead. - Uniform normals eliminate directional lighting: You used the same
SCNVector3(1,1,1)normal for all points. This means every point's surface is calculated to face the exact same direction, so the directional light hits them uniformly—no variation in brightness (which creates the appearance of "no shadow"). To fix this, assign normals that face the light source, or calculate proper face normals if you were using a non-point primitive.
If you switch to 3D primitives later, don't forget to enable shadows on your directional light:
directionalLightNode.light?.castsShadow = true directionalLightNode.light?.shadowRadius = 2.0 // Soften shadow edges for realism
3. Why does the custom point's black appear lighter than the SCNSphere's black?
This discrepancy comes down to three key differences in how SceneKit renders the two:
- Vertex color vs. material color: Your custom geometry uses vertex colors directly, while the
SCNSphereuses a material's diffuse color. In PBR, vertex colors multiply with the material's diffuse color. If your custom material's diffuse is the default white, the vertex color(0.07, 0.11, 0.12)is rendered as-is. For theSCNSphere, ensure you're setting the material's diffuse to the exact same RGB value, and match other PBR properties likeroughnessandmetallic(defaults for built-in primitives might differ from your custom material). - Lighting on curved vs. flat surfaces: A sphere has curved normals, so parts of it are angled away from the light, creating darker areas that make the overall black look deeper. Points are flat sprites with uniform normals, so they're evenly lit by the directional light and ambient light, making the same RGB value appear lighter.
- Color space differences:
UIColoruses sRGB color space by default, but vertex colors in SceneKit are treated as linear color space. To match the two, convert your vertex color values from sRGB to linear:// Helper to convert sRGB values to linear color space func srgbToLinear(_ value: Float) -> Float { return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4) } // Use this when defining your Vertex r/g/b values
Quick Fix Summary for Your Code
- Always include a normal source when using PBR with custom geometry.
- Replace point primitives with tiny spheres if you need shadow support.
- Match PBR material properties (roughness, metallic) between custom geometry and built-in primitives, and convert vertex colors to linear space if needed.
内容的提问来源于stack exchange,提问作者tospig

