import * as THREE from 'three' import * as React from 'react' import { shaderMaterial } from './shaderMaterial' import { extend, applyProps, ReactThreeFiber, useThree } from '@react-three/fiber' import { toCreasedNormals } from 'three-stdlib' import { version } from '../helpers/constants' const OutlinesMaterial = /* @__PURE__ */ shaderMaterial( { screenspace: false, color: /* @__PURE__ */ new THREE.Color('black'), opacity: 1, thickness: 0.05, size: /* @__PURE__ */ new THREE.Vector2(), }, `#include #include #include uniform float thickness; uniform float screenspace; uniform vec2 size; void main() { #if defined (USE_SKINNING) #include #include #include #include #include #endif #include #include #include #include vec4 tNormal = vec4(normal, 0.0); vec4 tPosition = vec4(transformed, 1.0); #ifdef USE_INSTANCING tNormal = instanceMatrix * tNormal; tPosition = instanceMatrix * tPosition; #endif 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; } }`, `uniform vec3 color; uniform float opacity; void main(){ gl_FragColor = vec4(color, opacity); #include #include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}> }` ) type OutlinesProps = JSX.IntrinsicElements['group'] & { /** Outline color, default: black */ color?: ReactThreeFiber.Color /** Line thickness is independent of zoom, default: false */ screenspace?: boolean /** Outline opacity, default: 1 */ opacity?: number /** Outline transparency, default: false */ transparent?: boolean /** Outline thickness, default 0.05 */ thickness?: number /** Geometry crease angle (0 === no crease), default: Math.PI */ angle?: number toneMapped?: boolean polygonOffset?: boolean polygonOffsetFactor?: number renderOrder?: number } export function Outlines({ color = 'black', opacity = 1, transparent = false, screenspace = false, toneMapped = true, polygonOffset = false, polygonOffsetFactor = 0, renderOrder = 0, thickness = 0.05, angle = Math.PI, ...props }: OutlinesProps) { const ref = React.useRef() const [material] = React.useState(() => new OutlinesMaterial({ side: THREE.BackSide })) const { gl } = useThree() const contextSize = gl.getDrawingBufferSize(new THREE.Vector2()) React.useMemo(() => extend({ OutlinesMaterial }), []) const oldAngle = React.useRef(0) const oldGeometry = React.useRef() React.useLayoutEffect(() => { const group = ref.current if (!group) return const parent = group.parent as THREE.Mesh & THREE.SkinnedMesh & THREE.InstancedMesh if (parent && parent.geometry) { if (oldAngle.current !== angle || oldGeometry.current !== parent.geometry) { oldAngle.current = angle oldGeometry.current = parent.geometry // Remove old mesh let mesh = group.children?.[0] as any if (mesh) { if (angle) mesh.geometry.dispose() group.remove(mesh) } if (parent.skeleton) { mesh = new THREE.SkinnedMesh() mesh.material = material mesh.bind(parent.skeleton, parent.bindMatrix) group.add(mesh) } else if (parent.isInstancedMesh) { mesh = new THREE.InstancedMesh(parent.geometry, material, parent.count) mesh.instanceMatrix = parent.instanceMatrix group.add(mesh) } else { mesh = new THREE.Mesh() mesh.material = material group.add(mesh) } mesh.geometry = angle ? toCreasedNormals(parent.geometry, angle) : parent.geometry } } }) React.useLayoutEffect(() => { const group = ref.current if (!group) return const mesh = group.children[0] as THREE.Mesh if (mesh) { mesh.renderOrder = renderOrder applyProps(mesh.material as any, { transparent, thickness, color, opacity, size: contextSize, screenspace, toneMapped, polygonOffset, polygonOffsetFactor, }) } }) React.useEffect(() => { return () => { // Dispose everything on unmount const group = ref.current if (!group) return const mesh = group.children[0] as THREE.Mesh if (mesh) { if (angle) mesh.geometry.dispose() group.remove(mesh) } } }, []) return } {...props} /> }