You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Metal的lineStrip绘制更粗的线条?

解决Metal lineStrip多次抖动绘制加粗无效及周期性问题

你的方案没达到预期,核心问题出在抖动范围计算、偏移方向逻辑和抖动空间选择三个地方,逐个给你修正:

1. 修正抖动范围的计算

你当前的jitter计算逻辑1/screenSizeX - 1/screenSizeY会导致抖动范围极小甚至趋近于0(当屏幕宽高接近时),而且没有匹配线条加粗的实际需求。改成基于屏幕像素的归一化抖动范围:

// 设定要加粗的像素宽度,比如2像素,可按需调整
let boldPixelWidth = 2.0
// 转换到Metal的NDC空间(范围[-1,1])
let jitterRange = boldPixelWidth / Float(self.screenSizeX)
// 生成X/Y轴的随机抖动,Z轴若无3D需求可设为0
var jitterX = Float.random(in: -jitterRange...jitterRange)
var jitterY = Float.random(in: -jitterRange...jitterRange)
var jitterZ: Float = 0.0

2. 重构顶点着色器的偏移逻辑

你当前根据vertexId奇偶做径向加减偏移,会把线条拉扯成对称折线,重复绘制后自然出现周期性。正确的做法是在屏幕空间添加偏移,而非模型空间:

快速实现版(随机屏幕偏移)

直接在屏幕空间给顶点加随机小偏移,多次叠加后形成模糊加粗效果:

vertex VertexOut vertexShader(uint vertexId [[vertex_id]],
                              const device VertexIn* vertices [[buffer(0)]],
                              constant VertexUniforms& uniforms [[buffer(1)]]) {
    VertexOut vertexOut;
    
    // 计算原始裁剪空间位置
    float4 position = uniforms.modelMatrix * float4(vertices[vertexId].position, 1.0);
    position = uniforms.viewProjectionMatrix * position;
    vertexOut.position = position;
    
    // 转换到屏幕NDC空间(范围[-1,1])
    float2 screenPos = position.xy / position.w;
    // 添加屏幕空间随机抖动
    screenPos += float2(uniforms.jitterX, uniforms.jitterY);
    // 转换回裁剪空间
    vertexOut.position.xy = screenPos * position.w;
    
    // 其他顶点属性处理(如颜色)
    vertexOut.color = vertices[vertexId].color;
    
    return vertexOut;
}

精准版(垂直于线条方向偏移)

如果需要更像实心粗线的效果,计算线条的垂直方向后添加偏移,避免模糊:

// 先实现一个简单的伪随机函数,避免重复图案
float random(float seed) {
    return fract(sin(seed * 12.9898) * 43758.5453);
}

vertex VertexOut vertexShader(uint vertexId [[vertex_id]],
                              const device VertexIn* vertices [[buffer(0)]],
                              constant VertexUniforms& uniforms [[buffer(1)]],
                              uint vertexCount [[buffer(2)]]) {
    VertexOut vertexOut;
    
    // 计算原始裁剪空间位置
    float4 position = uniforms.modelMatrix * float4(vertices[vertexId].position, 1.0);
    position = uniforms.viewProjectionMatrix * position;
    float4 originalPos = position;
    
    // 转换到屏幕空间
    float2 screenPos = originalPos.xy / originalPos.w;
    
    // 计算线条的方向向量
    float2 nextScreenPos;
    if (vertexId == vertexCount - 1) {
        // 最后一个顶点,复用前一个顶点的方向
        float4 prevPos = uniforms.modelMatrix * float4(vertices[vertexId-1].position, 1.0);
        prevPos = uniforms.viewProjectionMatrix * prevPos;
        nextScreenPos = prevPos.xy / prevPos.w;
    } else {
        float4 nextPos = uniforms.modelMatrix * float4(vertices[vertexId+1].position, 1.0);
        nextPos = uniforms.viewProjectionMatrix * nextPos;
        nextScreenPos = nextPos.xy / nextPos.w;
    }
    float2 lineDir = normalize(nextScreenPos - screenPos);
    // 计算垂直于线条的方向(顺时针旋转90度)
    float2 perpDir = float2(lineDir.y, -lineDir.x);
    
    // 添加垂直方向的随机抖动,用迭代次数做种子避免重复
    float jitterAmount = uniforms.jitterX;
    screenPos += perpDir * jitterAmount * random(vertexId + uniforms.iteration);
    
    // 转换回裁剪空间
    originalPos.xy = screenPos * originalPos.w;
    vertexOut.position = originalPos;
    
    vertexOut.color = vertices[vertexId].color;
    
    return vertexOut;
}

3. 调整绘制循环细节

  • 确保每次迭代都重新生成随机抖动值,并更新Uniforms后再调用draw,否则所有迭代用同一个抖动会导致周期性
  • 开启混合模式,让多次绘制的线条叠加产生加粗效果:
    commandEncoder.setBlendColorOperation(.add)
    commandEncoder.setBlendAlphaOperation(.add)
    commandEncoder.setSourceRGBBlendFactor(.sourceAlpha)
    commandEncoder.setDestinationRGBBlendFactor(.oneMinusSourceAlpha)
    
  • 迭代次数无需1024次,30-100次就能达到明显加粗效果,过多会浪费性能

内容的提问来源于stack exchange,提问作者PhilBot

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 02:12:17