Skip to content

Commit 6e3d7b4

Browse files
authored
feat: add screenspace to Outline (#60)
1 parent 7902a60 commit 6e3d7b4

File tree

3 files changed

+111
-32
lines changed

3 files changed

+111
-32
lines changed

.storybook/stories/Outlines.stories.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,27 @@ let allOutlines: OutlinesType[] = []
1616
const outlinesParams = {
1717
color: '#ffff00' as THREE.ColorRepresentation,
1818
thickness: 0.1,
19+
screenspace: false,
1920
}
2021

21-
const generateOutlines = () => {
22+
const generateOutlines = (gl: THREE.WebGLRenderer) => {
2223
return Outlines({
2324
color: new THREE.Color(outlinesParams.color),
2425
thickness: outlinesParams.thickness,
26+
screenspace: outlinesParams.screenspace,
27+
gl,
2528
})
2629
}
2730

28-
const setupTourMesh = () => {
31+
const setupTourMesh = (gl: THREE.WebGLRenderer) => {
2932
const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32)
3033
const mat = new THREE.MeshStandardMaterial({
3134
roughness: 0,
3235
color: 0xffffff * Math.random(),
3336
})
3437
const torusMesh = new THREE.Mesh(geometry, mat)
3538

36-
const outlines = generateOutlines()
39+
const outlines = generateOutlines(gl)
3740
torusMesh.traverse((child) => {
3841
if (child instanceof THREE.Mesh) {
3942
child.castShadow = true
@@ -47,12 +50,12 @@ const setupTourMesh = () => {
4750
return torusMesh
4851
}
4952

50-
const setupBox = () => {
53+
const setupBox = (gl: THREE.WebGLRenderer) => {
5154
const geometry = new THREE.BoxGeometry(2, 2, 2)
5255
const mat = new THREE.MeshBasicMaterial({ color: 'grey' })
5356
const boxMesh = new THREE.Mesh(geometry, mat)
5457
boxMesh.position.y = 1.2
55-
const outlines = generateOutlines()
58+
const outlines = generateOutlines(gl)
5659

5760
allOutlines.push(outlines)
5861
boxMesh.add(outlines.group)
@@ -88,9 +91,9 @@ export const OutlinesStory = async () => {
8891

8992
camera.position.set(10, 10, 10)
9093
scene.add(setupLight())
91-
scene.add(setupTourMesh())
94+
scene.add(setupTourMesh(renderer))
9295

93-
const box = setupBox()
96+
const box = setupBox(renderer)
9497
scene.add(box)
9598

9699
const floor = new THREE.Mesh(
@@ -118,9 +121,14 @@ const addOutlineGui = () => {
118121
outline.updateProps({ color: new THREE.Color(color) })
119122
})
120123
})
121-
folder.add(params, 'thickness', 0, 0.1, 0.01).onChange((thickness: number) => {
124+
folder.add(params, 'thickness', 0, 2, 0.01).onChange((thickness: number) => {
122125
allOutlines.forEach((outline) => {
123126
outline.updateProps({ thickness })
124127
})
125128
})
129+
folder.add(params, 'screenspace').onChange((screenspace: boolean) => {
130+
allOutlines.forEach((outline) => {
131+
outline.updateProps({ screenspace })
132+
})
133+
})
126134
}

README.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -511,15 +511,23 @@ An ornamental component that extracts the geometry from its parent and displays
511511
```tsx
512512
export type OutlinesProps = {
513513
/** Outline color, default: black */
514-
color: THREE.Color
514+
color?: THREE.Color
515+
/** Line thickness is independent of zoom, default: false */
516+
screenspace?: boolean
515517
/** Outline opacity, default: 1 */
516-
opacity: number
518+
opacity?: number
517519
/** Outline transparency, default: false */
518-
transparent: boolean
520+
transparent?: boolean
519521
/** Outline thickness, default 0.05 */
520-
thickness: number
522+
thickness?: number
521523
/** Geometry crease angle (0 === no crease), default: Math.PI */
522-
angle: number
524+
angle?: number
525+
toneMapped?: boolean
526+
polygonOffset?: boolean
527+
polygonOffsetFactor?: number
528+
renderOrder?: number
529+
/** needed if `screenspace` is true */
530+
gl?: THREE.WebGLRenderer
523531
}
524532
```
525533

src/core/Outlines.ts

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1+
import { shaderMaterial } from './shaderMaterial'
12
import * as THREE from 'three'
23
import { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils'
3-
import { shaderMaterial } from './shaderMaterial'
44

55
export type OutlinesProps = {
66
/** Outline color, default: black */
7-
color: THREE.Color
7+
color?: THREE.Color
8+
/** Line thickness is independent of zoom, default: false */
9+
screenspace?: boolean
810
/** Outline opacity, default: 1 */
9-
opacity: number
11+
opacity?: number
1012
/** Outline transparency, default: false */
11-
transparent: boolean
13+
transparent?: boolean
1214
/** Outline thickness, default 0.05 */
13-
thickness: number
15+
thickness?: number
1416
/** Geometry crease angle (0 === no crease), default: Math.PI */
15-
angle: number
17+
angle?: number
18+
toneMapped?: boolean
19+
polygonOffset?: boolean
20+
polygonOffsetFactor?: number
21+
renderOrder?: number
22+
/** needed if `screenspace` is true */
23+
gl?: THREE.WebGLRenderer
1624
}
1725

1826
export type OutlinesType = {
@@ -25,12 +33,20 @@ export type OutlinesType = {
2533
}
2634

2735
const OutlinesMaterial = shaderMaterial(
28-
{ color: new THREE.Color('black'), opacity: 1, thickness: 0.05 },
36+
{
37+
screenspace: false,
38+
color: new THREE.Color('black'),
39+
opacity: 1,
40+
thickness: 0.05,
41+
size: new THREE.Vector2(),
42+
},
2943
/* glsl */ `
3044
#include <common>
3145
#include <morphtarget_pars_vertex>
3246
#include <skinning_pars_vertex>
3347
uniform float thickness;
48+
uniform float screenspace;
49+
uniform vec2 size;
3450
void main() {
3551
#if defined (USE_SKINNING)
3652
#include <beginnormal_vertex>
@@ -43,14 +59,22 @@ const OutlinesMaterial = shaderMaterial(
4359
#include <morphtarget_vertex>
4460
#include <skinning_vertex>
4561
#include <project_vertex>
46-
vec4 transformedNormal = vec4(normal, 0.0);
47-
vec4 transformedPosition = vec4(transformed, 1.0);
62+
vec4 tNormal = vec4(normal, 0.0);
63+
vec4 tPosition = vec4(transformed, 1.0);
4864
#ifdef USE_INSTANCING
49-
transformedNormal = instanceMatrix * transformedNormal;
50-
transformedPosition = instanceMatrix * transformedPosition;
65+
tNormal = instanceMatrix * tNormal;
66+
tPosition = instanceMatrix * tPosition;
5167
#endif
52-
vec3 newPosition = transformedPosition.xyz + transformedNormal.xyz * thickness;
53-
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
68+
if (screenspace == 0.0) {
69+
vec3 newPosition = tPosition.xyz + tNormal.xyz * thickness;
70+
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
71+
} else {
72+
vec4 clipPosition = projectionMatrix * modelViewMatrix * tPosition;
73+
vec4 clipNormal = projectionMatrix * modelViewMatrix * tNormal;
74+
vec2 offset = normalize(clipNormal.xy) * thickness / size * clipPosition.w * 2.0;
75+
clipPosition.xy += offset;
76+
gl_Position = clipPosition;
77+
}
5478
}`,
5579
/* glsl */ `
5680
uniform vec3 color;
@@ -66,36 +90,48 @@ export function Outlines({
6690
color = new THREE.Color('black'),
6791
opacity = 1,
6892
transparent = false,
93+
screenspace = false,
94+
toneMapped = true,
95+
polygonOffset = false,
96+
polygonOffsetFactor = 0,
97+
renderOrder = 0,
6998
thickness = 0.05,
7099
angle = Math.PI,
100+
gl,
71101
}: Partial<OutlinesProps>): OutlinesType {
72102
const group = new THREE.Group()
73103

74104
let shapeProps: OutlinesProps = {
75105
color,
76106
opacity,
77107
transparent,
108+
screenspace,
109+
toneMapped,
110+
polygonOffset,
111+
polygonOffsetFactor,
112+
renderOrder,
78113
thickness,
79114
angle,
80115
}
81116

82-
function updateMesh(angle: number) {
117+
function updateMesh(angle?: number) {
83118
const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh & THREE.InstancedMesh
84119
group.clear()
85120
if (parent && parent.geometry) {
86121
let mesh
122+
const material = new OutlinesMaterial({ side: THREE.BackSide })
87123
if (parent.skeleton) {
88124
mesh = new THREE.SkinnedMesh()
89-
mesh.material = new OutlinesMaterial({ side: THREE.BackSide })
125+
mesh.material = material
90126
mesh.bind(parent.skeleton, parent.bindMatrix)
91127
group.add(mesh)
92128
} else if (parent.isInstancedMesh) {
93-
mesh = new THREE.InstancedMesh(parent.geometry, new OutlinesMaterial({ side: THREE.BackSide }), parent.count)
129+
mesh = new THREE.InstancedMesh(parent.geometry, material, parent.count)
94130
mesh.instanceMatrix = parent.instanceMatrix
95131
group.add(mesh)
96132
} else {
97133
mesh = new THREE.Mesh()
98-
mesh.material = new OutlinesMaterial({ side: THREE.BackSide })
134+
mesh.material = material
99135
group.add(mesh)
100136
}
101137
mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry
@@ -106,8 +142,35 @@ export function Outlines({
106142
shapeProps = { ...shapeProps, ...newProps }
107143
const mesh = group.children[0] as THREE.Mesh<THREE.BufferGeometry, THREE.Material>
108144
if (mesh) {
109-
const { transparent, thickness, color, opacity } = shapeProps
110-
Object.assign(mesh.material, { transparent, thickness, color, opacity })
145+
const {
146+
transparent,
147+
thickness,
148+
color,
149+
opacity,
150+
screenspace,
151+
toneMapped,
152+
polygonOffset,
153+
polygonOffsetFactor,
154+
renderOrder,
155+
} = shapeProps
156+
const contextSize = new THREE.Vector2()
157+
if (!gl && shapeProps.screenspace) {
158+
console.warn('Outlines: "screenspace" requires a WebGLRenderer instance to calculate the outline size')
159+
}
160+
if (gl) gl.getSize(contextSize)
161+
162+
Object.assign(mesh.material, {
163+
transparent,
164+
thickness,
165+
color,
166+
opacity,
167+
size: contextSize,
168+
screenspace,
169+
toneMapped,
170+
polygonOffset,
171+
polygonOffsetFactor,
172+
})
173+
if (renderOrder !== undefined) mesh.renderOrder = renderOrder
111174
}
112175
}
113176

0 commit comments

Comments
 (0)