Metal入门,使用Metal实现纹理效果

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);
    }
}

传参解释:

  1. texture2d output [[texture(0)]]

    • 这是一个可写的2D纹理,用于存储计算结果
    • float表示纹理像素使用浮点数格式
    • access::write表示这个纹理是只写的
    • [[texture(0)]]是Metal的属性限定符,表示这个纹理绑定到纹理槽0
  2. constant float &timer [[buffer(0)]]

    • 一个常量浮点数引用,用于接收时间值
    • [[buffer(0)]]表示这个值从索引为0的缓冲区中读取
    • 在这个着色器中用于创建动画效果
  3. texture2d inputTexture [[texture(1)]]

    • 一个可采样的2D输入纹理
    • access::sample表示这个纹理是只读并且可以采样的
    • [[texture(1)]]表示绑定到纹理槽1
    • 作为输入图像来源
  4. 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);

采样器的主要属性

  1. 过滤模式(Filter Modes)
  • mag_filter: 放大过滤,当纹理需要放大显示时使用

    • linear: 线性过滤,会对临近像素进行插值,使图像更平滑
    • nearest: 最近点过滤,使用最接近的像素,保持像素化效果
  • min_filter: 缩小过滤,当纹理需要缩小显示时使用

    • 同样有linearnearest选项
  1. 寻址模式(Address Modes)
  • address::repeat: 重复模式,当UV坐标超出[0,1]范围时,纹理会重复
  • address::clamp_to_edge: 边缘延伸,超出范围的UV会使用边缘像素值
  • address::mirrored_repeat: 镜像重复
  • address::clamp_to_zero: 超出范围的UV会返回透明黑色
  1. 其他常用属性
  • mip_filter: 多级渐进纹理过滤
  • max_anisotropy: 各向异性过滤程度,提高倾斜视角的质量

使用采样器采样纹理

在着色器中使用采样器采样纹理的方式:

float4 texColor = inputTexture.sample(textureSampler, texCoord);

这行代码中:

  • inputTexture: 输入的纹理
  • textureSampler: 定义的采样器
  • texCoord: 采样的UV坐标(通常在[0,1]范围内)
  • 返回的texColor是采样得到的颜色值

采样器的实际效果

  1. 重复寻址模式(address::repeat)

在代码中可以看到:

texCoord.x = fract(texCoord.x + timer * 0.02);

这使纹理沿x轴平移,当坐标超出[0,1]范围时,因为使用了repeat模式,纹理会循环重复,产生连续滚动效果。

  1. 线性过滤(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)")
        }
    }
}



你可能感兴趣的:(app开发,ios,xcode,3d)