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

如何为凸多边形内部按到最近边的距离实现渐变着色(仿Blender效果)

凸多边形内部按到最近边距离着色的实现方案

问题描述

给定位于[[-1,1],[-1,1]]平面内的凸多边形,边集定义如下:

edges = [
   [[-0.8, 0],[-0.3,0.6]],
   [[-0.3,0.6],[0,0.8]],
   [[0,0.8],[0.7,0.6]],
   [[0.7,0.6],[0.6,-0.9]],
   [[0.6,-0.9],[-0.8,0]]
]

需要实现类似Blender中“Distance to Edge”的效果:根据多边形内部每个点到最近边的距离进行渐变着色。此前尝试分割区域填充线性渐变未成功,查看相关源码也无帮助,以下提供2D Canvas和WebGL两种环境下的完整实现方案。


2D Canvas实现方案

Canvas 2D API没有原生支持距离场着色,我们可以通过逐像素计算距离的方式实现:

  1. 先绘制多边形路径,用于判断点是否在多边形内部;
  2. 遍历画布所有像素,将屏幕坐标转换为多边形的[-1,1]坐标系;
  3. 计算当前点到每条边的垂直距离,取最小值;
  4. 将最小距离映射为颜色值,写入ImageData并渲染。

完整代码

<canvas width="640" height="360"></canvas>
<script>
const edges = [
   [[-0.8, 0],[-0.3,0.6]],
   [[-0.3,0.6],[0,0.8]],
   [[0,0.8],[0.7,0.6]],
   [[0.7,0.6],[0.6,-0.9]],
   [[0.6,-0.9],[-0.8,0]]
];

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;

// 创建多边形路径,用于点-in-polygon判断
ctx.beginPath();
edges.forEach((edge, idx) => {
    const [x, y] = screenToCanvas(edge[0]);
    if (idx === 0) ctx.moveTo(x, y);
    ctx.lineTo(...screenToCanvas(edge[1]));
});
ctx.closePath();

// 获取ImageData用于像素操作
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 [px, py] = canvasToPolygon(x, y);
        
        // 判断点是否在多边形内部
        if (!ctx.isPointInPath(x, y)) {
            // 外部点设为背景色
            const idx = (y * width + x) * 4;
            data[idx] = 0;
            data[idx+1] = 0;
            data[idx+2] = 0;
            data[idx+3] = 255;
            continue;
        }
        
        // 计算到所有边的最小距离
        let minDist = Infinity;
        for (const [A, B] of edges) {
            const dist = pointToLineDistance([px, py], A, B);
            if (dist < minDist) minDist = dist;
        }
        
        // 将距离映射为灰度值(可自定义为彩色渐变)
        const maxDist = 0.3; // 可根据多边形实际大小调整
        const normalized = Math.min(minDist / maxDist, 1);
        const gray = Math.floor((1 - normalized) * 255);
        
        // 写入像素数据
        const idx = (y * width + x) * 4;
        data[idx] = gray;
        data[idx+1] = gray;
        data[idx+2] = gray;
        data[idx+3] = 255;
    }
}

// 渲染ImageData
ctx.putImageData(imageData, 0, 0);

// 辅助函数:多边形坐标转屏幕坐标
function screenToCanvas([x, y]) {
    return [
        width * 0.5 + x * 0.5 * width,
        height * 0.5 - y * 0.5 * height
    ];
}

// 辅助函数:屏幕坐标转多边形坐标
function canvasToPolygon(x, y) {
    return [
        (x - width * 0.5) / (0.5 * width),
        (height * 0.5 - y) / (0.5 * height)
    ];
}

// 辅助函数:计算点到线段的垂直距离
function pointToLineDistance(P, A, B) {
    const ABx = B[0] - A[0];
    const ABy = B[1] - A[1];
    const APx = P[0] - A[0];
    const APy = P[1] - A[1];
    
    const t = Math.max(0, Math.min(1, (APx * ABx + APy * ABy) / (ABx * ABx + ABy * ABy)));
    const projX = A[0] + t * ABx;
    const projY = A[1] + t * ABy;
    
    return Math.hypot(P[0] - projX, P[1] - projY);
}
</script>

WebGL实现方案

WebGL的片元着色器适合并行处理像素计算,效率远高于Canvas 2D逐像素遍历。核心思路是在片元着色器中计算每个点到所有边的距离,取最小值后映射为颜色。

完整代码

<canvas width="640" height="360"></canvas>
<script>
const edges = [
   [[-0.8, 0],[-0.3,0.6]],
   [[-0.3,0.6],[0,0.8]],
   [[0,0.8],[0.7,0.6]],
   [[0.7,0.6],[0.6,-0.9]],
   [[0.6,-0.9],[-0.8,0]]
];

// 转换为扁平化的边数据(每个边4个分量:Ax, Ay, Bx, By)
const edgeData = edges.flatMap(([A, B]) => [...A, ...B]);

const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");

if (!gl) {
    alert("浏览器不支持WebGL");
}

// 顶点着色器:绘制全屏矩形
const vertShaderSource = `
attribute vec2 a_position;
void main() {
    gl_Position = vec4(a_position, 0.0, 1.0);
}
`;

// 片元着色器:计算距离并着色
const fragShaderSource = `
precision mediump float;

uniform vec2 u_resolution;
uniform float u_edgeCount;
uniform vec4 u_edges[5]; // 边数量固定为5,可根据实际调整

// 计算点到线段的距离
float pointToLine(vec2 p, vec2 a, vec2 b) {
    vec2 ab = b - a;
    vec2 ap = p - a;
    float t = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0);
    vec2 proj = a + t * ab;
    return distance(p, proj);
}

// 判断点是否在凸多边形内部
bool pointInConvexPolygon(vec2 p) {
    for (int i = 0; i < int(u_edgeCount); i++) {
        vec2 a = u_edges[i].xy;
        vec2 b = u_edges[i].zw;
        vec2 edge = b - a;
        vec2 normal = vec2(-edge.y, edge.x); // 凸多边形的外法线
        if (dot(p - a, normal) > 0.0) {
            return false;
        }
    }
    return true;
}

void main() {
    // 将屏幕坐标转换为[-1,1]的多边形坐标系,保持宽高比
    vec2 uv = (gl_FragCoord.xy / u_resolution) * 2.0 - 1.0;
    uv.y *= u_resolution.y / u_resolution.x;
    
    // 判断是否在多边形内部
    if (!pointInConvexPolygon(uv)) {
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
        return;
    }
    
    // 计算到所有边的最小距离
    float minDist = 100.0;
    for (int i = 0; i < int(u_edgeCount); i++) {
        vec2 a = u_edges[i].xy;
        vec2 b = u_edges[i].zw;
        float dist = pointToLine(uv, a, b);
        if (dist < minDist) {
            minDist = dist;
        }
    }
    
    // 归一化距离并映射为颜色(这里用灰度渐变,可自定义为彩色)
    float maxDist = 0.3; // 可根据多边形实际大小调整
    float normalized = clamp(minDist / maxDist, 0.0, 1.0);
    float gray = 1.0 - normalized;
    gl_FragColor = vec4(gray, gray, gray, 1.0);
}
`;

// 创建并编译着色器
function createShader(gl, type, source) {
    const shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error(gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

// 创建着色器程序
const vertShader = createShader(gl, gl.VERTEX_SHADER, vertShaderSource);
const fragShader = createShader(gl, gl.FRAGMENT_SHADER, fragShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertShader);
gl.attachShader(program, fragShader);
gl.linkProgram(program);

if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error(gl.getProgramInfoLog(program));
}

// 全屏矩形顶点数据(覆盖整个画布)
const vertices = new Float32Array([
    -1.0, -1.0,
     1.0, -1.0,
    -1.0,  1.0,
     1.0,  1.0
]);

// 创建顶点缓冲区
const vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

// 获取属性和 uniform 位置
const aPositionLoc = gl.getAttribLocation(program, "a_position");
const uResolutionLoc = gl.getUniformLocation(program, "u_resolution");
const uEdgeCountLoc = gl.getUniformLocation(program, "u_edgeCount");
const uEdgesLoc = gl.getUniformLocation(program, "u_edges");

// 设置渲染状态
gl.useProgram(program);
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);

// 设置uniform变量
gl.uniform2f(uResolutionLoc, canvas.width, canvas.height);
gl.uniform1f(uEdgeCountLoc, edges.length);
gl.uniform4fv(uEdgesLoc, new Float32Array(edgeData));

// 设置顶点属性
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.enableVertexAttribArray(aPositionLoc);
gl.vertexAttribPointer(aPositionLoc, 2, gl.FLOAT, false, 0, 0);

// 绘制全屏矩形
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
</script>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 09:40:53