Metal入门,使用Metal实现纹理效果
kernel void Metal_compute(texture2d<float, access::write> output [[texture(0)]],constant float &timer [[buffer(0)]],texture2d<float, access::sample> 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> output [[texture(0)]]
- 这是一个可写的2D纹理,用于存储计算结果
float
表示纹理像素使用浮点数格式access::write
表示这个纹理是只写的[[texture(0)]]
是Metal的属性限定符,表示这个纹理绑定到纹理槽0
-
constant float &timer [[buffer(0)]]
- 一个常量浮点数引用,用于接收时间值
[[buffer(0)]]
表示这个值从索引为0的缓冲区中读取- 在这个着色器中用于创建动画效果
-
texture2d<float, access::sample> inputTexture [[texture(1)]]
- 一个可采样的2D输入纹理
access::sample
表示这个纹理是只读并且可以采样的[[texture(1)]]
表示绑定到纹理槽1- 作为输入图像来源
-
uint2 gid [[thread_position_in_grid]]
- 一个2D无符号整数向量,表示当前线程在计算网格中的位置
[[thread_position_in_grid]]
是Metal的内置属性限定符- 相当于当前像素的(x,y)坐标
- 用于确定要处理的像素位置
Metal采样器(Sampler)详解
Metal中的采样器(Sampler)是控制从纹理中读取像素(texel)的方式的对象。在着色器代码中,你可以看到这样的采样器定义:
constexpr sampler textureSampler(mag_filter::linear, min_filter::linear, address::repeat);
采样器的主要属性
- 过滤模式(Filter Modes)
-
mag_filter
: 放大过滤,当纹理需要放大显示时使用linear
: 线性过滤,会对临近像素进行插值,使图像更平滑nearest
: 最近点过滤,使用最接近的像素,保持像素化效果
-
min_filter
: 缩小过滤,当纹理需要缩小显示时使用- 同样有
linear
和nearest
选项
- 同样有
- 寻址模式(Address Modes)
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 UIKitclass MetalKernelView : MTKView
{private var commandQueue: MTLCommandQueue!private var computePipelineState: MTLComputePipelineState!var timer:Float = 0var 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 = .bgra8Unormself.isPaused = falseself.enableSetNeedsDisplay = falseself.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>.sizetimerBuffer = device.makeBuffer(bytes: &timer, length: bufferSize, options: .storageModeShared)}private func initializeClickBuffer() {guard let device = device else { return }// 创建包含点击位置的缓冲区let bufferSize = MemoryLayout<SIMD2<Float>>.sizeclickBuffer = 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.threadExecutionWidthlet h = computePipelineState.maxTotalThreadsPerThreadgroup / wlet 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)")}}
}