From 055eb1018ea6f374eda6146de2b501d99723119d Mon Sep 17 00:00:00 2001 From: "yuqi.pyq" Date: Thu, 20 Jul 2023 10:40:53 +0800 Subject: [PATCH] fix: draw 1px sub-pixel line correctly in webgl #1425 --- .../src/shader/instanced-line.frag | 7 +- .../src/shader/instanced-line.vert | 9 +- site/examples/perf/animation/demo/meta.json | 7 + .../perf/animation/demo/webgl-opacity.js | 58 ++++++++ site/examples/shape/line/demo/meta.json | 8 ++ site/examples/shape/line/demo/thin-line.js | 124 ++++++++++++++++++ 6 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 site/examples/perf/animation/demo/webgl-opacity.js create mode 100644 site/examples/shape/line/demo/thin-line.js diff --git a/packages/g-plugin-device-renderer/src/shader/instanced-line.frag b/packages/g-plugin-device-renderer/src/shader/instanced-line.frag index 01ba0b6b5..6aa2b386c 100644 --- a/packages/g-plugin-device-renderer/src/shader/instanced-line.frag +++ b/packages/g-plugin-device-renderer/src/shader/instanced-line.frag @@ -24,7 +24,12 @@ void main() { outputColor = u_Color; #endif - float blur = smoothstep(0.0, v_Distance.y, 1.0 - abs(v_Distance.x)); + float blur; + if (v_Distance.y < 1.0) { + blur = smoothstep(0.0, v_Distance.y, 1.0 - abs(v_Distance.x)); + } else { + blur = 1.0 / v_Distance.y; + } float u_dash_offset = v_Dash.y; float u_dash_array = v_Dash.z; float u_dash_ratio = v_Dash.w; diff --git a/packages/g-plugin-device-renderer/src/shader/instanced-line.vert b/packages/g-plugin-device-renderer/src/shader/instanced-line.vert index 3bae5eb5f..db096fca8 100644 --- a/packages/g-plugin-device-renderer/src/shader/instanced-line.vert +++ b/packages/g-plugin-device-renderer/src/shader/instanced-line.vert @@ -30,6 +30,7 @@ void main() { } else { strokeWidth = u_StrokeWidth; } + float clampedStrokeWidth = max(strokeWidth, 1.0); float isBillboard = a_Dash.w; bool isPerspective = isPerspectiveMatrix(u_ProjectionMatrix); @@ -43,8 +44,8 @@ void main() { vec2 screen1 = u_Viewport * (0.5 * clip1.xy / clip1.w + 0.5); vec2 xBasis = normalize(screen1 - screen0); vec2 yBasis = vec2(-xBasis.y, xBasis.x); - vec2 pt0 = screen0 + strokeWidth * (a_Position.x * xBasis + a_Position.y * yBasis); - vec2 pt1 = screen1 + strokeWidth * (a_Position.x * xBasis + a_Position.y * yBasis); + vec2 pt0 = screen0 + clampedStrokeWidth * (a_Position.x * xBasis + a_Position.y * yBasis); + vec2 pt1 = screen1 + clampedStrokeWidth * (a_Position.x * xBasis + a_Position.y * yBasis); vec2 pt = mix(pt0, pt1, a_Position.z); vec4 clip = mix(clip0, clip1, a_Position.z); gl_Position = vec4(clip.w * (2.0 * pt / u_Viewport - 1.0), clip.z, clip.w); @@ -52,12 +53,12 @@ void main() { vec2 xBasis = a_PointB.xy - a_PointA.xy; vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); - vec2 point = a_PointA.xy + xBasis * a_Position.x + yBasis * strokeWidth * a_Position.y; + vec2 point = a_PointA.xy + xBasis * a_Position.x + yBasis * clampedStrokeWidth * a_Position.y; point = point - u_Anchor.xy * abs(xBasis); // round & square if (a_Cap > 1.0) { - point += sign(a_Position.x - 0.5) * normalize(xBasis) * vec2(strokeWidth / 2.0); + point += sign(a_Position.x - 0.5) * normalize(xBasis) * vec2(clampedStrokeWidth / 2.0); } gl_Position = project(vec4(point, u_ZIndex, 1.0), u_ProjectionMatrix, u_ViewMatrix, u_ModelMatrix); } diff --git a/site/examples/perf/animation/demo/meta.json b/site/examples/perf/animation/demo/meta.json index ffa6b10b4..51ce7cae1 100644 --- a/site/examples/perf/animation/demo/meta.json +++ b/site/examples/perf/animation/demo/meta.json @@ -34,6 +34,13 @@ "zh": "在 WebGL 中更新 Text 的位置", "en": "Update position of Text in WebGL" } + }, + { + "filename": "webgl-opacity.js", + "title": { + "zh": "在 WebGL 中更新 Opacity", + "en": "Update opacity of shapes in WebGL" + } } ] } diff --git a/site/examples/perf/animation/demo/webgl-opacity.js b/site/examples/perf/animation/demo/webgl-opacity.js new file mode 100644 index 000000000..48f930648 --- /dev/null +++ b/site/examples/perf/animation/demo/webgl-opacity.js @@ -0,0 +1,58 @@ +import { Canvas, CanvasEvent, Rect, runtime } from '@antv/g'; +import { Renderer } from '@antv/g-webgl'; +import Stats from 'stats.js'; + +runtime.enableCSSParsing = false; + +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 500, + renderer: new Renderer(), +}); + +canvas.addEventListener(CanvasEvent.READY, () => { + const rect1 = new Rect({ + style: { + x: 200, + y: 200, + width: 200, + height: 200, + fill: 'blue', + }, + }); + + const rect2 = new Rect({ + style: { + x: 250, + y: 250, + width: 100, + height: 100, + fill: 'red', + }, + }); + + canvas.appendChild(rect1); + canvas.appendChild(rect2); + + rect2.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: 2000, + fill: 'both', + iterations: Infinity, + }); +}); + +// stats +const stats = new Stats(); +stats.showPanel(0); +const $stats = stats.dom; +$stats.style.position = 'absolute'; +$stats.style.left = '0px'; +$stats.style.top = '0px'; +const $wrapper = document.getElementById('container'); +$wrapper.appendChild($stats); +canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + if (stats) { + stats.update(); + } +}); diff --git a/site/examples/shape/line/demo/meta.json b/site/examples/shape/line/demo/meta.json index 037524f8b..eda802c5f 100644 --- a/site/examples/shape/line/demo/meta.json +++ b/site/examples/shape/line/demo/meta.json @@ -19,6 +19,14 @@ "en": "Marker" }, "screenshot": "https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*X5W_TYz-2SIAAAAAAAAAAAAAARQnAQ" + }, + { + "filename": "thin-line.js", + "title": { + "zh": "绘制宽度小于 1px 的线", + "en": "Thin Line" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*FNTzSp46rwgAAAAAAAAAAAAADmJ7AQ/original" } ] } diff --git a/site/examples/shape/line/demo/thin-line.js b/site/examples/shape/line/demo/thin-line.js new file mode 100644 index 000000000..fb82685a6 --- /dev/null +++ b/site/examples/shape/line/demo/thin-line.js @@ -0,0 +1,124 @@ +import { Canvas, CanvasEvent, Line, Path } from '@antv/g'; +import { Renderer as CanvasRenderer } from '@antv/g-canvas'; +import { Renderer as CanvaskitRenderer } from '@antv/g-canvaskit'; +import { Renderer as SVGRenderer } from '@antv/g-svg'; +import { Renderer as WebGLRenderer } from '@antv/g-webgl'; +import { Renderer as WebGPURenderer } from '@antv/g-webgpu'; +import * as lil from 'lil-gui'; +import Stats from 'stats.js'; + +// create a renderer +const canvasRenderer = new CanvasRenderer(); +const webglRenderer = new WebGLRenderer(); +const svgRenderer = new SVGRenderer(); +const canvaskitRenderer = new CanvaskitRenderer({ + wasmDir: '/', + fonts: [ + { + name: 'Roboto', + url: '/Roboto-Regular.ttf', + }, + { + name: 'sans-serif', + url: 'https://mdn.alipayobjects.com/huamei_qa8qxu/afts/file/A*064aSK2LUPEAAAAAAAAAAAAADmJ7AQ/NotoSansCJKsc-VF.ttf', + }, + ], +}); +const webgpuRenderer = new WebGPURenderer({ + shaderCompilerPath: '/glsl_wgsl_compiler_bg.wasm', +}); + +// create a canvas +const canvas = new Canvas({ + container: 'container', + width: 600, + height: 500, + renderer: canvasRenderer, +}); + +// create a line +const line1 = new Line({ + style: { + x1: 200, + y1: 100, + x2: 400, + y2: 100, + stroke: '#1890FF', + lineWidth: 0.1, + cursor: 'pointer', + }, +}); +const line2 = line1.cloneNode(); +const line3 = line1.cloneNode(); +const line4 = line1.cloneNode(); +line2.style.lineWidth = 0.5; +line3.style.lineWidth = 1; +line4.style.lineWidth = 2; +line2.translate(0, 50); +line3.translate(0, 100); +line4.translate(0, 150); + +const path = new Path({ + style: { + lineWidth: 0.5, + stroke: '#54BECC', + d: 'M 0,40 C 5.5555555555555545,40,22.222222222222218,44.44444444444445,33.33333333333333,40 C 44.444444444444436,35.55555555555556,55.55555555555554,14.66666666666667,66.66666666666666,13.333333333333336 C 77.77777777777777,12.000000000000002,88.88888888888887,32,100,32 C 111.11111111111113,32,122.22222222222221,14.66666666666667,133.33333333333331,13.333333333333336 C 144.44444444444443,12.000000000000002,155.55555555555557,24,166.66666666666669,24 C 177.7777777777778,24,188.8888888888889,11.111111111111114,200,13.333333333333336 C 211.1111111111111,15.555555555555557,222.22222222222226,35.111111111111114,233.33333333333334,37.333333333333336 C 244.44444444444443,39.55555555555555,255.55555555555551,31.22222222222223,266.66666666666663,26.66666666666667 C 277.77777777777777,22.111111111111114,294.4444444444444,12.777777777777779,300,10', + transform: 'translate(100, 100)', + }, +}); + +canvas.addEventListener(CanvasEvent.READY, () => { + canvas.appendChild(line1); + canvas.appendChild(line2); + canvas.appendChild(line3); + canvas.appendChild(line4); + canvas.appendChild(path); +}); + +// stats +const stats = new Stats(); +stats.showPanel(0); +const $stats = stats.dom; +$stats.style.position = 'absolute'; +$stats.style.left = '0px'; +$stats.style.top = '0px'; +const $wrapper = document.getElementById('container'); +$wrapper.appendChild($stats); +canvas.addEventListener(CanvasEvent.AFTER_RENDER, () => { + if (stats) { + stats.update(); + } +}); + +// GUI +const gui = new lil.GUI({ autoPlace: false }); +$wrapper.appendChild(gui.domElement); +const rendererFolder = gui.addFolder('renderer'); +const rendererConfig = { + renderer: 'canvas', +}; + +rendererFolder + .add(rendererConfig, 'renderer', [ + 'canvas', + 'svg', + 'webgl', + 'webgpu', + 'canvaskit', + ]) + .onChange((rendererName) => { + let renderer; + if (rendererName === 'canvas') { + renderer = canvasRenderer; + } else if (rendererName === 'svg') { + renderer = svgRenderer; + } else if (rendererName === 'webgl') { + renderer = webglRenderer; + } else if (rendererName === 'webgpu') { + renderer = webgpuRenderer; + } else if (rendererName === 'canvaskit') { + renderer = canvaskitRenderer; + } + canvas.setRenderer(renderer); + }); +rendererFolder.open();