Skip to content

Commit

Permalink
feat(Outline): add component, demo, docs (#532)
Browse files Browse the repository at this point in the history
* squash

* chore: lint

* feat(Outline): add component, demo, docs

* chore(shaderMaterial): destructure import

* docs(Outline): update playground demo

* chore: lint

---------

Co-authored-by: alvarosabu <[email protected]>
  • Loading branch information
andretchen0 and alvarosabu authored Dec 4, 2024
1 parent ad916a2 commit 5bae4de
Show file tree
Hide file tree
Showing 10 changed files with 286 additions and 15 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export default defineConfig({
{ text: 'Edges', link: '/guide/abstractions/edges' },
{ text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' },
{ text: 'AnimatedSprite', link: '/guide/abstractions/animated-sprite' },
{ text: 'Outline', link: '/guide/abstractions/outline' },
{ text: 'Image', link: '/guide/abstractions/image' },
{ text: 'Billboard', link: '/guide/abstractions/billboard' },
],
Expand Down
23 changes: 23 additions & 0 deletions docs/.vitepress/theme/components/OutlineDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { OrbitControls, Outline } from '@tresjs/cientos'
</script>

<template>
<TresCanvas clear-color="#4f4f4f">
<TresPerspectiveCamera />
<OrbitControls />
<TresAmbientLight :intensity="3.14" />
<TresPointLight :intensity="50" :position="[2, 2, 0]" />
<TresMesh :position-x="-0.75">
<TresBoxGeometry />
<TresMeshPhongMaterial />
<Outline :thickness="7.5" color="#82dbc5" />
</TresMesh>
<TresMesh :position-x="0.75">
<TresSphereGeometry :args="[0.5]" />
<TresMeshPhongMaterial />
<Outline :thickness="7.5" color="#fbb03b" />
</TresMesh>
</TresCanvas>
</template>
1 change: 1 addition & 0 deletions docs/component-list/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export default [
},
{ text: 'Sampler', link: '/guide/abstractions/sampler' },
{ text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' },
{ text: 'Outline', link: '/guide/abstractions/outline' },
{ text: 'Image', link: '/guide/abstractions/image' },
{ text: 'Billboard', link: '/guide/abstractions/billboard' },
],
Expand Down
22 changes: 22 additions & 0 deletions docs/guide/abstractions/outline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Outline

`<Outline />` creates an inverted-hull outline using its parent's geometry. Supported parents are `<TresMesh>` and `<TresSkinnedMesh>`.

<DocsDemo>
<OutlineDemo />
</DocsDemo>

## Usage

<<< @/.vitepress/theme/components/OutlineDemo.vue

## Props

| Props | Description | Default |
|--------------|--------------------------------------------------------------------| ------- |
| color | Outline color | `'black'` |
| screenspace | Whether line thickness is independent of zoom | `false` |
| opacity | Outline opacity | `1` |
| transparent | Outline transparency | `false` |
| thickness | Outline thickness | `0.05` |
| angle | Geometry crease angle (`0` is no crease). See [BufferGeometryUtils.toCreasedNormals](https://threejs.org/docs/#examples/en/utils/BufferGeometryUtils.toCreasedNormals) | `Math.PI` |
31 changes: 31 additions & 0 deletions playground/vue/src/pages/abstractions/OutlineDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { OrbitControls, Outline } from '@tresjs/cientos'
const thickness = shallowRef(1)
const color = shallowRef('black')
let elapsed = 0
let intervalId: ReturnType<typeof setInterval>
onMounted(() => {
intervalId = setInterval(() => {
elapsed += 0.01 * 1000 / 30
thickness.value = (1 + Math.sin(elapsed)) * 5
color.value = Math.cos(elapsed) > 0 ? 'blue' : 'orange'
}, 1000 / 30)
})
onUnmounted(() => clearInterval(intervalId))
</script>

<template>
<TresCanvas>
<TresPerspectiveCamera :position="[0, 0, 10]" />
<OrbitControls />
<TresMesh>
<TresTorusKnotGeometry />
<TresMeshBasicMaterial />
<Outline :thickness="thickness" :color="color" />
</TresMesh>
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/vue/src/router/routes/abstractions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ export const abstractionsRoutes = [
name: 'AnimatedSprite',
component: () => import('../../pages/abstractions/AnimatedSpriteDemo.vue'),
},
{
path: '/abstractions/outline',
name: 'Outline',
component: () => import('../../pages/abstractions/OutlineDemo.vue'),
},
{
path: '/abstractions/image',
name: 'Image',
Expand Down
59 changes: 59 additions & 0 deletions src/core/abstractions/Outline/OutlineMaterialImpl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Color, Vector2 } from 'three'
import { shaderMaterial } from '../../../utils/shaderMaterial'

// NOTE: Source
// https://github.com/pmndrs/drei/blob/master/src/core/Outlines.tsx

const OutlineMaterialImpl = shaderMaterial(
{
screenspace: false,
color: new Color('black'),
opacity: 1,
thickness: 0.05,
size: new Vector2(1, 1),
},
`#include <common>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
uniform float thickness;
uniform bool screenspace;
uniform vec2 size;
void main() {
#if defined (USE_SKINNING)
#include <beginnormal_vertex>
#include <morphnormal_vertex>
#include <skinbase_vertex>
#include <skinnormal_vertex>
#include <defaultnormal_vertex>
#endif
#include <begin_vertex>
#include <morphtarget_vertex>
#include <skinning_vertex>
#include <project_vertex>
vec4 tNormal = vec4(normal, 0.0);
vec4 tPosition = vec4(transformed, 1.0);
#ifdef USE_INSTANCING
tNormal = instanceMatrix * tNormal;
tPosition = instanceMatrix * tPosition;
#endif
if (screenspace) {
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 <tonemapping_fragment>
#include <colorspace_fragment>
}`,
)

export default OutlineMaterialImpl
126 changes: 126 additions & 0 deletions src/core/abstractions/Outline/component.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<script setup lang="ts">
import type { TresColor } from '@tresjs/core'
import { normalizeColor, useTres } from '@tresjs/core'
import type { BufferGeometry, Group } from 'three'
import { BackSide, InstancedMesh, Mesh, SkinnedMesh, Vector2 } from 'three'
import { onMounted, onUnmounted, shallowRef, watch } from 'vue'
import OutlineMaterialImpl from './OutlineMaterialImpl'
import { toCreasedNormals } from 'three-stdlib'
// NOTE: Source
// https://github.com/pmndrs/drei/blob/master/src/core/Outlines.tsx
interface OutlineProps {
/** Outline color, default: black */
color?: TresColor
/** 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 (-1 === no crease), default: Math.PI, See [BufferGeometryUtils.toCreasedNormals](https://threejs.org/docs/#examples/en/utils/BufferGeometryUtils.toCreasedNormals) */
angle?: number
toneMapped?: boolean
polygonOffset?: boolean
polygonOffsetFactor?: number
renderOrder?: number
}
const props = withDefaults(defineProps<OutlineProps>(), {
color: 'black',
opacity: 1,
transparent: false,
screenspace: false,
toneMapped: true,
polygonOffset: false,
polygonOffsetFactor: 0,
renderOrder: 0,
thickness: 0.05,
angle: Math.PI,
})
const groupRef = shallowRef()
defineExpose({ instance: groupRef })
const material = new OutlineMaterialImpl({ ...props })
const contextSize = new Vector2(1, 1)
let oldAngle = 0
let oldGeometry: BufferGeometry | null = null
function updateMesh(group: Group) {
const parent = group.parent as Mesh & SkinnedMesh & InstancedMesh
if (!parent || !parent.geometry) { return }
if (oldAngle !== props.angle || oldGeometry !== parent.geometry) {
oldAngle = props.angle
oldGeometry = parent.geometry
// NOTE: Remove old mesh
let mesh = group.children?.[0] as any
if (mesh) {
if (props.angle) { mesh.geometry.dispose() }
group.remove(mesh)
}
if (parent.skeleton) {
mesh = new SkinnedMesh()
mesh.material = material
mesh.bind(parent.skeleton, parent.bindMatrix)
group.add(mesh)
}
else if (parent.isInstancedMesh) {
mesh = new InstancedMesh(parent.geometry, material, parent.count)
mesh.instanceMatrix = parent.instanceMatrix
group.add(mesh)
}
else {
mesh = new Mesh()
mesh.material = material
group.add(mesh)
}
mesh.geometry = props.angle ? toCreasedNormals(parent.geometry, props.angle) : parent.geometry
}
}
function updateMaterial() {
material.side = BackSide
material.transparent = props.transparent
material.thickness = props.thickness
material.color = normalizeColor(props.color)
material.opacity = props.opacity
material.size = contextSize
material.screenspace = props.screenspace
material.toneMapped = props.toneMapped
material.polygonOffset = props.polygonOffset
material.polygonOffsetFactor = props.polygonOffsetFactor
}
const sizes = useTres().sizes
watch(() => [sizes.width.value, sizes.height.value], ([w, h]) => {
contextSize.set(w, h)
})
watch(() => [props.angle], () => {
if (groupRef.value) { updateMesh(groupRef.value) }
})
watch(() => [props.transparent, props.thickness, props.color, props.opacity, contextSize, props.screenspace, props.toneMapped, props.polygonOffset, props.polygonOffsetFactor], () => updateMaterial(), { immediate: true },
)
onMounted(() => updateMesh(groupRef.value))
onUnmounted(() => {
const mesh = groupRef.value?.children[0] as Mesh
if (mesh) {
mesh.geometry.dispose()
material.dispose()
mesh.removeFromParent()
}
})
</script>

<template>
<TresGroup ref="groupRef" />
</template>
2 changes: 2 additions & 0 deletions src/core/abstractions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Image from './Image/component.vue'
import Lensflare from './Lensflare/component.vue'
import Levioso from './Levioso.vue'
import MouseParallax from './MouseParallax.vue'
import Outline from './Outline/component.vue'
import PositionalAudio from './PositionalAudio.vue'
import Reflector from './Reflector.vue'
import Text3D from './Text3D.vue'
Expand All @@ -26,6 +27,7 @@ export {
Lensflare,
Levioso,
MouseParallax,
Outline,
PositionalAudio,
Reflector,
Sampler,
Expand Down
31 changes: 16 additions & 15 deletions src/utils/shaderMaterial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,39 +22,40 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

import * as THREE from 'three'
import type { Color, CubeTexture, Matrix3, Matrix4, Quaternion, Texture, Vector2, Vector3, Vector4 } from 'three'
import { MathUtils, ShaderMaterial, UniformsUtils } from 'three'

export function shaderMaterial(
uniforms: {
[name: string]:
| THREE.CubeTexture
| THREE.Texture
| CubeTexture
| Texture
| Int32Array
| Float32Array
| THREE.Matrix4
| THREE.Matrix3
| THREE.Quaternion
| THREE.Vector4
| THREE.Vector3
| THREE.Vector2
| THREE.Color
| Matrix4
| Matrix3
| Quaternion
| Vector4
| Vector3
| Vector2
| Color
| number
| boolean
| Array<any>
| null
},
vertexShader: string,
fragmentShader: string,
onInit?: (material?: THREE.ShaderMaterial) => void,
onInit?: (material?: ShaderMaterial) => void,
) {
const material = class extends THREE.ShaderMaterial {
const material = class extends ShaderMaterial {
public key: string = ''
constructor(parameters = {}) {
const entries = Object.entries(uniforms)
// Create unforms and shaders
super({
uniforms: entries.reduce((acc, [name, value]) => {
const uniform = THREE.UniformsUtils.clone({ [name]: { value } })
const uniform = UniformsUtils.clone({ [name]: { value } })
return {
...acc,
...uniform,
Expand All @@ -76,7 +77,7 @@ export function shaderMaterial(
// Call onInit
if (onInit) { onInit(this) }
}
} as unknown as typeof THREE.ShaderMaterial & { key: string }
material.key = THREE.MathUtils.generateUUID()
} as unknown as typeof ShaderMaterial & { key: string }
material.key = MathUtils.generateUUID()
return material
}

0 comments on commit 5bae4de

Please sign in to comment.