浏览代码

Nodes: Line2 (fat lines) (#26704)

* line2 implementation

* fix code errors

* export accessors and code style

* use set/get for setters/getters

---------

Co-authored-by: aardgoose <[email protected]>
aardgoose 1 年之前
父节点
当前提交
11f4ca78ea

+ 1 - 0
examples/files.json

@@ -326,6 +326,7 @@
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_ies_spotlight",
 		"webgpu_lights_phong",
 		"webgpu_lights_phong",
 		"webgpu_lights_selective",
 		"webgpu_lights_selective",
+		"webgpu_lines_fat",
 		"webgpu_loader_gltf",
 		"webgpu_loader_gltf",
 		"webgpu_loader_gltf_compressed",
 		"webgpu_loader_gltf_compressed",
 		"webgpu_loader_gltf_iridescence",
 		"webgpu_loader_gltf_iridescence",

+ 1 - 0
examples/jsm/nodes/Nodes.js

@@ -71,6 +71,7 @@ export { default as CameraNode, cameraProjectionMatrix, cameraViewMatrix, camera
 export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js';
 export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js';
 export { default as ExtendedMaterialNode, materialNormal } from './accessors/ExtendedMaterialNode.js';
 export { default as ExtendedMaterialNode, materialNormal } from './accessors/ExtendedMaterialNode.js';
 export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
 export { default as InstanceNode, instance } from './accessors/InstanceNode.js';
+export { default as LineMaterialNode, materialLineDashSize, materialLineDashOffset, materialLineGapSize, materialLineScale, materialLineWidth } from './accessors/LineMaterialNode.js';
 export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialReflectivity, materialRoughness, materialMetalness, materialRotation, materialSheen, materialSheenRoughness } from './accessors/MaterialNode.js';
 export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialReflectivity, materialRoughness, materialMetalness, materialRotation, materialSheen, materialSheenRoughness } from './accessors/MaterialNode.js';
 export { default as MaterialReferenceNode, materialReference } from './accessors/MaterialReferenceNode.js';
 export { default as MaterialReferenceNode, materialReference } from './accessors/MaterialReferenceNode.js';
 export { default as MorphNode, morph } from './accessors/MorphNode.js';
 export { default as MorphNode, morph } from './accessors/MorphNode.js';

+ 4 - 0
examples/jsm/nodes/accessors/LineMaterialNode.js

@@ -15,11 +15,15 @@ class LineMaterialNode extends MaterialNode {
 LineMaterialNode.SCALE = 'scale';
 LineMaterialNode.SCALE = 'scale';
 LineMaterialNode.DASH_SIZE = 'dashSize';
 LineMaterialNode.DASH_SIZE = 'dashSize';
 LineMaterialNode.GAP_SIZE = 'gapSize';
 LineMaterialNode.GAP_SIZE = 'gapSize';
+LineMaterialNode.LINEWIDTH = 'linewidth';
+LineMaterialNode.DASH_OFFSET = 'dashOffset';
 
 
 export default LineMaterialNode;
 export default LineMaterialNode;
 
 
 export const materialLineScale = nodeImmutable( LineMaterialNode, LineMaterialNode.SCALE );
 export const materialLineScale = nodeImmutable( LineMaterialNode, LineMaterialNode.SCALE );
+export const materialLineDashOffset = nodeImmutable( LineMaterialNode, LineMaterialNode.DASH_OFFSET );
 export const materialLineDashSize = nodeImmutable( LineMaterialNode, LineMaterialNode.DASH_SIZE );
 export const materialLineDashSize = nodeImmutable( LineMaterialNode, LineMaterialNode.DASH_SIZE );
 export const materialLineGapSize = nodeImmutable( LineMaterialNode, LineMaterialNode.GAP_SIZE );
 export const materialLineGapSize = nodeImmutable( LineMaterialNode, LineMaterialNode.GAP_SIZE );
+export const materialLineWidth = nodeImmutable( LineMaterialNode, LineMaterialNode.LINEWIDTH );
 
 
 addNodeClass( LineMaterialNode );
 addNodeClass( LineMaterialNode );

+ 449 - 0
examples/jsm/nodes/materials/Line2NodeMaterial.js

@@ -0,0 +1,449 @@
+import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js';
+import { temp } from '../core/VarNode.js';
+import { varying } from '../core/VaryingNode.js';
+import { property } from '../core/PropertyNode.js';
+import { attribute } from '../core/AttributeNode.js';
+import { cameraProjectionMatrix } from '../accessors/CameraNode.js';
+import { materialColor } from '../accessors/MaterialNode.js';
+import { modelViewMatrix } from '../accessors/ModelNode.js';
+import { positionGeometry } from '../accessors/PositionNode.js';
+import { abs, mix, mod, dot, clamp, smoothstep } from '../math/MathNode.js';
+import { tslFn, ShaderNode, float, vec2, vec3, vec4 } from '../shadernode/ShaderNode.js';
+import { uv } from '../accessors/UVNode.js';
+import { materialLineScale, materialLineDashSize, materialLineGapSize, materialLineDashOffset, materialLineWidth } from '../accessors/LineMaterialNode.js';
+import { viewport } from '../display/ViewportNode.js';
+import { dashSize, gapSize } from '../core/PropertyNode.js';
+
+import { LineDashedMaterial } from 'three';
+
+const defaultValues = new LineDashedMaterial();
+
+class Line2NodeMaterial extends NodeMaterial {
+
+	constructor( params = {} ) {
+
+		super();
+
+		this.normals = false;
+		this.lights = false;
+
+		this.setDefaultValues( defaultValues );
+
+		this.useAlphaToCoverage = true;
+		this.useColor = params.vertexColors;
+		this.useDash = params.dashed;
+		this.useWorldUnits = false;
+
+		this.dashOffset = 0;
+		this.lineWidth = 1;
+
+		this.lineColorNode = null;
+
+		this.offsetNode = null;
+		this.dashScaleNode = null;
+		this.dashSizeNode = null;
+		this.gapSizeNode = null;
+
+		this.constructShaders();
+
+		this.setValues( params );
+
+	}
+
+	constructShaders() {
+
+		const useAlphaToCoverage = this.alphaToCoverage;
+		const useColor = this.useColor;
+		const useDash = this.dashed;
+		const useWorldUnits = this.worldUnits;
+
+		const trimSegment = tslFn( ( { start, end } ) => {
+
+			const a = cameraProjectionMatrix.element( 2 ).element( 2 ); // 3nd entry in 3th column
+			const b = cameraProjectionMatrix.element( 3 ).element( 2 ); // 3nd entry in 4th column
+			const nearEstimate = b.mul( -0.5 ).div( a );
+
+			const alpha = nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );
+
+			return vec4( mix( start.xyz, end.xyz, alpha ), end.w );
+
+		} );
+
+		this.vertexNode = new ShaderNode( ( stack ) => {
+
+			stack.assign( varying( vec2(), 'vUv' ), uv() );
+
+			const instanceStart = attribute( 'instanceStart' );
+			const instanceEnd = attribute( 'instanceEnd' );
+
+			// camera space
+
+			const start = property( 'vec4', 'start' );
+			const end = property( 'vec4', 'end' );
+
+			stack.assign( start, modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ); // force assignment into correct place in flow
+			stack.assign( end, modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) );
+
+
+			if ( useWorldUnits ) {
+
+				stack.assign( varying( vec3(), 'worldStart' ), start.xyz );
+				stack.assign( varying( vec3(), 'worldEnd' ), end.xyz );
+
+			}
+
+			const aspect = viewport.z.div( viewport.w );
+
+			// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
+			// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
+			// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
+			// perhaps there is a more elegant solution -- WestLangley
+
+			const perspective = cameraProjectionMatrix.element( 2 ).element( 3 ).equal( -1.0 ); // 4th entry in the 3rd column
+
+			stack.if( perspective, ( stack ) => {
+
+				stack.if( start.z.lessThan( 0.0 ).and( end.z.greaterThan( 0.0 ) ), ( stack ) => {
+
+					stack.assign( end, trimSegment( { start: start, end: end } ) );
+
+				} ).elseif( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), ( stack ) => {
+
+					stack.assign( start, trimSegment( { start: end, end: start } ) );
+
+			 	} );
+
+			} );
+
+			// clip space
+			const clipStart = cameraProjectionMatrix.mul( start );
+			const clipEnd = cameraProjectionMatrix.mul( end );
+
+			// ndc space
+			const ndcStart = clipStart.xyz.div( clipStart.w );
+			const ndcEnd = clipEnd.xyz.div( clipEnd.w );
+
+			// direction
+			const dir = ndcEnd.xy.sub( ndcStart.xy );
+
+			// account for clip-space aspect ratio
+			stack.assign( dir.x, dir.x.mul( aspect ) );
+			stack.assign( dir, dir.normalize() );
+
+			const clip = temp( vec4() );
+
+			if ( useWorldUnits ) {
+
+				// get the offset direction as perpendicular to the view vector
+				const worldDir = end.xyz.sub( start.xyz ).normalize();
+
+				const offset = positionGeometry.y.lessThan( 0.5 ).cond(
+					start.xyz.cross( worldDir ).normalize(),
+					end.xyz.cross( worldDir ).normalize()
+
+				);
+
+				// sign flip
+				stack.assign( offset, positionGeometry.x.lessThan( 0.0 ).cond( offset.negate(), offset ) );
+
+				const forwardOffset = worldDir.dot( vec3( 0.0, 0.0, 1.0 ) );
+
+				// don't extend the line if we're rendering dashes because we
+				// won't be rendering the endcaps
+				if ( ! useDash ) {
+
+					// extend the line bounds to encompass endcaps
+					stack.assign( start, start.sub( vec4( worldDir.mul( materialLineWidth ).mul( 0.5 ), 0 ) ) );
+					stack.assign( end, end.add( vec4( worldDir.mul( materialLineWidth ).mul( 0.5 ), 0 ) ) );
+
+					// shift the position of the quad so it hugs the forward edge of the line
+					stack.assign( offset, offset.sub( vec3( dir.mul( forwardOffset ), 0 ) ) );
+					stack.assign( offset.z, offset.z.add( 0.5 ) );
+
+				}
+
+				// endcaps
+
+				stack.if( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), ( stack ) => {
+
+					stack.assign( offset, offset.add( vec3( dir.mul( 2.0 ).mul( forwardOffset ), 0 ) ) );
+
+				} );
+
+				// adjust for linewidth
+				stack.assign( offset, offset.mul( materialLineWidth ).mul( 0.5 ) );
+
+				// set the world position
+
+				const worldPos = varying( vec4(), 'worldPos' );
+
+				stack.assign( worldPos, positionGeometry.y.lessThan( 0.5 ).cond( start, end ) );
+				stack.assign( worldPos, worldPos.add( vec4( offset, 0 ) ) );
+
+				// project the worldpos
+				stack.assign( clip, cameraProjectionMatrix.mul( worldPos ) );
+
+				// shift the depth of the projected points so the line
+				// segments overlap neatly
+				const clipPose = temp( vec3() );
+
+				stack.assign( clipPose, positionGeometry.y.lessThan( 0.5 ).cond( ndcStart, ndcEnd ) );
+				stack.assign( clip.z, clipPose.z.mul( clip.w ) );
+
+			} else {
+
+				const offset = property( 'vec2', 'offset' );
+
+				stack.assign( offset, vec2( dir.y, dir.x.negate() ) );
+
+				// undo aspect ratio adjustment
+				stack.assign( dir.x, dir.x.div( aspect ) );
+				stack.assign( offset.x, offset.x.div( aspect ) );
+
+				// sign flip
+				stack.assign( offset, positionGeometry.x.lessThan( 0.0 ).cond( offset.negate(), offset ) );
+
+				// endcaps
+				stack.if( positionGeometry.y.lessThan( 0.0 ), ( stack ) => {
+
+					stack.assign( offset, offset.sub( dir ) );
+
+				} ).elseif( positionGeometry.y.greaterThan( 1.0 ), ( stack ) => {
+
+					stack.assign( offset, offset.add( dir ) );
+
+				} );
+
+				// adjust for linewidth
+				stack.assign( offset, offset.mul( materialLineWidth ) );
+
+				// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
+				stack.assign( offset, offset.div( viewport.w ) );
+
+				// select end
+				stack.assign( clip, positionGeometry.y.lessThan( 0.5 ).cond( clipStart, clipEnd ) );
+
+				// back to clip space
+				stack.assign( offset, offset.mul( clip.w ) );
+
+				stack.assign( clip, clip.add( vec4( offset, 0, 0 ) ) );
+
+			}
+
+			return clip;
+
+		} );
+
+		const closestLineToLine = tslFn( ( { p1, p2, p3, p4 } ) => {
+
+			const p13 = p1.sub( p3 );
+			const p43 = p4.sub( p3 );
+
+			const p21 = p2.sub( p1 );
+
+			const d1343 = dot( p13, p43 );
+			const d4321 = dot( p43, p21 );
+			const d1321 = dot( p13, p21 );
+			const d4343 = dot( p43, p43 );
+			const d2121 = dot( p21, p21 );
+
+			const denom = d2121.mul( d4343 ).sub( d4321.mul( d4321 ) );
+			const numer = d1343.mul( d4321 ).sub( d1321.mul( d4343 ) );
+
+			const mua = clamp( numer.div( denom ), 0, 1 );
+			const mub = clamp( d1343.add( d4321.mul( mua ) ).div( d4343 ), 0, 1 );
+
+			return vec2( mua, mub );
+
+		} );
+
+		this.colorNode = new ShaderNode( ( stack ) => {
+
+			const vUv = varying( vec2(), 'vUv' );
+
+			if ( useDash ) {
+
+				const offsetNode = this.offsetNode ? float( this.offsetNodeNode ) : materialLineDashOffset;
+				const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
+				const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
+				const gapSizeNode = this.dashSizeNode ? float( this.dashGapNode ) : materialLineGapSize;
+
+				stack.assign( dashSize, dashSizeNode );
+				stack.assign( gapSize, gapSizeNode );
+
+				const instanceDistanceStart = attribute( 'instanceDistanceStart' );
+				const instanceDistanceEnd = attribute( 'instanceDistanceEnd' );
+
+				const lineDistance = positionGeometry.y.lessThan( 0.5 ).cond( dashScaleNode.mul( instanceDistanceStart ), materialLineScale.mul( instanceDistanceEnd ) );
+
+				const vLineDistance = varying( lineDistance.add( materialLineDashOffset ) );
+				const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance;
+
+				stack.add( vUv.y.lessThan( - 1.0 ).or( vUv.y.greaterThan( 1.0 ) ).discard() ); // discard endcaps
+				stack.add( mod( vLineDistanceOffset, dashSize.add( gapSize ) ).greaterThan( dashSize ).discard() ); // todo - FIX
+
+			}
+
+			 // force assignment into correct place in flow
+			const alpha = property( 'float', 'alpha' );
+			stack.assign( alpha, 1 );
+
+			if ( useWorldUnits ) {
+
+
+				let worldStart = varying( vec3(), 'worldStart' );
+				let worldEnd = varying( vec3(), 'worldEnd' );
+
+				// Find the closest points on the view ray and the line segment
+				const rayEnd = varying( vec4(), 'worldPos' ).xyz.normalize().mul( 1e5 );
+				const lineDir = worldEnd.sub( worldStart );
+				const params = closestLineToLine( { p1: worldStart, p2: worldEnd, p3: vec3( 0.0, 0.0, 0.0 ), p4: rayEnd } );
+
+				const p1 = worldStart.add( lineDir.mul( params.x ) );
+				const p2 = rayEnd.mul( params.y );
+				const delta = p1.sub( p2 );
+				const len = delta.length();
+				const norm = len.div( materialLineWidth );
+
+				if ( ! useDash ) {
+
+					if ( useAlphaToCoverage ) {
+
+						const dnorm = norm.fwidth();
+						stack.assign( alpha, smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
+
+					} else {
+
+						stack.add( norm.greaterThan( 0.5 ).discard() );
+
+					}
+
+				}
+
+			} else {
+
+				// round endcaps
+
+				if ( useAlphaToCoverage ) {
+
+					const a = vUv.x;
+					const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
+
+					const len2 = a.mul( a ).add( b.mul( b ) );
+
+					// force assignment out of following 'if' statement - to avoid uniform control flow errors
+					const dlen = property( 'float', 'dlen' );
+					stack.assign( dlen, len2.fwidth() );
+
+					stack.if( abs( vUv.y ).greaterThan( 1.0 ), ( stack ) => {
+
+						stack.assign( alpha, smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
+
+					} );
+
+				} else {
+
+					stack.if( abs( vUv.y ).greaterThan( 1.0 ), ( stack ) => {
+
+						const a = vUv.x;
+						const b = vUv.y.greaterThan( 0.0 ).cond( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
+						const len2 = a.mul( a ).add( b.mul( b ) );
+
+						stack.add( len2.greaterThan( 1.0 ).discard() );
+
+					} );
+
+				}
+
+			}
+
+			let lineColorNode;
+
+			if ( this.lineColorNode ) {
+
+				lineColorNode = this.lineColorNode;
+
+			} else {
+
+				if ( useColor ) {
+
+					const instanceColorStart = attribute( 'instanceColorStart' );
+					const instanceColorEnd = attribute( 'instanceColorEnd' );
+
+					lineColorNode = varying( positionGeometry.y.lessThan( 0.5 ).cond( instanceColorStart, instanceColorEnd ) );
+
+				} else {
+
+					lineColorNode = materialColor;
+
+				}
+
+			}
+
+			return vec4( lineColorNode, alpha );
+
+		} );
+
+		this.needsUpdate = true;
+
+	}
+
+
+	get worldUnits() {
+
+		return this.useWorldUnits;
+
+	}
+
+	set worldUnits( value ) {
+
+		if ( this.useWorldUnits !== value ) {
+
+			this.useWorldUnits = value;
+			this.constructShaders();
+
+		}
+
+	}
+
+
+	get dashed() {
+
+		return this.useDash;
+
+	}
+
+	set dashed( value ) {
+
+		if ( this.useDash !== value ) {
+
+			this.useDash = value;
+			this.constructShaders();
+
+		}
+
+	}
+
+
+	get alphaToCoverage() {
+
+		return this.useAlphaToCoverage;
+
+	}
+
+	set alphaToCoverage( value ) {
+
+		if ( this.useAlphaToCoverage !== value ) {
+
+			this.useAlphaToCoverage = value;
+			this.constructShaders();
+
+		}
+
+	}
+
+}
+
+export default Line2NodeMaterial;
+
+addNodeMaterial( Line2NodeMaterial );

+ 1 - 0
examples/jsm/nodes/materials/Materials.js

@@ -3,6 +3,7 @@
 export { default as NodeMaterial, addNodeMaterial, createNodeMaterialFromType } from './NodeMaterial.js';
 export { default as NodeMaterial, addNodeMaterial, createNodeMaterialFromType } from './NodeMaterial.js';
 export { default as LineBasicNodeMaterial } from './LineBasicNodeMaterial.js';
 export { default as LineBasicNodeMaterial } from './LineBasicNodeMaterial.js';
 export { default as LineDashedNodeMaterial } from './LineDashedNodeMaterial.js';
 export { default as LineDashedNodeMaterial } from './LineDashedNodeMaterial.js';
+export { default as Line2NodeMaterial } from './Line2NodeMaterial.js';
 export { default as MeshNormalNodeMaterial } from './MeshNormalNodeMaterial.js';
 export { default as MeshNormalNodeMaterial } from './MeshNormalNodeMaterial.js';
 export { default as MeshBasicNodeMaterial } from './MeshBasicNodeMaterial.js';
 export { default as MeshBasicNodeMaterial } from './MeshBasicNodeMaterial.js';
 export { default as MeshLambertNodeMaterial } from './MeshLambertNodeMaterial.js';
 export { default as MeshLambertNodeMaterial } from './MeshLambertNodeMaterial.js';

二进制
examples/screenshots/webgpu_lines_fat.jpg


+ 334 - 0
examples/webgpu_lines_fat.html

@@ -0,0 +1,334 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgpu - lines - fat</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+
+	<body>
+
+		<div id="container"></div>
+
+		<div id="info"><a href="https://threejs.org" target="_blank">three.js</a> - fat lines</div>
+
+		<!-- Import maps polyfill -->
+		<!-- Remove this when import maps will be widely supported -->
+		<script async src="https://unpkg.com/[email protected]/dist/es-module-shims.js"></script>
+
+		<script type="importmap">
+			{
+				"imports": {
+					"three": "../build/three.module.js",
+					"three/addons/": "./jsm/",
+					"three/nodes": "./jsm/nodes/Nodes.js"
+				}
+			}
+		</script>
+
+		<script type="module">
+
+			import * as THREE from 'three';
+
+			import WebGPU from 'three/addons/capabilities/WebGPU.js';
+			import WebGPURenderer from 'three/addons/renderers/webgpu/WebGPURenderer.js';
+
+			import Stats from 'three/addons/libs/stats.module.js';
+
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
+			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { Line2NodeMaterial, LineDashedNodeMaterial, LineBasicNodeMaterial, color } from 'three/nodes';
+			import { Line2 } from 'three/addons/lines/Line2.js';
+			import { LineGeometry } from 'three/addons/lines/LineGeometry.js';
+			import * as GeometryUtils from 'three/addons/utils/GeometryUtils.js';
+
+			let line, renderer, scene, camera, camera2, controls, backgroundNode;
+			let line1;
+			let matLine, matLineBasic, matLineDashed;
+			let stats;
+			let gui;
+
+			// viewport
+			let insetWidth;
+			let insetHeight;
+
+			init();
+			animate();
+
+			function init() {
+
+				if ( WebGPU.isAvailable() === false ) {
+
+					document.body.appendChild( WebGPU.getErrorMessage() );
+
+					throw new Error( 'No WebGPU support' );
+
+				}
+
+				renderer = new WebGPURenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setClearColor( 0x000000, 0.0 );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				scene = new THREE.Scene();
+
+				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.set( - 40, 0, 60 );
+
+				camera2 = new THREE.PerspectiveCamera( 40, 1, 1, 1000 );
+				camera2.position.copy( camera.position );
+
+				controls = new OrbitControls( camera, renderer.domElement );
+				controls.minDistance = 10;
+				controls.maxDistance = 500;
+
+				backgroundNode = color( 0x222222 );
+
+				// Position and THREE.Color Data
+
+				const positions = [];
+				const colors = [];
+
+				const points = GeometryUtils.hilbert3D( new THREE.Vector3( 0, 0, 0 ), 20.0, 1, 0, 1, 2, 3, 4, 5, 6, 7 );
+
+				const spline = new THREE.CatmullRomCurve3( points );
+				const divisions = Math.round( 12 * points.length );
+				const point = new THREE.Vector3();
+				const lineColor = new THREE.Color();
+
+				for ( let i = 0, l = divisions; i < l; i ++ ) {
+
+					const t = i / l;
+
+					spline.getPoint( t, point );
+					positions.push( point.x, point.y, point.z );
+
+					lineColor.setHSL( t, 1.0, 0.5, THREE.SRGBColorSpace );
+					colors.push( lineColor.r, lineColor.g, lineColor.b );
+
+				}
+
+
+				// Line2 ( LineGeometry, LineMaterial )
+
+				const geometry = new LineGeometry();
+				geometry.setPositions( positions );
+				geometry.setColors( colors );
+				geometry.instanceCount = positions.length / 3 - 1;
+
+				matLine = new Line2NodeMaterial( {
+
+					color: 0xffffff,
+					linewidth: 5, // in world units with size attenuation, pixels otherwise
+					vertexColors: true,
+					dashed: false,
+					alphaToCoverage: true,
+
+				} );
+
+				line = new Line2( geometry, matLine );
+				line.computeLineDistances();
+				line.scale.set( 1, 1, 1 );
+				scene.add( line );
+
+				const geo = new THREE.BufferGeometry();
+				geo.setAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
+				geo.setAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
+
+				matLineBasic = new LineBasicNodeMaterial( { vertexColors: true } );
+				matLineDashed = new LineDashedNodeMaterial( { vertexColors: true, scale: 2, dashSize: 1, gapSize: 1 } );
+
+				line1 = new THREE.Line( geo, matLineBasic );
+				line1.computeLineDistances();
+				line1.visible = false;
+				scene.add( line1 );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize );
+				onWindowResize();
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				initGui();
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+				insetWidth = window.innerHeight / 4; // square
+				insetHeight = window.innerHeight / 4;
+
+				camera2.aspect = insetWidth / insetHeight;
+				camera2.updateProjectionMatrix();
+
+			}
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				stats.update();
+
+				// main scene
+
+				renderer.setClearColor( 0x000000, 0 );
+
+				renderer.setViewport( 0, 0, window.innerWidth, window.innerHeight );
+
+				renderer.autoClear = true;
+
+				scene.backgroundNode = null;
+				renderer.render( scene, camera );
+
+				// inset scene
+
+				renderer.clearDepth(); // important!
+
+				renderer.setScissorTest( true );
+
+				renderer.setScissor( 20, window.innerHeight - insetHeight - 20, insetWidth, insetHeight );
+
+				renderer.setViewport( 20, window.innerHeight - insetHeight - 20, insetWidth, insetHeight );
+
+				camera2.position.copy( camera.position );
+				camera2.quaternion.copy( camera.quaternion );
+
+				renderer.autoClear = false;
+
+				scene.backgroundNode = backgroundNode;
+				renderer.render( scene, camera2 );
+
+				renderer.setScissorTest( false );
+
+			}
+
+			//
+
+			function initGui() {
+
+				gui = new GUI();
+
+				const param = {
+					'line type': 0,
+					'world units': false,
+					'width': 5,
+					'alphaToCoverage': true,
+					'dashed': false,
+					'dash offset': 0,
+					'dash scale': 1,
+					'dash / gap': 1
+				};
+
+				gui.add( param, 'line type', { 'LineGeometry': 0, '"line-strip"': 1 } ).onChange( function ( val ) {
+
+					switch ( val ) {
+
+						case 0:
+							line.visible = true;
+
+							line1.visible = false;
+
+							break;
+
+						case 1:
+							line.visible = false;
+
+							line1.visible = true;
+
+							break;
+
+					}
+
+				} );
+
+				gui.add( param, 'world units' ).onChange( function ( val ) {
+
+					matLine.worldUnits = val;
+					matLine.needsUpdate = true;
+
+				} );
+
+				gui.add( param, 'width', 1, 10 ).onChange( function ( val ) {
+
+					matLine.linewidth = val;
+
+				} );
+
+				gui.add( param, 'alphaToCoverage' ).onChange( function ( val ) {
+
+					matLine.alphaToCoverage = val;
+
+				} );
+
+				gui.add( param, 'dashed' ).onChange( function ( val ) {
+
+					matLine.dashed = val;
+					line1.material = val ? matLineDashed : matLineBasic;
+
+				} );
+
+				gui.add( param, 'dash scale', 0.5, 2, 0.1 ).onChange( function ( val ) {
+
+					matLine.scale = val;
+					matLineDashed.scale = val;
+
+				} );
+
+				gui.add( param, 'dash offset', 0, 5, 0.1 ).onChange( function ( val ) {
+
+					matLine.dashOffset = val;
+					matLineDashed.dashOffset = val;
+
+				} );
+
+				gui.add( param, 'dash / gap', { '2 : 1': 0, '1 : 1': 1, '1 : 2': 2 } ).onChange( function ( val ) {
+
+					switch ( val ) {
+
+						case 0:
+							matLine.dashSize = 2;
+							matLine.gapSize = 1;
+
+							matLineDashed.dashSize = 2;
+							matLineDashed.gapSize = 1;
+
+							break;
+
+						case 1:
+							matLine.dashSize = 1;
+							matLine.gapSize = 1;
+
+							matLineDashed.dashSize = 1;
+							matLineDashed.gapSize = 1;
+
+							break;
+
+						case 2:
+							matLine.dashSize = 1;
+							matLine.gapSize = 2;
+
+							matLineDashed.dashSize = 1;
+							matLineDashed.gapSize = 2;
+
+							break;
+
+					}
+
+				} );
+
+			}
+
+		</script>
+
+	</body>
+
+</html>

+ 1 - 0
test/e2e/puppeteer.js

@@ -117,6 +117,7 @@ const exceptionList = [
 	'webgpu_depth_texture',
 	'webgpu_depth_texture',
 	'webgpu_instance_mesh',
 	'webgpu_instance_mesh',
 	'webgpu_lights_ies_spotlight',
 	'webgpu_lights_ies_spotlight',
+	'webgpu_lines_fat',
 	'webgpu_loader_gltf',
 	'webgpu_loader_gltf',
 	'webgpu_loader_gltf_compressed',
 	'webgpu_loader_gltf_compressed',
 	'webgpu_loader_gltf_iridescence',
 	'webgpu_loader_gltf_iridescence',