[3d] add lineart mode (#2800)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Terry Jia
2025-03-02 10:48:23 -05:00
committed by GitHub
parent 699ebe2f93
commit b1713b4c80
19 changed files with 1234 additions and 22 deletions

View File

@@ -1,6 +1,15 @@
import * as THREE from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry'
import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils'
import { ColoredShadowMaterial } from './conditional-lines/ColoredShadowMaterial'
import { ConditionalEdgesGeometry } from './conditional-lines/ConditionalEdgesGeometry'
import { ConditionalEdgesShader } from './conditional-lines/ConditionalEdgesShader.js'
import { ConditionalLineMaterial } from './conditional-lines/Lines2/ConditionalLineMaterial'
import { ConditionalLineSegmentsGeometry } from './conditional-lines/Lines2/ConditionalLineSegmentsGeometry'
import {
EventManagerInterface,
MaterialMode,
@@ -10,7 +19,12 @@ import {
export class ModelManager implements ModelManagerInterface {
currentModel: THREE.Object3D | null = null
originalModel: THREE.Object3D | THREE.BufferGeometry | GLTF | null = null
originalModel:
| THREE.Object3D
| THREE.Group
| THREE.BufferGeometry
| GLTF
| null = null
originalRotation: THREE.Euler | null = null
currentUpDirection: UpDirection = 'original'
materialMode: MaterialMode = 'original'
@@ -27,6 +41,15 @@ export class ModelManager implements ModelManagerInterface {
private activeCamera: THREE.Camera
private setupCamera: (size: THREE.Vector3) => void
LIGHT_MODEL = 0xffffff
LIGHT_LINES = 0x455a64
conditionalModel: THREE.Object3D | null = null
edgesModel: THREE.Object3D | null = null
backgroundModel: THREE.Object3D | null = null
shadowModel: THREE.Object3D | null = null
depthModel: THREE.Object3D | null = null
constructor(
scene: THREE.Scene,
renderer: THREE.WebGLRenderer,
@@ -71,6 +94,8 @@ export class ModelManager implements ModelManagerInterface {
this.standardMaterial.dispose()
this.wireframeMaterial.dispose()
this.depthMaterial.dispose()
this.disposeLineartModel()
}
createSTLMaterial(): THREE.MeshStandardMaterial {
@@ -83,10 +108,271 @@ export class ModelManager implements ModelManagerInterface {
})
}
disposeLineartModel(): void {
this.disposeEdgesModel()
this.disposeShadowModel()
this.disposeBackgroundModel()
this.disposeDepthModel()
this.disposeConditionalModel()
}
disposeEdgesModel(): void {
if (this.edgesModel) {
if (this.edgesModel.parent) {
this.edgesModel.parent.remove(this.edgesModel)
}
this.edgesModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
if (Array.isArray(child.material)) {
child.material.forEach((m) => m.dispose())
} else {
child.material.dispose()
}
}
})
}
}
initEdgesModel() {
this.disposeEdgesModel()
if (!this.currentModel) {
return
}
this.edgesModel = this.currentModel.clone()
this.scene.add(this.edgesModel)
const meshes: THREE.Mesh[] = []
this.edgesModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child)
}
})
for (const key in meshes) {
const mesh = meshes[key]
const parent = mesh.parent
let lineGeom = new THREE.EdgesGeometry(mesh.geometry, 10)
const line = new THREE.LineSegments(
lineGeom,
new THREE.LineBasicMaterial({ color: this.LIGHT_LINES })
)
line.position.copy(mesh.position)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
const thickLineGeom = new LineSegmentsGeometry().fromEdgesGeometry(
lineGeom
)
const thickLines = new LineSegments2(
thickLineGeom,
new LineMaterial({ color: this.LIGHT_LINES, linewidth: 13 })
)
thickLines.position.copy(mesh.position)
thickLines.scale.copy(mesh.scale)
thickLines.rotation.copy(mesh.rotation)
parent?.remove(mesh)
parent?.add(line)
parent?.add(thickLines)
}
}
disposeBackgroundModel(): void {
if (this.backgroundModel) {
if (this.backgroundModel.parent) {
this.backgroundModel.parent.remove(this.backgroundModel)
}
this.backgroundModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeShadowModel(): void {
if (this.shadowModel) {
if (this.shadowModel.parent) {
this.shadowModel.parent.remove(this.shadowModel)
}
this.shadowModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeDepthModel(): void {
if (this.depthModel) {
if (this.depthModel.parent) {
this.depthModel.parent.remove(this.depthModel)
}
this.depthModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
disposeConditionalModel(): void {
if (this.conditionalModel) {
if (this.conditionalModel.parent) {
this.conditionalModel.parent.remove(this.conditionalModel)
}
this.conditionalModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.dispose()
}
})
}
}
initBackgroundModel() {
this.disposeBackgroundModel()
this.disposeShadowModel()
this.disposeDepthModel()
if (!this.currentModel) {
return
}
this.backgroundModel = this.currentModel.clone()
this.backgroundModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshBasicMaterial({
color: this.LIGHT_MODEL
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.renderOrder = 2
}
})
this.scene.add(this.backgroundModel)
this.shadowModel = this.currentModel.clone()
this.shadowModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new ColoredShadowMaterial({
color: this.LIGHT_MODEL,
shininess: 1.0
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.receiveShadow = true
child.renderOrder = 2
}
})
this.scene.add(this.shadowModel)
this.depthModel = this.currentModel.clone()
this.depthModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = new THREE.MeshBasicMaterial({
color: this.LIGHT_MODEL
})
child.material.polygonOffset = true
child.material.polygonOffsetFactor = 1
child.material.polygonOffsetUnits = 1
child.material.colorWrite = false
child.renderOrder = 1
}
})
this.scene.add(this.depthModel)
}
initConditionalModel() {
this.disposeConditionalModel()
if (!this.currentModel) {
return
}
this.conditionalModel = this.currentModel.clone()
this.scene.add(this.conditionalModel)
this.conditionalModel.visible = true
const meshes: THREE.Mesh[] = []
this.conditionalModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
meshes.push(child)
}
})
for (const key in meshes) {
const mesh = meshes[key]
const parent = mesh.parent
const mergedGeom = mesh.geometry.clone()
for (const key in mergedGeom.attributes) {
if (key !== 'position') {
mergedGeom.deleteAttribute(key)
}
}
const lineGeom = new ConditionalEdgesGeometry(mergeVertices(mergedGeom))
const material = new THREE.ShaderMaterial(ConditionalEdgesShader)
material.uniforms.diffuse.value.set(this.LIGHT_LINES)
const line = new THREE.LineSegments(lineGeom, material)
line.position.copy(mesh.position)
line.scale.copy(mesh.scale)
line.rotation.copy(mesh.rotation)
const thickLineGeom =
new ConditionalLineSegmentsGeometry().fromConditionalEdgesGeometry(
lineGeom
)
const conditionalLineMaterial = new ConditionalLineMaterial({
color: this.LIGHT_LINES,
linewidth: 2
})
const thickLines = new LineSegments2(
thickLineGeom,
conditionalLineMaterial
)
thickLines.position.copy(mesh.position)
thickLines.scale.copy(mesh.scale)
thickLines.rotation.copy(mesh.rotation)
parent?.remove(mesh)
parent?.add(line)
parent?.add(thickLines)
}
}
setMaterialMode(mode: MaterialMode): void {
if (!this.currentModel || mode === this.materialMode) {
return
}
this.disposeLineartModel()
this.materialMode = mode
if (!this.currentModel) return
if (mode === 'lineart' || this.materialMode === 'lineart') {
this.eventManager.emitEvent('materialLoadingStart', null)
}
if (mode === 'depth') {
this.renderer.outputColorSpace = THREE.LinearSRGBColorSpace
@@ -94,6 +380,10 @@ export class ModelManager implements ModelManagerInterface {
this.renderer.outputColorSpace = THREE.SRGBColorSpace
}
if (this.currentModel) {
this.currentModel.visible = mode !== 'lineart'
}
this.currentModel.traverse((child) => {
if (child instanceof THREE.Mesh) {
switch (mode) {
@@ -153,7 +443,6 @@ export class ModelManager implements ModelManagerInterface {
opacity: 1.0
})
break
case 'wireframe':
if (!this.originalMaterials.has(child)) {
this.originalMaterials.set(child, child.material)
@@ -165,7 +454,6 @@ export class ModelManager implements ModelManagerInterface {
opacity: 1.0
})
break
case 'original':
const originalMaterial = this.originalMaterials.get(child)
if (originalMaterial) {
@@ -178,6 +466,57 @@ export class ModelManager implements ModelManagerInterface {
}
})
if (mode === 'lineart') {
setTimeout(() => {
this.initEdgesModel()
this.initBackgroundModel()
this.initConditionalModel()
if (this.conditionalModel) {
this.conditionalModel.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material &&
child.material.resolution
) {
this.renderer.getSize(child.material.resolution)
child.material.resolution.multiplyScalar(window.devicePixelRatio)
child.material.linewidth = 1
}
})
}
if (this.edgesModel) {
this.edgesModel.traverse((child) => {
if (
child instanceof THREE.Mesh &&
child.material &&
child.material.resolution
) {
this.renderer.getSize(child.material.resolution)
child.material.resolution.multiplyScalar(window.devicePixelRatio)
child.material.linewidth = 1
}
})
}
if (this.backgroundModel) {
this.backgroundModel.visible = true
this.backgroundModel.traverse((child) => {
if (child instanceof THREE.Mesh && child.material) {
child.material.transparent = false
child.material.opacity = 0.25
child.material.color.set(this.LIGHT_MODEL)
}
})
}
this.eventManager.emitEvent('materialLoadingEnd', null)
}, 50)
} else if (this.materialMode === 'lineart') {
this.eventManager.emitEvent('materialLoadingEnd', null)
}
this.eventManager.emitEvent('materialModeChange', mode)
}
@@ -188,9 +527,7 @@ export class ModelManager implements ModelManagerInterface {
}
})
if (this.materialMode !== 'original') {
this.setMaterialMode(this.materialMode)
}
this.setMaterialMode('original')
}
clearModel(): void {

View File

@@ -0,0 +1,159 @@
import { Color, ShaderLib, ShaderMaterial, UniformsUtils } from 'three'
export class ColoredShadowMaterial extends ShaderMaterial {
get color() {
return this.uniforms.diffuse.value
}
get shadowColor() {
return this.uniforms.shadowColor.value
}
set shininess(v) {
this.uniforms.shininess.value = v
}
get shininess() {
return this.uniforms.shininess.value
}
constructor(options) {
super({
uniforms: UniformsUtils.merge([
ShaderLib.phong.uniforms,
{
shadowColor: {
value: new Color(0xff0000)
}
}
]),
vertexShader: `
#define PHONG
varying vec3 vViewPosition;
#ifndef FLAT_SHADED
varying vec3 vNormal;
#endif
#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <uv_vertex>
#include <uv2_vertex>
#include <color_vertex>
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#ifndef FLAT_SHADED
vNormal = normalize( transformedNormal );
#endif
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <displacementmap_vertex>
#include <project_vertex>
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
vViewPosition = - mvPosition.xyz;
#include <worldpos_vertex>
#include <envmap_vertex>
#include <shadowmap_vertex>
#include <fog_vertex>
}
`,
fragmentShader: `
#define PHONG
uniform vec3 diffuse;
uniform vec3 emissive;
uniform vec3 specular;
uniform float shininess;
uniform float opacity;
uniform vec3 shadowColor;
#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_common_pars_fragment>
#include <envmap_pars_fragment>
#include <cube_uv_reflection_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_phong_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec4 diffuseColor = vec4( 1.0, 1.0, 1.0, opacity );
ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
vec3 totalEmissiveRadiance = emissive;
#include <logdepthbuf_fragment>
#include <map_fragment>
#include <color_fragment>
#include <alphamap_fragment>
#include <alphatest_fragment>
#include <specularmap_fragment>
#include <normal_fragment_begin>
#include <normal_fragment_maps>
#include <emissivemap_fragment>
#include <lights_phong_fragment>
#include <lights_fragment_begin>
#include <lights_fragment_maps>
#include <lights_fragment_end>
#include <aomap_fragment>
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;
#include <envmap_fragment>
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
#include <dithering_fragment>
gl_FragColor.rgb = mix(
shadowColor.rgb,
diffuse.rgb,
min( gl_FragColor.r, 1.0 )
);
}
`
})
Object.defineProperties(this, {
opacity: {
set(v) {
this.uniforms.opacity.value = v
},
get() {
return this.uniforms.opacity.value
}
}
})
this.setValues(options)
this.lights = true
}
}

View File

@@ -0,0 +1,122 @@
import { BufferAttribute, BufferGeometry, Triangle, Vector3 } from 'three'
const vec0 = new Vector3()
const vec1 = new Vector3()
const vec2 = new Vector3()
const vec3 = new Vector3()
const vec4 = new Vector3()
const triangle0 = new Triangle()
const triangle1 = new Triangle()
const normal0 = new Vector3()
const normal1 = new Vector3()
export class ConditionalEdgesGeometry extends BufferGeometry {
constructor(geometry) {
super()
const edgeInfo = {}
const position = geometry.attributes.position
let index
if (geometry.index) {
index = geometry.index
} else {
const arr = new Array(position.count / 3).fill().map((_, i) => i)
index = new BufferAttribute(new Uint32Array(arr), 1, false)
}
for (let i = 0, l = index.count; i < l; i += 3) {
const indices = [index.getX(i + 0), index.getX(i + 1), index.getX(i + 2)]
for (let j = 0; j < 3; j++) {
const index0 = indices[j]
const index1 = indices[(j + 1) % 3]
const hash = `${index0}_${index1}`
const reverseHash = `${index1}_${index0}`
if (reverseHash in edgeInfo) {
edgeInfo[reverseHash].controlIndex1 = indices[(j + 2) % 3]
edgeInfo[reverseHash].tri1 = i / 3
} else {
edgeInfo[hash] = {
index0,
index1,
controlIndex0: indices[(j + 2) % 3],
controlIndex1: null,
tri0: i / 3,
tri1: null
}
}
}
}
const edgePositions = []
const edgeDirections = []
const edgeControl0 = []
const edgeControl1 = []
for (const key in edgeInfo) {
const { index0, index1, controlIndex0, controlIndex1, tri0, tri1 } =
edgeInfo[key]
if (controlIndex1 === null) {
continue
}
triangle0.a.fromBufferAttribute(position, index.getX(tri0 * 3 + 0))
triangle0.b.fromBufferAttribute(position, index.getX(tri0 * 3 + 1))
triangle0.c.fromBufferAttribute(position, index.getX(tri0 * 3 + 2))
triangle1.a.fromBufferAttribute(position, index.getX(tri1 * 3 + 0))
triangle1.b.fromBufferAttribute(position, index.getX(tri1 * 3 + 1))
triangle1.c.fromBufferAttribute(position, index.getX(tri1 * 3 + 2))
triangle0.getNormal(normal0).normalize()
triangle1.getNormal(normal1).normalize()
if (normal0.dot(normal1) < 0.01) {
continue
}
// positions
vec0.fromBufferAttribute(position, index0)
vec1.fromBufferAttribute(position, index1)
// direction
vec2.subVectors(vec0, vec1)
// control positions
vec3.fromBufferAttribute(position, controlIndex0)
vec4.fromBufferAttribute(position, controlIndex1)
// create arrays
edgePositions.push(vec0.x, vec0.y, vec0.z)
edgeDirections.push(vec2.x, vec2.y, vec2.z)
edgeControl0.push(vec3.x, vec3.y, vec3.z)
edgeControl1.push(vec4.x, vec4.y, vec4.z)
edgePositions.push(vec1.x, vec1.y, vec1.z)
edgeDirections.push(vec2.x, vec2.y, vec2.z)
edgeControl0.push(vec3.x, vec3.y, vec3.z)
edgeControl1.push(vec4.x, vec4.y, vec4.z)
}
this.setAttribute(
'position',
new BufferAttribute(new Float32Array(edgePositions), 3, false)
)
this.setAttribute(
'direction',
new BufferAttribute(new Float32Array(edgeDirections), 3, false)
)
this.setAttribute(
'control0',
new BufferAttribute(new Float32Array(edgeControl0), 3, false)
)
this.setAttribute(
'control1',
new BufferAttribute(new Float32Array(edgeControl1), 3, false)
)
}
}

View File

@@ -0,0 +1,92 @@
import { Color } from 'three'
export const ConditionalEdgesShader = {
uniforms: {
diffuse: {
value: new Color()
},
opacity: {
value: 1.0
}
},
vertexShader: /* glsl */ `
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
void main() {
#include <color_vertex>
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
gl_Position = projectionMatrix * mvPosition;
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( position + direction, 1.0 );
c0 /= c0.w;
c1 /= c1.w;
p0 /= p0.w;
p1 /= p1.w;
// Get the direction of the segment and an orthogonal vector
vec2 dir = p1.xy - p0.xy;
vec2 norm = vec2( -dir.y, dir.x );
// Get control point directions from the line
vec2 c0dir = c0.xy - p1.xy;
vec2 c1dir = c1.xy - p1.xy;
// If the vectors to the controls points are pointed in different directions away
// from the line segment then the line should not be drawn.
float d0 = dot( normalize( norm ), normalize( c0dir ) );
float d1 = dot( normalize( norm ), normalize( c1dir ) );
float discardFlag = float( sign( d0 ) != sign( d1 ) );
gl_Position = discardFlag > 0.5 ? c0 : gl_Position;
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
}
`,
fragmentShader: /* glsl */ `
uniform vec3 diffuse;
uniform float opacity;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
void main() {
#include <clipping_planes_fragment>
vec3 outgoingLight = vec3( 0.0 );
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
outgoingLight = diffuseColor.rgb; // simple shader
gl_FragColor = vec4( outgoingLight, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`
}

View File

@@ -0,0 +1,375 @@
import { ShaderMaterial, UniformsLib, UniformsUtils, Vector2 } from 'three'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial'
/**
* parameters = {
* color: <hex>,
* linewidth: <float>,
* dashed: <boolean>,
* dashScale: <float>,
* dashSize: <float>,
* gapSize: <float>,
* resolution: <Vector2>, // to be set by renderer
* }
*/
const uniforms = {
linewidth: { value: 1 },
resolution: { value: new Vector2(1, 1) },
dashScale: { value: 1 },
dashSize: { value: 1 },
gapSize: { value: 1 }, // todo FIX - maybe change to totalSize
opacity: { value: 1 }
}
const shader = {
uniforms: UniformsUtils.merge([
UniformsLib.common,
UniformsLib.fog,
uniforms
]),
vertexShader: /* glsl */ `
#include <common>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>
uniform float linewidth;
uniform vec2 resolution;
attribute vec3 control0;
attribute vec3 control1;
attribute vec3 direction;
attribute vec3 instanceStart;
attribute vec3 instanceEnd;
attribute vec3 instanceColorStart;
attribute vec3 instanceColorEnd;
varying vec2 vUv;
#ifdef USE_DASH
uniform float dashScale;
attribute float instanceDistanceStart;
attribute float instanceDistanceEnd;
varying float vLineDistance;
#endif
void trimSegment( const in vec4 start, inout vec4 end ) {
// trim end segment so it terminates between the camera plane and the near plane
// conservative estimate of the near plane
float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column
float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column
float nearEstimate = - 0.5 * b / a;
float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );
end.xyz = mix( start.xyz, end.xyz, alpha );
}
void main() {
#ifdef USE_COLOR
vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;
#endif
#ifdef USE_DASH
vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd;
#endif
float aspect = resolution.x / resolution.y;
vUv = uv;
// camera space
vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );
vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );
// 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
bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column
if ( perspective ) {
if ( start.z < 0.0 && end.z >= 0.0 ) {
trimSegment( start, end );
} else if ( end.z < 0.0 && start.z >= 0.0 ) {
trimSegment( end, start );
}
}
// clip space
vec4 clipStart = projectionMatrix * start;
vec4 clipEnd = projectionMatrix * end;
// ndc space
vec2 ndcStart = clipStart.xy / clipStart.w;
vec2 ndcEnd = clipEnd.xy / clipEnd.w;
// direction
vec2 dir = ndcEnd - ndcStart;
// account for clip-space aspect ratio
dir.x *= aspect;
dir = normalize( dir );
// perpendicular to dir
vec2 offset = vec2( dir.y, - dir.x );
// undo aspect ratio adjustment
dir.x /= aspect;
offset.x /= aspect;
// sign flip
if ( position.x < 0.0 ) offset *= - 1.0;
// endcaps
if ( position.y < 0.0 ) {
offset += - dir;
} else if ( position.y > 1.0 ) {
offset += dir;
}
// adjust for linewidth
offset *= linewidth;
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
offset /= resolution.y;
// select end
vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;
// back to clip space
offset *= clip.w;
clip.xy += offset;
gl_Position = clip;
vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation
#include <logdepthbuf_vertex>
#include <clipping_planes_vertex>
#include <fog_vertex>
// conditional logic
// Transform the line segment ends and control points into camera clip space
vec4 c0 = projectionMatrix * modelViewMatrix * vec4( control0, 1.0 );
vec4 c1 = projectionMatrix * modelViewMatrix * vec4( control1, 1.0 );
vec4 p0 = projectionMatrix * modelViewMatrix * vec4( instanceStart, 1.0 );
vec4 p1 = projectionMatrix * modelViewMatrix * vec4( instanceStart + direction, 1.0 );
c0 /= c0.w;
c1 /= c1.w;
p0 /= p0.w;
p1 /= p1.w;
// Get the direction of the segment and an orthogonal vector
vec2 segDir = p1.xy - p0.xy;
vec2 norm = vec2( - segDir.y, segDir.x );
// Get control point directions from the line
vec2 c0dir = c0.xy - p1.xy;
vec2 c1dir = c1.xy - p1.xy;
// If the vectors to the controls points are pointed in different directions away
// from the line segment then the line should not be drawn.
float d0 = dot( normalize( norm ), normalize( c0dir ) );
float d1 = dot( normalize( norm ), normalize( c1dir ) );
float discardFlag = float( sign( d0 ) != sign( d1 ) );
gl_Position = discardFlag > 0.5 ? c0 : gl_Position;
// end conditional line logic
}
`,
fragmentShader: /* glsl */ `
uniform vec3 diffuse;
uniform float opacity;
#ifdef USE_DASH
uniform float dashSize;
uniform float gapSize;
#endif
varying float vLineDistance;
#include <common>
#include <color_pars_fragment>
#include <fog_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>
varying vec2 vUv;
void main() {
#include <clipping_planes_fragment>
#ifdef USE_DASH
if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps
if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX
#endif
if ( abs( vUv.y ) > 1.0 ) {
float a = vUv.x;
float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;
float len2 = a * a + b * b;
if ( len2 > 1.0 ) discard;
}
vec4 diffuseColor = vec4( diffuse, opacity );
#include <logdepthbuf_fragment>
#include <color_fragment>
gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );
#include <tonemapping_fragment>
#include <fog_fragment>
#include <premultiplied_alpha_fragment>
}
`
}
class ConditionalLineMaterial extends LineMaterial {
constructor(parameters) {
super({
type: 'ConditionalLineMaterial',
uniforms: UniformsUtils.clone(shader.uniforms),
vertexShader: shader.vertexShader,
fragmentShader: shader.fragmentShader,
clipping: true // required for clipping support
})
this.dashed = false
Object.defineProperties(this, {
color: {
enumerable: true,
get: function () {
return this.uniforms.diffuse.value
},
set: function (value) {
this.uniforms.diffuse.value = value
}
},
linewidth: {
enumerable: true,
get: function () {
return this.uniforms.linewidth.value
},
set: function (value) {
this.uniforms.linewidth.value = value
}
},
dashScale: {
enumerable: true,
get: function () {
return this.uniforms.dashScale.value
},
set: function (value) {
this.uniforms.dashScale.value = value
}
},
dashSize: {
enumerable: true,
get: function () {
return this.uniforms.dashSize.value
},
set: function (value) {
this.uniforms.dashSize.value = value
}
},
gapSize: {
enumerable: true,
get: function () {
return this.uniforms.gapSize.value
},
set: function (value) {
this.uniforms.gapSize.value = value
}
},
opacity: {
enumerable: true,
get: function () {
return this.uniforms.opacity.value
},
set: function (value) {
this.uniforms.opacity.value = value
}
},
resolution: {
enumerable: true,
get: function () {
return this.uniforms.resolution.value
},
set: function (value) {
this.uniforms.resolution.value.copy(value)
}
}
})
this.setValues(parameters)
}
}
ConditionalLineMaterial.prototype.isConditionalLineMaterial = true
export { ConditionalLineMaterial }

View File

@@ -0,0 +1,39 @@
import * as THREE from 'three'
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js'
export class ConditionalLineSegmentsGeometry extends LineSegmentsGeometry {
fromConditionalEdgesGeometry(geometry) {
super.fromEdgesGeometry(geometry)
const { direction, control0, control1 } = geometry.attributes
this.setAttribute(
'direction',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(direction.array, 6, 1),
3,
0
)
)
this.setAttribute(
'control0',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(control0.array, 6, 1),
3,
0
)
)
this.setAttribute(
'control1',
new THREE.InterleavedBufferAttribute(
new THREE.InstancedInterleavedBuffer(control1.array, 6, 1),
3,
0
)
)
return this
}
}

View File

@@ -0,0 +1,44 @@
import { BufferAttribute, BufferGeometry, Vector3 } from 'three'
const vec = new Vector3()
export class OutsideEdgesGeometry extends BufferGeometry {
constructor(geometry) {
super()
const edgeInfo = {}
const index = geometry.index
const position = geometry.attributes.position
for (let i = 0, l = index.count; i < l; i += 3) {
const indices = [index.getX(i + 0), index.getX(i + 1), index.getX(i + 2)]
for (let j = 0; j < 3; j++) {
const index0 = indices[j]
const index1 = indices[(j + 1) % 3]
const hash = `${index0}_${index1}`
const reverseHash = `${index1}_${index0}`
if (reverseHash in edgeInfo) {
delete edgeInfo[reverseHash]
} else {
edgeInfo[hash] = [index0, index1]
}
}
}
const edgePositions = []
for (const key in edgeInfo) {
const [i0, i1] = edgeInfo[key]
vec.fromBufferAttribute(position, i0)
edgePositions.push(vec.x, vec.y, vec.z)
vec.fromBufferAttribute(position, i1)
edgePositions.push(vec.x, vec.y, vec.z)
}
this.setAttribute(
'position',
new BufferAttribute(new Float32Array(edgePositions), 3, false)
)
}
}

View File

@@ -8,7 +8,12 @@ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
export type MaterialMode = 'original' | 'normal' | 'wireframe' | 'depth'
export type MaterialMode =
| 'original'
| 'normal'
| 'wireframe'
| 'depth'
| 'lineart'
export type UpDirection = 'original' | '-x' | '+x' | '-y' | '+y' | '-z' | '+z'
export type CameraType = 'perspective' | 'orthographic'