当前位置: 首页 > news >正文

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

传参解释:

  1. texture2d<float, access::write> output [[texture(0)]]

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

    • 一个常量浮点数引用,用于接收时间值
    • [[buffer(0)]]表示这个值从索引为0的缓冲区中读取
    • 在这个着色器中用于创建动画效果
  3. texture2d<float, access::sample> 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 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)")}}
}

相关文章:

  • [C++面试] 基础题
  • const修饰指针
  • 【网络篇】TCP协议的三次握手和四次挥手
  • 如何让Wi-Fi设备传输距离达到1100米?涂鸦新方案让通信距离远超传统5倍
  • Go 语言中的 Struct Tag 的用法详解
  • 从零开始:用Python语言基础构建宠物养成游戏:从核心知识到完整实战
  • MySQL 数据库表结构修改与字段添加
  • 常见的游戏服务器架构有哪些?
  • 【MySQL】06.MySQL表的增删查改
  • (1)深度学习基础知识(八股)——常用名词解释
  • gd32e230c8t6 驱动ws2812
  • vue2实现元素拖拽
  • 自由开发者计划 002:创建一个贷款计算器的微信小程序
  • Elasticsearch 写入性能优化有哪些常见手段?
  • 2025版 JavaScript性能优化实战指南从入门到精通
  • 【机器学习基础】机器学习入门核心算法:线性回归(Linear Regression)
  • 用vue canvas画一个能源电表和设备的监测图
  • 《STL--string的使用及其底层实现》
  • (第94天)OGG 微服务搭建 Oracle 19C CDB 架构同步
  • Openwrt下使用ffmpeg配合自建RTSP服务器实现推流
  • 加强服务保障满足群众急需i/sem对seo的影响有哪些
  • WordPress会员中心模板/临沂网站seo
  • 视觉品牌网站建设/企业专业搜索引擎优化
  • 运营平台/seo管理系统培训运营
  • 阳江招聘网官网/广州seo关键词
  • 怎么做支付网站/seo主要做什么