JavaScript 里的 SDF(有向距离场):在 2D 画布上实现无限分辨率的图形渲染

JavaScript 中的 SDF(有向距离场):在 2D 画布上实现无限分辨率的图形渲染

大家好,我是你们今天的讲师。今天我们来深入探讨一个非常有趣、也非常实用的技术主题:SDF(Signed Distance Field,有向距离场) 在 JavaScript 和 HTML5 Canvas 中的应用。

如果你曾经遇到过以下问题:

  • 图形在缩放时变得模糊或锯齿严重?
  • 想要在不同分辨率下保持清晰度,但又不想用多张图片?
  • 希望用代码动态生成高质量矢量图形?

那么你一定会爱上今天的内容——使用 SDF 技术,在 2D Canvas 上实现真正“无限分辨率”的图形渲染


一、什么是 SDF?它为什么重要?

定义与基本原理

SDF 是一种表示形状的方法,其核心思想是:

对于图像中的任意一点 (x, y),计算该点到最近边界(轮廓)的距离,并带上符号(正/负):

  • 如果点在形状外部,距离为正;
  • 如果点在形状内部,距离为负;
  • 如果点恰好在边界上,距离为 0。

这种数据结构可以被看作是一个二维数组(或纹理),每个像素存储的是该位置到最近边界的“带符号距离”。

为什么 SDF 能实现无限分辨率?

因为它是基于数学函数定义的!我们不是直接绘制像素,而是通过一个连续函数(比如 distanceToShape(x, y))来判断每个点是否属于某个图形。

只要这个函数足够精确,无论你在屏幕上放大多少倍,都能得到平滑、清晰的结果 —— 这就是所谓的 “无限分辨率”

✅ 关键优势:不依赖像素密度,只依赖算法精度!


二、从零开始构建一个简单的 SDF 渲染器

我们将一步步从最基础的圆形 SDF 开始,逐步扩展成可复用的框架。

步骤 1:创建 HTML + Canvas 环境

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8" />
    <title>SDF 渲染示例</title>
    <style>
        canvas {
            border: 1px solid #ccc;
            image-rendering: pixelated; /* 防止浏览器自动插值 */
        }
    </style>
</head>
<body>
    <canvas id="canvas" width="600" height="400"></canvas>
    <script src="sdf.js"></script>
</body>
</html>

步骤 2:实现基础圆形 SDF 函数

// sdf.js

function circleSDF(x, y, cx, cy, radius) {
    const dx = x - cx;
    const dy = y - cy;
    return Math.sqrt(dx * dx + dy * dy) - radius;
}

这个函数返回的是从点 (x, y) 到圆心 (cx, cy) 的距离减去半径,结果就是带符号的距离。

输入参数 含义
x, y 当前要测试的点坐标
cx, cy 圆心坐标
radius 圆的半径

💡 示例:如果点在圆内,距离小于半径 → 返回负数;反之则为正数。


三、如何将 SDF 映射为像素颜色?

我们要做的是:遍历画布上的每一个像素,调用对应的 SDF 函数,根据结果决定该像素的颜色。

实现简单着色逻辑

function renderSDF(ctx, sdfFunction, width, height, colorFn) {
    const imageData = ctx.createImageData(width, height);
    const data = imageData.data;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const dist = sdfFunction(x, y);

            // 使用简单的阈值着色:距离 <= 0 表示在形状内部
            const alpha = Math.min(1, Math.abs(dist) / 3); // 控制边缘柔化程度
            const r = colorFn ? colorFn(dist).r : 255;
            const g = colorFn ? colorFn(dist).g : 0;
            const b = colorFn ? colorFn(dist).b : 0;

            const idx = (y * width + x) * 4;
            data[idx] = r;
            data[idx + 1] = g;
            data[idx + 2] = b;
            data[idx + 3] = Math.floor(alpha * 255);
        }
    }

    ctx.putImageData(imageData, 0, 0);
}

这里的关键是:

  • sdfFunction(x, y) 返回当前点的有向距离;
  • 我们可以用 Math.abs(dist) 来控制边缘的透明度(即抗锯齿效果);
  • 最终把 RGBA 数据写入 ImageData 并绘制到 Canvas。

四、实战案例:渲染多个形状(矩形 + 圆形)

现在我们来做一个组合例子,展示如何用 SDF 渲染两个形状。

function unionSDF(a, b) {
    return Math.min(a, b);
}

function subtractSDF(a, b) {
    return Math.max(a, -b);
}

function intersectSDF(a, b) {
    return Math.max(a, b);
}

// 定义两个基础形状的 SDF
function rectSDF(x, y, rx, ry, cx, cy) {
    const dx = Math.abs(x - cx) - rx;
    const dy = Math.abs(y - cy) - ry;
    const dist = Math.max(dx, dy);
    return dist;
}

function complexShape(x, y) {
    const circleDist = circleSDF(x, y, 200, 200, 80);
    const rectDist = rectSDF(x, y, 60, 60, 350, 200);

    // 组合:圆形和矩形的并集
    return unionSDF(circleDist, rectDist);
}

// 渲染复杂形状
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

renderSDF(ctx, complexShape, canvas.width, canvas.height, (dist) => ({
    r: 255,
    g: 100 + Math.abs(dist) * 5,
    b: 100 + Math.abs(dist) * 5
}));

✅ 效果说明:

  • 圆形和矩形合并为一个整体;
  • 边缘自然过渡,无锯齿;
  • 放大后依然清晰(因为本质是数学表达式)。

五、进阶技巧:抗锯齿与边缘平滑

刚才的例子中我们用了 alpha = Math.min(1, Math.abs(dist) / 3) 来模拟抗锯齿。这其实是一种 基于距离的软阴影技术

更专业的做法是使用 SDF 的梯度信息 来进行边缘采样优化。

计算梯度近似(用于抗锯齿)

function computeGradient(sdfFunc, x, y, eps = 0.01) {
    const dx = sdfFunc(x + eps, y) - sdfFunc(x - eps, y);
    const dy = sdfFunc(x, y + eps) - sdfFunc(x, y - eps);
    return { dx, dy };
}

然后我们可以利用梯度方向来做更精细的采样,例如在边缘处增加亚像素采样(类似 MSAA),但这已经超出本文范围,适合进一步研究。


六、性能优化建议(针对大规模场景)

虽然 SDF 很强大,但它本质上是 O(n²) 的逐像素计算。对于大型画布或实时动画来说,必须考虑性能。

优化策略 描述 是否推荐
分块处理 将大画布分成若干小块,按需渲染 ✅ 强烈推荐
缓存 SDF 数据 若形状不变,缓存每帧的 distance map ✅ 推荐
使用 WebGL 将 SDF 计算移至 GPU(Shader) ✅ 最佳实践
动态 LOD(细节层次) 根据缩放级别调整分辨率 ✅ 推荐

示例:分块渲染(伪代码)

function renderInChunks(ctx, sdfFunc, width, height, chunkSize = 64) {
    for (let y = 0; y < height; y += chunkSize) {
        for (let x = 0; x < width; x += chunkSize) {
            const w = Math.min(chunkSize, width - x);
            const h = Math.min(chunkSize, height - y);
            const subCtx = createOffscreenCanvas(w, h).getContext('2d');
            renderSDF(subCtx, sdfFunc, w, h, null);
            ctx.drawImage(subCtx.canvas, x, y);
        }
    }
}

这样可以避免一次性处理整个画布带来的卡顿问题。


七、应用场景总结(不只是图形!)

应用场景 说明 是否适合 SDF
字体渲染(如 FreeType 替代方案) 用 SDF 存储字体轮廓,支持任意缩放 ✅ 极佳
UI 设计系统 快速生成图标、按钮等矢量元素 ✅ 推荐
游戏开发(Unity / Unreal 的替代) 用 SDF 实现低功耗、高清晰度 UI ✅ 成熟应用
科学可视化 可视化复杂几何体(如流体、粒子) ✅ 适用
AR/VR 内容生成 实时生成高质量 2D 图形 ✅ 高效方案

🧠 提示:SDF 不仅能渲染静态图形,还可以结合动画时间 t 来实现动态变形(比如心跳波纹、流动效果)!


八、常见误区澄清

误区 解释 正确做法
“SDF 只能用来画形状” 错!它可以表示任意连续函数 用数学表达式定义复杂形状
“SDF 性能差” 不一定!合理分块+缓存即可高效运行 优先优化内存访问模式
“只能用在 Canvas” 错!WebGL、Three.js、甚至原生 App 都可用 扩展性强,跨平台友好
“SDF 无法做光照” 错!配合法线(来自梯度)可实现 Phong 着色 加入光照模型即可

九、结语:未来趋势与学习路径

SDF 已经成为现代图形引擎(尤其是移动端和 Web)的标准工具之一。苹果的 Core Graphics、Google 的 Skia、以及 Unity 的 Shader Graph 都内置了对 SDF 的支持。

如果你想深入掌握这项技能,请按如下路径学习:

  1. ✅ 掌握基础 SDF 数学(圆、矩形、椭圆);
  2. ✅ 实践组合操作(union, intersection, difference);
  3. ✅ 学会用 WebGL 实现高性能 SDF 渲染(GLSL shader);
  4. ✅ 结合动画、交互(鼠标事件、拖拽)制作完整应用;
  5. ✅ 探索高级应用(如物理模拟、粒子系统、UI 动画)。

十、附录:完整可运行示例代码(HTML + JS)

<!-- index.html -->
<canvas id="canvas" width="600" height="400"></canvas>
<script>
function circleSDF(x, y, cx, cy, radius) {
    const dx = x - cx;
    const dy = y - cy;
    return Math.sqrt(dx*dx + dy*dy) - radius;
}

function renderSDF(ctx, sdfFunc, width, height, colorFn) {
    const imageData = ctx.createImageData(width, height);
    const data = imageData.data;

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const dist = sdfFunc(x, y);
            const alpha = Math.min(1, Math.abs(dist) / 3);
            const r = colorFn ? colorFn(dist).r : 255;
            const g = colorFn ? colorFn(dist).g : 0;
            const b = colorFn ? colorFn(dist).b : 0;

            const idx = (y * width + x) * 4;
            data[idx] = r;
            data[idx + 1] = g;
            data[idx + 2] = b;
            data[idx + 3] = Math.floor(alpha * 255);
        }
    }

    ctx.putImageData(imageData, 0, 0);
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

renderSDF(ctx, (x, y) => circleSDF(x, y, 300, 200, 100), 
          canvas.width, canvas.height, 
          (dist) => ({ r: 255, g: 100 + Math.abs(dist)*5, b: 100 }));
</script>

运行这段代码,你会看到一个完美中心对齐、无锯齿、无限缩放的圆形!🎉


这就是我们今天的全部内容。希望你现在已经理解了 SDF 的强大之处,并且能够在实际项目中灵活运用它。记住一句话:

“当你不再依赖像素,而是在用数学说话时,你就拥有了真正的无限分辨率。”

谢谢大家!欢迎提问,我们一起讨论更多可能性 😊

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注