kernel void Metal_compute(texture2d output [[texture(0)]],
constant float &timer [[buffer(0)]],
texture2d inputTexture [[texture(1)]],
uint2 gid [[thread_position_in_grid]])
{
// 获取输出纹理的尺寸
int width = output.get_width();
int height = output.get_height();
// 计算标准化的UV坐标
float2 uv = float2(gid) / float2(width, height);
// 创建采样器
constexpr sampler textureSampler(mag_filter::linear,
min_filter::linear,
address::repeat);
// 关键修改:调整UV坐标以覆盖整个屏幕
// Metal纹理坐标原点在左上角,Y轴向下,而标准UV坐标原点在左下角,Y轴向上
float2 texCoord = float2(uv.x, uv.y);
// 考虑纹理和屏幕的纵横比差异
float textureAspect = float(inputTexture.get_width()) / float(inputTexture.get_height());
float screenAspect = float(width) / float(height);
float2 centered = uv - 0.5;
// 如果宽度大于高度,调整x坐标
if (screenAspect > 1.0) {
centered.x *= screenAspect;
}
// 如果高度大于宽度,调整y坐标
else {
centered.y /= screenAspect;
}
float radius = 0.3; // 稍微缩小半径确保在所有设备上都可见
float distance = length(centered);
// 调整纹理坐标以保持纵横比
if (textureAspect > screenAspect) {
// 纹理比屏幕更宽,需要裁剪宽度
float scaledWidth = screenAspect / textureAspect;
texCoord.x = texCoord.x * scaledWidth + (1.0 - scaledWidth) * 0.5;
} else {
// 纹理比屏幕更高,需要裁剪高度
float scaledHeight = textureAspect / screenAspect;
texCoord.y = texCoord.y * scaledHeight + (1.0 - scaledHeight) * 0.5;
}
// 实现纹理沿x轴平移的效果
// 使用timer控制移动速度,调整0.2可以控制移动快慢
texCoord.x = fract(texCoord.x + timer * 0.02);
// 采样纹理
float4 texColor = inputTexture.sample(textureSampler, texCoord);
// 星球效果
if (distance <= radius) {
// 计算法线用于光照
float z = sqrt(radius * radius - centered.x * centered.x - centered.y * centered.y);
float3 normal = normalize(float3(centered.x, centered.y, z));
// 创建一个随时间变化的光源方向
float3 lightDir = normalize(float3(cos(timer), sin(timer), 0.5));
// 基本漫反射光照
float diffuse = max(0.0, dot(normal, lightDir));
// 将纹理颜色与光照结合
float3 finalColor = texColor.rgb * (diffuse * 0.7 + 0.3);
output.write(float4(finalColor, texColor.a), gid);
} else {
// 星空背景
output.write(float4(0), gid);
}
}
texture2d
float
表示纹理像素使用浮点数格式access::write
表示这个纹理是只写的[[texture(0)]]
是Metal的属性限定符,表示这个纹理绑定到纹理槽0constant float &timer [[buffer(0)]]
[[buffer(0)]]
表示这个值从索引为0的缓冲区中读取texture2d
access::sample
表示这个纹理是只读并且可以采样的[[texture(1)]]
表示绑定到纹理槽1uint2 gid [[thread_position_in_grid]]
[[thread_position_in_grid]]
是Metal的内置属性限定符Metal中的采样器(Sampler)是控制从纹理中读取像素(texel)的方式的对象。在着色器代码中,你可以看到这样的采样器定义:
constexpr sampler textureSampler(mag_filter::linear,
min_filter::linear,
address::repeat);
mag_filter
: 放大过滤,当纹理需要放大显示时使用
linear
: 线性过滤,会对临近像素进行插值,使图像更平滑nearest
: 最近点过滤,使用最接近的像素,保持像素化效果min_filter
: 缩小过滤,当纹理需要缩小显示时使用
linear
和nearest
选项address::repeat
: 重复模式,当UV坐标超出[0,1]范围时,纹理会重复address::clamp_to_edge
: 边缘延伸,超出范围的UV会使用边缘像素值address::mirrored_repeat
: 镜像重复address::clamp_to_zero
: 超出范围的UV会返回透明黑色mip_filter
: 多级渐进纹理过滤max_anisotropy
: 各向异性过滤程度,提高倾斜视角的质量在着色器中使用采样器采样纹理的方式:
float4 texColor = inputTexture.sample(textureSampler, texCoord);
这行代码中:
inputTexture
: 输入的纹理textureSampler
: 定义的采样器texCoord
: 采样的UV坐标(通常在[0,1]范围内)texColor
是采样得到的颜色值address::repeat
)在代码中可以看到:
texCoord.x = fract(texCoord.x + timer * 0.02);
这使纹理沿x轴平移,当坐标超出[0,1]范围时,因为使用了repeat
模式,纹理会循环重复,产生连续滚动效果。
linear
)使纹理在放大和缩小时平滑过渡,避免像素化。在行星效果中特别重要,确保表面纹理光滑过渡。
光照效果之前已经提到过了,不再过多进行赘述
然后想实现星球自转的效果,我们可以采用让星球背景进行平移,这样就利用相对性实现了星球的自转
//
// MetalKernelView.swift
// MetalDemo
//
// Created by ricard.li on 2025/5/20.
//
import MetalKit
import UIKit
class MetalKernelView : MTKView
{
private var commandQueue: MTLCommandQueue!
private var computePipelineState: MTLComputePipelineState!
var timer:Float = 0
var timerBuffer : MTLBuffer!
// 触摸位置
var clickPosition: SIMD2<Float> = SIMD2<Float>(0, 0)
var clickBuffer: MTLBuffer!
var texture : MTLTexture!
override init(frame: CGRect, device: MTLDevice?) {
super.init(frame: frame, device: device)
self.device = device ?? MTLCreateSystemDefaultDevice()
configure()
// 添加触摸手势识别器
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
self.addGestureRecognizer(tapGesture)
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
// 设置刷新率和渲染控制
self.colorPixelFormat = .bgra8Unorm
self.isPaused = false
self.enableSetNeedsDisplay = false
self.preferredFramesPerSecond = 60
// 重要:设置为false,允许计算着色器访问纹理
self.framebufferOnly = false
// 创建命令队列
commandQueue = device?.makeCommandQueue()
// 创建计算管线状态
guard let library = device?.makeDefaultLibrary(),
let kernelFunc = library.makeFunction(name: "Metal_compute") else {
fatalError("无法加载计算内核函数")
}
do {
computePipelineState = try device?.makeComputePipelineState(function: kernelFunc)
} catch {
fatalError("无法创建计算管线状态: \(error)")
}
// 初始化timer缓冲区
initializeTimerBuffer()
// 初始化点击位置缓冲区
initializeClickBuffer()
// 设置纹理
setUpTexture()
print("Metal初始化完成")
}
private func initializeTimerBuffer() {
guard let device = device else { return }
// 创建一个包含timer值的缓冲区
let bufferSize = MemoryLayout<Float>.size
timerBuffer = device.makeBuffer(bytes: &timer, length: bufferSize, options: .storageModeShared)
}
private func initializeClickBuffer() {
guard let device = device else { return }
// 创建包含点击位置的缓冲区
let bufferSize = MemoryLayout<SIMD2<Float>>.size
clickBuffer = device.makeBuffer(bytes: &clickPosition, length: bufferSize, options: .storageModeShared)
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
let location = gestureRecognizer.location(in: self)
// 将点击位置归一化到 0-1 范围
clickPosition.x = Float(location.x / bounds.width)
clickPosition.y = Float(1.0 - location.y / bounds.height) // 翻转Y轴,Metal的坐标系从左下角开始
// 更新缓冲区中的值
if let bufferContents = clickBuffer?.contents() {
memcpy(bufferContents, &clickPosition, MemoryLayout<SIMD2<Float>>.size)
}
// 强制重绘
setNeedsDisplay()
}
func update() {
// 增加计时器值
timer += 1.0 / Float(preferredFramesPerSecond)
// 更新缓冲区中的值
if let bufferContents = timerBuffer?.contents() {
memcpy(bufferContents, &timer, MemoryLayout<Float>.size)
}
// // 触发重绘
// draw()
}
override func draw(_ rect: CGRect) {
// 更新timer值
update()
guard let commandBuffer = commandQueue.makeCommandBuffer(),
let drawable = currentDrawable else {
return
}
// 创建计算命令编码器
guard let computeEncoder = commandBuffer.makeComputeCommandEncoder() else {
return
}
// 设置计算管线状态
computeEncoder.setComputePipelineState(computePipelineState)
// 设置输出纹理
computeEncoder.setTexture(drawable.texture, index: 0)
// 设置timer缓冲区
computeEncoder.setBuffer(timerBuffer, offset: 0, index: 0)
// 设置输入纹理
computeEncoder.setTexture(texture, index: 1)
// 计算线程组大小和网格大小
let w = computePipelineState.threadExecutionWidth
let h = computePipelineState.maxTotalThreadsPerThreadgroup / w
let threadsPerThreadgroup = MTLSize(width: w, height: h, depth: 1)
let threadsPerGrid = MTLSize(width: drawable.texture.width,
height: drawable.texture.height,
depth: 1)
// 调度计算内核
computeEncoder.dispatchThreads(threadsPerGrid,
threadsPerThreadgroup: threadsPerThreadgroup)
// 结束编码
computeEncoder.endEncoding()
// 呈现结果并提交命令
commandBuffer.present(drawable)
commandBuffer.commit()
}
private func setUpTexture() {
guard let device = device else { return }
// 创建纹理加载器
let textureLoader = MTKTextureLoader(device: device)
// 从Assets.xcassets加载纹理
do {
// 使用正确的方法从Assets.xcassets加载图片
let options: [MTKTextureLoader.Option: Any] = [
.textureUsage: MTLTextureUsage.shaderRead.rawValue,
.generateMipmaps: false
]
// 直接使用图片名称加载,无需扩展名
texture = try textureLoader.newTexture(name: "2k_jupiter",
scaleFactor: 1.0,
bundle: Bundle.main,
options: options)
print("从Assets成功加载纹理")
} catch {
print("从Assets加载纹理失败: \(error)")
}
}
}