| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>Fat Lines/Mesh Lines 2</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs", | |||||
| "@threepipe/plugin-tweakpane": "./../../plugins/tweakpane/dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/global-loading.mjs"></script> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import { | |||||
| _testFinish, | |||||
| _testStart, | |||||
| Color, generateUiConfig, | |||||
| LineGeometry2, | |||||
| LineMaterial2, | |||||
| LoadingScreenPlugin, | |||||
| MeshLine, | |||||
| PickingPlugin, | |||||
| ThreeViewer, | |||||
| } from 'threepipe' | |||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | |||||
| // Read more about the example - https://threepipe.org/notes/fat-lines | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| plugins: [LoadingScreenPlugin, PickingPlugin], | |||||
| dropzone: true, | |||||
| }) | |||||
| viewer.scene.autoNearFarEnabled = false | |||||
| viewer.scene.backgroundColor = new Color(0x333333) | |||||
| const spiral = { | |||||
| radius: 1, | |||||
| height: 2, | |||||
| loops: 10, | |||||
| width: 5, | |||||
| } | |||||
| const line = new MeshLine(new LineGeometry2(), new LineMaterial2()) | |||||
| line.material.color = new Color(0xffffff) | |||||
| line.material.vertexColors = true | |||||
| line.material.linewidth = 5 // pixels | |||||
| line.rotateX(Math.PI / 2) | |||||
| function updateSpiral() { | |||||
| const {positions, colors} = makeSpiral(spiral.radius, spiral.height, spiral.loops) | |||||
| line.geometry.setPositions(positions) | |||||
| line.geometry.setColors(colors) | |||||
| line.material.linewidth = spiral.width | |||||
| line.setDirty() | |||||
| } | |||||
| updateSpiral() | |||||
| viewer.scene.addObject(line) | |||||
| const ui = viewer.addPluginSync(new TweakpaneUiPlugin(true)) | |||||
| ui.setupPluginUi(PickingPlugin) | |||||
| ui.appendChild({ | |||||
| type: 'folder', | |||||
| label: 'Spiral', | |||||
| children: generateUiConfig(spiral), | |||||
| onChange: updateSpiral, | |||||
| expanded: true, | |||||
| }) | |||||
| ui.appendChild(line.uiConfig) | |||||
| } | |||||
| _testStart() | |||||
| init().finally(_testFinish) | |||||
| function makeSpiral(radius = 1, height = 2, loops = 10) { | |||||
| const positions: number[] = [] | |||||
| const colors: number[] = [] | |||||
| const segments = 1000 | |||||
| const angleStep = Math.PI * 2 * loops / segments | |||||
| const heightStep = height / segments | |||||
| for (let i = 0; i <= segments; i++) { | |||||
| const angle = i * angleStep | |||||
| const x = radius * Math.cos(angle) | |||||
| const y = radius * Math.sin(angle) | |||||
| const z = i * heightStep - height / 2 | |||||
| positions.push(x, y, z) | |||||
| // Color gradient from blue to red | |||||
| const colorValue = i / segments | |||||
| colors.push(colorValue, 0, 1 - colorValue) // RGB | |||||
| } | |||||
| return {positions, colors} | |||||
| } |
| } from 'threepipe' | } from 'threepipe' | ||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | ||||
| // Read more about the example - https://threepipe.org/notes/gltf-mesh-lines | |||||
| async function init() { | async function init() { | ||||
| const viewer = new ThreeViewer({ | const viewer = new ThreeViewer({ |
| } from 'threepipe' | } from 'threepipe' | ||||
| import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | import {TweakpaneUiPlugin} from '@threepipe/plugin-tweakpane' | ||||
| // Read more about the example - https://threepipe.org/notes/gltf-mesh-lines | |||||
| async function init() { | async function init() { | ||||
| const viewer = new ThreeViewer({ | const viewer = new ThreeViewer({ |
| <li><a href="./3dm-to-glb/">Convert 3DM to GLB </a></li> | <li><a href="./3dm-to-glb/">Convert 3DM to GLB </a></li> | ||||
| <li><a href="./hdr-to-exr/">Convert HDR to EXR </a></li> | <li><a href="./hdr-to-exr/">Convert HDR to EXR </a></li> | ||||
| <li><a href="./fat-lines/">Fat Lines <br/>(Mesh Lines) </a></li> | <li><a href="./fat-lines/">Fat Lines <br/>(Mesh Lines) </a></li> | ||||
| <li><a href="./fat-line-spiral/">Line Spiral<br/>(Mesh Lines) </a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Experiments</h2> | <h2 class="category">Experiments</h2> | ||||
| <ul> | <ul> |
| }, | }, | ||||
| plugins: [LoadingScreenPlugin], | plugins: [LoadingScreenPlugin], | ||||
| }) | }) | ||||
| viewer.addPluginSync(new SSAAPlugin()) | viewer.addPluginSync(new SSAAPlugin()) | ||||
| await Promise.all([ | await Promise.all([ |
| export {ObjectShaderMaterial} from './material/ObjectShaderMaterial' | export {ObjectShaderMaterial} from './material/ObjectShaderMaterial' | ||||
| export {UnlitMaterial, MeshBasicMaterial2} from './material/UnlitMaterial' | export {UnlitMaterial, MeshBasicMaterial2} from './material/UnlitMaterial' | ||||
| export {UnlitLineMaterial, LineBasicMaterial2} from './material/UnlitLineMaterial' | export {UnlitLineMaterial, LineBasicMaterial2} from './material/UnlitLineMaterial' | ||||
| export {LineMaterial2} from './material/LineMaterial2' | |||||
| export {LineMaterial2, MeshLineMaterial} from './material/LineMaterial2' | |||||
| export {LegacyPhongMaterial} from './material/LegacyPhongMaterial' | export {LegacyPhongMaterial} from './material/LegacyPhongMaterial' | ||||
| export {Mesh2} from './object/Mesh2' | export {Mesh2} from './object/Mesh2' | ||||
| export {MeshLine} from './object/MeshLine' | export {MeshLine} from './object/MeshLine' |
| } | } | ||||
| } | } | ||||
| export class MeshLineMaterial extends LineMaterial2 {} |
| import {Line2} from 'three/examples/jsm/lines/Line2' | import {Line2} from 'three/examples/jsm/lines/Line2' | ||||
| import {iObjectCommons} from './iObjectCommons' | import {iObjectCommons} from './iObjectCommons' | ||||
| import {IMaterial} from '../IMaterial' | import {IMaterial} from '../IMaterial' | ||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| export class MeshLine< | export class MeshLine< | ||||
| TGeometry extends LineGeometry2 = LineGeometry2, | TGeometry extends LineGeometry2 = LineGeometry2, | ||||
| declare material: TMaterial | declare material: TMaterial | ||||
| declare readonly materials: IMaterial[] | declare readonly materials: IMaterial[] | ||||
| declare geometry: TGeometry | declare geometry: TGeometry | ||||
| declare uiConfig: UiObjectConfig | |||||
| /** | /** | ||||
| * @deprecated use `this` instead | * @deprecated use `this` instead |
| export const VERSION = '0.0.47' | |||||
| export const VERSION = '0.0.48' |
| {text: 'Plugin System', link: 'guide/plugin-system'}, | {text: 'Plugin System', link: 'guide/plugin-system'}, | ||||
| ] | ] | ||||
| }, | }, | ||||
| { | |||||
| text: 'Articles', collapsed: false, | |||||
| items: [ | |||||
| {text: 'Mesh Lines (Spiral)', link: 'notes/fat-lines'}, | |||||
| {text: 'glTF Mesh Lines', link: 'notes/gltf-mesh-lines'}, | |||||
| ] | |||||
| }, | |||||
| { | { | ||||
| text: 'Core Plugins', collapsed: false, | text: 'Core Plugins', collapsed: false, | ||||
| items: [ | items: [ | ||||
| { | { | ||||
| text: 'Packages', collapsed: false, | text: 'Packages', collapsed: false, | ||||
| items: [ | items: [ | ||||
| {text: 'WebGi Rendering Plugins', link: 'https://webgi.dev/'}, | |||||
| {text: 'Tweakpane Plugin', link: 'package/plugin-tweakpane'}, | {text: 'Tweakpane Plugin', link: 'package/plugin-tweakpane'}, | ||||
| {text: 'Blueprint.js Plugin', link: 'package/plugin-blueprintjs'}, | {text: 'Blueprint.js Plugin', link: 'package/plugin-blueprintjs'}, | ||||
| {text: 'Tweakpane Editor Plugin', link: 'package/plugin-tweakpane-editor'}, | {text: 'Tweakpane Editor Plugin', link: 'package/plugin-tweakpane-editor'}, |
| --- | |||||
| next: | |||||
| text: 'GLTF Fat/Mesh Lines' | |||||
| link: './gltf-mesh-lines' | |||||
| prev: false | |||||
| aside: false | |||||
| --- | |||||
| # Fat Lines: Spiral Example | |||||
| <iframe src="https://threepipe.org/examples/fat-line-spiral/" style="width:100%;height:600px;border:none;"></iframe> | |||||
| This example shows how to use ThreePipe's fat lines (MeshLine) to draw a thick, colorful spiral. The spiral's shape and color are generated in code, and you can adjust its parameters live using the UI. | |||||
| **How it works:** | |||||
| - The spiral is made by calculating 3D positions in a loop, using radius, height, and loops for shape. | |||||
| - Each point gets a color, creating a gradient from blue to red. | |||||
| - `MeshLine` (an extension of three.js Line2) is used to make the line thick and support per-vertex colors. | |||||
| - The UI lets you change the spiral's radius, height, and loops, and see updates instantly. | |||||
| **Key code:** | |||||
| ```typescript | |||||
| function makeSpiral(radius = 1, height = 2, loops = 10) { | |||||
| const positions = [], colors = [] | |||||
| for (let i = 0; i <= 1000; i++) { | |||||
| const t = i / 1000, angle = t * Math.PI * 2 * loops | |||||
| positions.push(radius * Math.cos(angle), radius * Math.sin(angle), t * height - height/2) | |||||
| colors.push(t, 0, 1-t) | |||||
| } | |||||
| return {positions, colors} | |||||
| } | |||||
| const line = new MeshLine(new LineGeometry2(), new LineMaterial2()) | |||||
| line.material.vertexColors = true | |||||
| line.material.linewidth = 5 | |||||
| function updateSpiral() { | |||||
| const {positions, colors} = makeSpiral(spiral.radius, spiral.height, spiral.loops) | |||||
| line.geometry.setPositions(positions) | |||||
| line.geometry.setColors(colors) | |||||
| line.material.setDirty() | |||||
| } | |||||
| ``` | |||||
| The line and its controls are added to the UI with Tweakpane: | |||||
| ```typescript | |||||
| ui.appendChild({ | |||||
| type: 'folder', | |||||
| label: 'Spiral', | |||||
| children: generateUiConfig(spiral), | |||||
| onChange: updateSpiral, | |||||
| }) | |||||
| ui.appendChild(line.uiConfig) | |||||
| ``` | |||||
| See the [full code here](https://github.com/repalash/threepipe/blob/master/examples/fat-line-spiral/script.ts), live example on [threepipe.org/examples](https://threepipe.org/examples/fat-line-spiral/). | |||||
| ::: warning | |||||
| Fat lines (MeshLine) cannot be exported with glTF. If you want to create lines that can be exported as glTF lines, use three.js `Line` directly (for now). | |||||
| ::: | |||||
| ## Features of `MeshLine` | |||||
| - **Adjustable thickness:** Unlike standard three.js lines, fat lines can be any width, not just 1 pixel. | |||||
| - **Per-vertex color:** Supports gradients and color effects along the line. | |||||
| - **UI integration:** Easily expose line and spiral parameters for live editing. | |||||
| - **Performance:** Efficiently renders thousands of segments as a single mesh. | |||||
| ## How Fat Lines Work Under the Hood | |||||
| ThreePipe's `MeshLine` is built on top of the three.js [Line2](https://threejs.org/docs/#examples/en/lines/Line2) addon. Instead of drawing a single-pixel line, it creates a mesh strip along the path, allowing for thick, anti-aliased lines. The geometry stores positions and colors, and the material handles width, color, and other effects. This approach works in all modern browsers and supports advanced features like dashes and transparency. | |||||
| ## When to Use `MeshLine` | |||||
| Use fat lines when you need: | |||||
| - Outlines, paths, or polylines with variable thickness | |||||
| - Technical illustrations or stylized effects | |||||
| - Color gradients or dashes along a line | |||||
| - Interactive geometry that updates in real time | |||||
| - Using as a viewer (not exporting as glTF) | |||||
| For simple, single-pixel lines, the default three.js `Line` is still fastest, but for anything more advanced, `MeshLine` is the way to go. | |||||
| ## Using with glTF files | |||||
| When importing glTF files with embedded line geometries and materials, `GLTFLoader2.UseMeshLines` feature can be used to automatically convert those lines to `MeshLine` instances. This allows you to take advantage of the advanced line rendering capabilities without needing to modify the original GLTF files. | |||||
| Checkout the article on [GLTF Mesh Lines](https://threepipe.org/notes/gltf-mesh-lines) for more details on how to use this feature. |
| --- | |||||
| prev: | |||||
| text: 'Fat Lines' | |||||
| link: './fat-lines' | |||||
| next: false | |||||
| aside: false | |||||
| --- | |||||
| # GLTF Mesh Lines: Fat Lines in glTF | |||||
| [GLTF Mesh Lines Example](https://threepipe.org/examples/#gltf-mesh-lines/) | |||||
| <iframe src="https://threepipe.org/examples/gltf-mesh-lines/" style="width:100%;min-height:600px;border:none;" loading="lazy" title="GLTF Mesh Lines Example"></iframe> | |||||
| See Also: [Fat Lines Example](https://threepipe.org/examples/#fat-lines/) | |||||
| See Also: [Fat Line Spiral Example](https://threepipe.org/examples/#fat-line-spiral/) | |||||
| If you've ever tried to render thick ("fat") lines in three.js, especially when importing models from GLTF files, you know it can be surprisingly tricky. By default, three.js only supports 1-pixel wide lines due to WebGL limitations. This makes it difficult to achieve visually appealing, stylized, or technical line renderings directly from imported models. | |||||
| The standard `THREE.Line` and `THREE.LineSegments` classes rely on basic WebGL line rendering, which is limited to a width of 1 pixel on most platforms. As a result, creating thick lines for wireframes, outlines, or technical illustrations is not possible out of the box. | |||||
| To work around this, three.js provides the [Line2, LineMaterial](https://threejs.org/docs/#examples/en/lines/Line2) classes in three.js addons. | |||||
| These render lines as thin meshes, allowing for customizable widths, colors, and other properties. | |||||
| The `threepipe` framework extends this further by providing an option to automatically use these advanced line types for all lines imported from GLTF files. | |||||
| You can enable this feature globally by setting: | |||||
| ```ts | |||||
| GLTFLoader2.UseMeshLines = true; | |||||
| ``` | |||||
| ::: danger WARNING | |||||
| Lines imported this way may not export correctly when using the glTF exporter, as the mesh-based lines are not standard GLTF lines. This may change in later versions of three.js or threepipe. | |||||
| Only enable the feature when using `threepipe` as a viewer. | |||||
| ::: | |||||
| Once the feature is enabled, glTF files with lines will be imported using `Line2` and `LineMaterial2` instead of the default line classes. | |||||
| This can be controlled per-glTF file during load with the `useMeshLines` option: | |||||
| ```ts | |||||
| const obj = await viewer.load('model.gltf', { | |||||
| useMeshLines: false, | |||||
| }); | |||||
| ``` | |||||
| When enabled, all lines in the imported GLTF will use `Line2`/`LineMaterial2`, allowing you to set properties like `linewidth` (yes, it's all lowercase) | |||||
| ```ts | |||||
| material.linewidth = 10; | |||||
| ``` | |||||
| > **Note:** The resolution of the line is set automatically based on the render size, so `linewidth` is always in pixels. If you want the line width to attenuate with distance (i.e., be in world units), you can set the `worldUnits` flag on the material to `true`. | |||||
| ## API Documentation | |||||
| - `MeshLine` - https://threepipe.org/docs/classes/MeshLine.html | |||||
| - `MeshLineSegments` - https://threepipe.org/docs/classes/MeshLineSegments.html | |||||
| - `LineMaterial2`/`MeshLineMaterial` - https://threepipe.org/docs/classes/LineMaterial2.html | |||||
| - `GLTFLoader2` - https://threepipe.org/docs/classes/GLTFLoader2.html | |||||
| - three.js `LineMaterial` - https://threejs.org/docs/?q=line#examples/en/lines/LineMaterial | |||||
| - three.js `Line2` - https://threejs.org/docs/?q=line#examples/en/lines/Line2 |