本文主要通过自己对 Metal By Example 理解编写,这一篇文章讲解 Metal 中实现天空盒子,还有在模型上的反射和折射。

Metal By Example Cover

天空盒子

天空盒子(Skybox)就是一个在立方体内部装饰了纹理的场景,在立方体中心设置一个相机来看到的视角。

这里的立方体纹理不同于之前提到过 2D 纹理,立方体纹理在 Metal 是左手系坐标:

Metal By Example Coordinate

立方体纹理由 6 张图片组成,其中负 y (ny or negative y) 在最中心:

Metal By Example Unwrapped Cube Map

MTLTextureDescriptor *textureDescriptor = [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm
                                                                                                size:cubeSize
                                                                                           mipmapped:NO];

id<MTLTexture> texture = [device newTextureWithDescriptor:textureDescriptor];

Slice 和 Cube Texture 对应关系:

Face number Cube texture face
0 Positive X
1 Negative X
2 Positive Y
3 Negative Y
4 Positive Z
5 Negative Z
for (size_t slice = 0; slice < 6; ++slice) {
  NSString *imageName = imageNameArray[slice];
  UIImage *image = [UIImage imageNamed:imageName];
  uint8_t *imageData = [self dataForImage:image];
  
  [texture replaceRegion:region
             mipmapLevel:0
                   slice:slice
               withBytes:imageData
             bytesPerRow:bytesPerRow
           bytesPerImage:bytesPerImage];
  free(imageData);
}

天空盒子的 Vertex Shader,纹理坐标就是立方体的物体空间:

vertex ProjectedVertex vertex_skybox(Vertex inVertex             [[stage_in]],
                                     constant Uniforms &uniforms [[buffer(1)]],
                                     uint vid                    [[vertex_id]])
{
    float4 position = inVertex.position;
    
    ProjectedVertex outVert;
    outVert.position = uniforms.modelViewProjectionMatrix * position;
    outVert.texCoords = position;
    return outVert;
}

Fragment Shader 并没有什么特别的,像以前一样将纹理和纹理坐标进行 Sample,只是将 z 坐标取反来适配 Sampler:

fragment half4 fragment_cube_lookup(ProjectedVertex vert          [[stage_in]],
                                    constant Uniforms &uniforms   [[buffer(0)]],
                                    texturecube<half> cubeTexture [[texture(0)]],
                                    sampler cubeSampler           [[sampler(0)]])
{
    float3 texCoords = float3(vert.texCoords.x, vert.texCoords.y, -vert.texCoords.z);
    return cubeTexture.sample(cubeSampler, texCoords);
}

反射

光在两种物质分界面上改变传播方向又返回原来物质中的现象,叫做光的反射。

Metal 中实现,可以逆向的来想,有一条射线从摄像机发射到物体表面,反射后到立方体纹理上的交汇处。

Metal 中使用内置函数 reflect 来实现此功能,注意纹理坐标基于世界坐标来计算:

Metal By Example Reflection

vertex ProjectedVertex vertex_reflect(Vertex inVertex             [[stage_in]],
                                      constant Uniforms &uniforms [[buffer(1)]],
                                      uint vid                    [[vertex_id]])
{
    float4 modelPosition = inVertex.position;
    float4 modelNormal = inVertex.normal;
    
    float4 worldCameraPosition = uniforms.worldCameraPosition;
    float4 worldPosition = uniforms.modelMatrix * modelPosition;
    float4 worldNormal = normalize(uniforms.normalMatrix * modelNormal);
    float4 worldEyeDirection = normalize(worldPosition - worldCameraPosition);
    
    ProjectedVertex outVert;
    outVert.position = uniforms.modelViewProjectionMatrix * modelPosition;
    outVert.texCoords = reflect(worldEyeDirection, worldNormal);
    
    return outVert;
}

折射

光从一种透明介质斜射入另一种透明介质时,传播方向一般会发生变化,这种现象叫光的折射,光在发生折射时入射角与折射角符合斯涅尔定律(Snell’sLaw),入射角 θI 与折射角 θT 的正弦之比叫做介质的绝对折射率,简称折射率(Index of Refraction)。

Metal 中使用内置函数 refract 来实现此功能,采用从空气进入玻璃的折射率来计算:

Metal By Example Refraction

// some common indices of refraction
constant float kEtaAir = 1.000277;
//constant float kEtaWater = 1.333;
constant float kEtaGlass = 1.5;

constant float kEtaRatio = kEtaAir / kEtaGlass;


vertex ProjectedVertex vertex_refract(Vertex inVertex             [[stage_in]],
                                      constant Uniforms &uniforms [[buffer(1)]],
                                      uint vid                    [[vertex_id]])
{
    float4 modelPosition = inVertex.position;
    float4 modelNormal = inVertex.normal;

    float4 worldCameraPosition = uniforms.worldCameraPosition;
    float4 worldPosition = uniforms.modelMatrix * modelPosition;
    float4 worldNormal = normalize(uniforms.normalMatrix * modelNormal);
    float4 worldEyeDirection = normalize(worldPosition - worldCameraPosition);

    ProjectedVertex outVert;
    outVert.position = uniforms.modelViewProjectionMatrix * modelPosition;
    outVert.texCoords = refract(worldEyeDirection, worldNormal, kEtaRatio);

    return outVert;
}

Core Motion

利用 Core Motion 来获取设备的方向信息来转换场景的位置:

self.motionManager = [[CMMotionManager alloc] init];
if (self.motionManager.deviceMotionAvailable)
{
  self.motionManager.deviceMotionUpdateInterval = 1 / 60.0;
  CMAttitudeReferenceFrame frame = CMAttitudeReferenceFrameXTrueNorthZVertical;
  [self.motionManager startDeviceMotionUpdatesUsingReferenceFrame:frame];
}
CMDeviceMotion *motion = self.motionManager.deviceMotion;
CMRotationMatrix m = motion.attitude.rotationMatrix;

// permute rotation matrix from Core Motion to get scene orientation
vector_float4 X = { m.m12, m.m22, m.m32, 0 };
vector_float4 Y = { m.m13, m.m23, m.m33, 0 };
vector_float4 Z = { m.m11, m.m21, m.m31, 0 };
vector_float4 W = {     0,     0,     0, 1 };

matrix_float4x4 orientation = { X, Y, Z, W };
self.renderer.sceneOrientation = orientation;

代码和效果

danjiang / mbe-sample-code / CubeMapping