[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

@@ -35,6 +35,7 @@
:hasBackgroundImage="hasBackgroundImage"
:upDirection="upDirection"
:materialMode="materialMode"
:isAnimation="false"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"

View File

@@ -42,6 +42,7 @@
:hasBackgroundImage="hasBackgroundImage"
:upDirection="upDirection"
:materialMode="materialMode"
:isAnimation="true"
@updateBackgroundImage="handleBackgroundImageUpdate"
@switchCamera="switchCamera"
@toggleGrid="toggleGrid"

View File

@@ -105,7 +105,7 @@
</div>
<div v-if="activeCategory === 'model'" class="flex flex-col">
<div class="relative show-up-direction">
<div v-if="notMaterialLineart" class="relative show-up-direction">
<Button
class="p-button-rounded p-button-text"
@click="toggleUpDirection"
@@ -278,6 +278,7 @@ const props = defineProps<{
hasBackgroundImage?: boolean
upDirection: UpDirection
materialMode: MaterialMode
isAnimation: boolean
}>()
const isMenuOpen = ref(false)
@@ -346,12 +347,25 @@ const upDirections: UpDirection[] = [
'+z'
]
const showMaterialMode = ref(false)
const materialModes: MaterialMode[] = [
'original',
'normal',
'wireframe'
//'depth' disable for now
]
const materialModes = computed(() => {
const modes: MaterialMode[] = [
'original',
'normal',
'wireframe'
//'depth' disable for now
]
if (!props.isAnimation) {
modes.push('lineart')
}
return modes
})
const notMaterialLineart = computed(() => {
return props.materialMode !== 'lineart'
})
const switchCamera = () => {
emit('switchCamera')

View File

@@ -6,7 +6,7 @@
<script setup lang="ts">
import { LGraphNode } from '@comfyorg/litegraph'
import { onMounted, onUnmounted, ref, toRaw, watchEffect } from 'vue'
import { onMounted, onUnmounted, ref, toRaw, watch, watchEffect } from 'vue'
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
import Load3d from '@/extensions/core/load3d/Load3d'
@@ -16,6 +16,7 @@ import {
MaterialMode,
UpDirection
} from '@/extensions/core/load3d/interfaces'
import { t } from '@/i18n'
import { useLoad3dService } from '@/services/load3dService'
const props = defineProps<{
@@ -50,8 +51,12 @@ const eventConfig = {
backgroundImageChange: (value: string) =>
emit('backgroundImageChange', value),
upDirectionChange: (value: string) => emit('upDirectionChange', value),
modelLoadingStart: () => loadingOverlayRef.value?.startLoading(),
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading()
modelLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.loadingModel')),
modelLoadingEnd: () => loadingOverlayRef.value?.endLoading(),
materialLoadingStart: () =>
loadingOverlayRef.value?.startLoading(t('load3d.switchingMaterialMode')),
materialLoadingEnd: () => loadingOverlayRef.value?.endLoading()
} as const
watchEffect(() => {
@@ -66,10 +71,20 @@ watchEffect(() => {
rawLoad3d.togglePreview(props.showPreview)
rawLoad3d.setBackgroundImage(props.backgroundImage)
rawLoad3d.setUpDirection(props.upDirection)
rawLoad3d.setMaterialMode(props.materialMode)
}
})
watch(
() => props.materialMode,
(newValue) => {
if (load3d.value) {
const rawLoad3d = toRaw(load3d.value)
rawLoad3d.setMaterialMode(newValue)
}
}
)
const emit = defineEmits<{
(e: 'materialModeChange', materialMode: string): void
(e: 'backgroundColorChange', color: string): void

View File

@@ -7,7 +7,7 @@
<div class="flex flex-col items-center">
<div class="spinner"></div>
<div class="text-white mt-4 text-lg">
{{ t('load3d.loadingModel') }}
{{ loadingMessage }}
</div>
</div>
</div>
@@ -20,9 +20,11 @@ import { ref } from 'vue'
import { t } from '@/i18n'
const modelLoading = ref(false)
const loadingMessage = ref('')
const startLoading = () => {
const startLoading = (message?: string) => {
modelLoading.value = true
loadingMessage.value = message || t('load3d.loadingModel')
}
const endLoading = () => {

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'

View File

@@ -858,6 +858,7 @@
"scene": "Scene",
"model": "Model",
"camera": "Camera",
"light": "Light"
"light": "Light",
"switchingMaterialMode": "Switching Material Mode..."
}
}

View File

@@ -335,6 +335,7 @@
"scene": "Scène",
"showGrid": "Afficher la grille",
"switchCamera": "Changer de caméra",
"switchingMaterialMode": "Changement de mode de matériau...",
"upDirection": "Direction Haut",
"uploadBackgroundImage": "Télécharger l'image de fond"
},

View File

@@ -335,6 +335,7 @@
"scene": "シーン",
"showGrid": "グリッドを表示",
"switchCamera": "カメラを切り替える",
"switchingMaterialMode": "マテリアルモードの切り替え中...",
"upDirection": "上方向",
"uploadBackgroundImage": "背景画像をアップロード"
},

View File

@@ -335,6 +335,7 @@
"scene": "장면",
"showGrid": "그리드 표시",
"switchCamera": "카메라 전환",
"switchingMaterialMode": "재료 모드 전환 중...",
"upDirection": "위 방향",
"uploadBackgroundImage": "배경 이미지 업로드"
},

View File

@@ -335,6 +335,7 @@
"scene": "Сцена",
"showGrid": "Показать сетку",
"switchCamera": "Переключить камеру",
"switchingMaterialMode": "Переключение режима материала...",
"upDirection": "Направление Вверх",
"uploadBackgroundImage": "Загрузить фоновое изображение"
},

View File

@@ -335,6 +335,7 @@
"scene": "场景",
"showGrid": "显示网格",
"switchCamera": "切换摄像头",
"switchingMaterialMode": "切换材质模式中...",
"upDirection": "向上方向",
"uploadBackgroundImage": "上传背景图片"
},