如何为凸多边形内部按到最近边的距离实现渐变着色(仿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,1]坐标系;
- 计算当前点到每条边的垂直距离,取最小值;
- 将最小距离映射为颜色值,写入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
相关产品推荐
相关产品推荐

