Skip to content

Commit

Permalink
feat: add screenspace to Outline (#60)
Browse files Browse the repository at this point in the history
  • Loading branch information
RodrigoHamuy authored Apr 14, 2024
1 parent 7902a60 commit 6e3d7b4
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 32 deletions.
24 changes: 16 additions & 8 deletions .storybook/stories/Outlines.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,27 @@ let allOutlines: OutlinesType[] = []
const outlinesParams = {
color: '#ffff00' as THREE.ColorRepresentation,
thickness: 0.1,
screenspace: false,
}

const generateOutlines = () => {
const generateOutlines = (gl: THREE.WebGLRenderer) => {
return Outlines({
color: new THREE.Color(outlinesParams.color),
thickness: outlinesParams.thickness,
screenspace: outlinesParams.screenspace,
gl,
})
}

const setupTourMesh = () => {
const setupTourMesh = (gl: THREE.WebGLRenderer) => {
const geometry = new THREE.TorusKnotGeometry(1, 0.35, 100, 32)
const mat = new THREE.MeshStandardMaterial({
roughness: 0,
color: 0xffffff * Math.random(),
})
const torusMesh = new THREE.Mesh(geometry, mat)

const outlines = generateOutlines()
const outlines = generateOutlines(gl)
torusMesh.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.castShadow = true
Expand All @@ -47,12 +50,12 @@ const setupTourMesh = () => {
return torusMesh
}

const setupBox = () => {
const setupBox = (gl: THREE.WebGLRenderer) => {
const geometry = new THREE.BoxGeometry(2, 2, 2)
const mat = new THREE.MeshBasicMaterial({ color: 'grey' })
const boxMesh = new THREE.Mesh(geometry, mat)
boxMesh.position.y = 1.2
const outlines = generateOutlines()
const outlines = generateOutlines(gl)

allOutlines.push(outlines)
boxMesh.add(outlines.group)
Expand Down Expand Up @@ -88,9 +91,9 @@ export const OutlinesStory = async () => {

camera.position.set(10, 10, 10)
scene.add(setupLight())
scene.add(setupTourMesh())
scene.add(setupTourMesh(renderer))

const box = setupBox()
const box = setupBox(renderer)
scene.add(box)

const floor = new THREE.Mesh(
Expand Down Expand Up @@ -118,9 +121,14 @@ const addOutlineGui = () => {
outline.updateProps({ color: new THREE.Color(color) })
})
})
folder.add(params, 'thickness', 0, 0.1, 0.01).onChange((thickness: number) => {
folder.add(params, 'thickness', 0, 2, 0.01).onChange((thickness: number) => {
allOutlines.forEach((outline) => {
outline.updateProps({ thickness })
})
})
folder.add(params, 'screenspace').onChange((screenspace: boolean) => {
allOutlines.forEach((outline) => {
outline.updateProps({ screenspace })
})
})
}
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -511,15 +511,23 @@ An ornamental component that extracts the geometry from its parent and displays
```tsx
export type OutlinesProps = {
/** Outline color, default: black */
color: THREE.Color
color?: THREE.Color
/** Line thickness is independent of zoom, default: false */
screenspace?: boolean
/** Outline opacity, default: 1 */
opacity: number
opacity?: number
/** Outline transparency, default: false */
transparent: boolean
transparent?: boolean
/** Outline thickness, default 0.05 */
thickness: number
thickness?: number
/** Geometry crease angle (0 === no crease), default: Math.PI */
angle: number
angle?: number
toneMapped?: boolean
polygonOffset?: boolean
polygonOffsetFactor?: number
renderOrder?: number
/** needed if `screenspace` is true */
gl?: THREE.WebGLRenderer
}
```
Expand Down
101 changes: 82 additions & 19 deletions src/core/Outlines.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { shaderMaterial } from './shaderMaterial'
import * as THREE from 'three'
import { toCreasedNormals } from 'three/examples/jsm/utils/BufferGeometryUtils'
import { shaderMaterial } from './shaderMaterial'

export type OutlinesProps = {
/** Outline color, default: black */
color: THREE.Color
color?: THREE.Color
/** Line thickness is independent of zoom, default: false */
screenspace?: boolean
/** Outline opacity, default: 1 */
opacity: number
opacity?: number
/** Outline transparency, default: false */
transparent: boolean
transparent?: boolean
/** Outline thickness, default 0.05 */
thickness: number
thickness?: number
/** Geometry crease angle (0 === no crease), default: Math.PI */
angle: number
angle?: number
toneMapped?: boolean
polygonOffset?: boolean
polygonOffsetFactor?: number
renderOrder?: number
/** needed if `screenspace` is true */
gl?: THREE.WebGLRenderer
}

export type OutlinesType = {
Expand All @@ -25,12 +33,20 @@ export type OutlinesType = {
}

const OutlinesMaterial = shaderMaterial(
{ color: new THREE.Color('black'), opacity: 1, thickness: 0.05 },
{
screenspace: false,
color: new THREE.Color('black'),
opacity: 1,
thickness: 0.05,
size: new THREE.Vector2(),
},
/* glsl */ `
#include <common>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
uniform float thickness;
uniform float screenspace;
uniform vec2 size;
void main() {
#if defined (USE_SKINNING)
#include <beginnormal_vertex>
Expand All @@ -43,14 +59,22 @@ const OutlinesMaterial = shaderMaterial(
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <project_vertex>
vec4 transformedNormal = vec4(normal, 0.0);
vec4 transformedPosition = vec4(transformed, 1.0);
vec4 tNormal = vec4(normal, 0.0);
vec4 tPosition = vec4(transformed, 1.0);
#ifdef USE_INSTANCING
transformedNormal = instanceMatrix * transformedNormal;
transformedPosition = instanceMatrix * transformedPosition;
tNormal = instanceMatrix * tNormal;
tPosition = instanceMatrix * tPosition;
#endif
vec3 newPosition = transformedPosition.xyz + transformedNormal.xyz * thickness;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
if (screenspace == 0.0) {
vec3 newPosition = tPosition.xyz + tNormal.xyz * thickness;
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
} else {
vec4 clipPosition = projectionMatrix * modelViewMatrix * tPosition;
vec4 clipNormal = projectionMatrix * modelViewMatrix * tNormal;
vec2 offset = normalize(clipNormal.xy) * thickness / size * clipPosition.w * 2.0;
clipPosition.xy += offset;
gl_Position = clipPosition;
}
}`,
/* glsl */ `
uniform vec3 color;
Expand All @@ -66,36 +90,48 @@ export function Outlines({
color = new THREE.Color('black'),
opacity = 1,
transparent = false,
screenspace = false,
toneMapped = true,
polygonOffset = false,
polygonOffsetFactor = 0,
renderOrder = 0,
thickness = 0.05,
angle = Math.PI,
gl,
}: Partial<OutlinesProps>): OutlinesType {
const group = new THREE.Group()

let shapeProps: OutlinesProps = {
color,
opacity,
transparent,
screenspace,
toneMapped,
polygonOffset,
polygonOffsetFactor,
renderOrder,
thickness,
angle,
}

function updateMesh(angle: number) {
function updateMesh(angle?: number) {
const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh & THREE.InstancedMesh
group.clear()
if (parent && parent.geometry) {
let mesh
const material = new OutlinesMaterial({ side: THREE.BackSide })
if (parent.skeleton) {
mesh = new THREE.SkinnedMesh()
mesh.material = new OutlinesMaterial({ side: THREE.BackSide })
mesh.material = material
mesh.bind(parent.skeleton, parent.bindMatrix)
group.add(mesh)
} else if (parent.isInstancedMesh) {
mesh = new THREE.InstancedMesh(parent.geometry, new OutlinesMaterial({ side: THREE.BackSide }), parent.count)
mesh = new THREE.InstancedMesh(parent.geometry, material, parent.count)
mesh.instanceMatrix = parent.instanceMatrix
group.add(mesh)
} else {
mesh = new THREE.Mesh()
mesh.material = new OutlinesMaterial({ side: THREE.BackSide })
mesh.material = material
group.add(mesh)
}
mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry
Expand All @@ -106,8 +142,35 @@ export function Outlines({
shapeProps = { ...shapeProps, ...newProps }
const mesh = group.children[0] as THREE.Mesh<THREE.BufferGeometry, THREE.Material>
if (mesh) {
const { transparent, thickness, color, opacity } = shapeProps
Object.assign(mesh.material, { transparent, thickness, color, opacity })
const {
transparent,
thickness,
color,
opacity,
screenspace,
toneMapped,
polygonOffset,
polygonOffsetFactor,
renderOrder,
} = shapeProps
const contextSize = new THREE.Vector2()
if (!gl && shapeProps.screenspace) {
console.warn('Outlines: "screenspace" requires a WebGLRenderer instance to calculate the outline size')
}
if (gl) gl.getSize(contextSize)

Object.assign(mesh.material, {
transparent,
thickness,
color,
opacity,
size: contextSize,
screenspace,
toneMapped,
polygonOffset,
polygonOffsetFactor,
})
if (renderOrder !== undefined) mesh.renderOrder = renderOrder
}
}

Expand Down

0 comments on commit 6e3d7b4

Please sign in to comment.