Mar 29, 2023

Tresjs + Vue - Hybridly Sticker Holographic

A simple sticker effect using Tresjs for Vue as part of my 3D experiments with TreeJS


Recently i saw a cool holographic sticker effect on twitter made by Baptiste Adrien, and i have decided to take a stab on trying to replicate using Vue and Trejs. It turned out to be actually harder than i thought, but i’m happy with the results so far.

One of the things that still bugs me is the Reaact community has way more active contributors and libraries for Threejs, while Vue is still catching up. I have tried to reach TresJS discord for some help and see if i could get my way around it :p, but didnt got any luck.

One of the challenges of this effect is called the MeshTransmissionMaterial that is not available on Tresjs, so i had to use the MeshPhysicalMaterial from pmndrs/drei-vanilla and try to get the same effect using the available materials. But not everything is lost, TresJs provides a good abstraction around ThreeJS, and i could get a result closer to React Version.

The MeshTransmissionMaterial makes the base mesh transparent, so it could reflect the background & items inside it, creating the desired effect, little i know about Shaders & 3D this was indeed a fun journey to learn more about it.

Vue + TresJSSection titled Vue + TresJS

The following Demo is using Tresjs + Vue, notice that badge absorbes way more light than the react version, still unsure why.

Vue implementationSection titled Vue implementation

But lets see the actual code! here is the main scene, for the Vue version.

<script setup lang="ts">
import { OrbitControls, Environment, useGLTF } from '@tresjs/cientos'
import { TresCanvas } from '@tresjs/core'
import Stickers from './vue-sticker.vue'
import MeshTransmissionMaterial from './vue-transmission-material.vue'
const { nodes } = await useGLTF('/img/crafts/hybridly-stickers/pack.glb')
</script>
<template>
	<div class="relative size-[300px] lg:size-[600px]">
		<TresCanvas
			style="width: 100%; height: 100%;"
			:alpha="true"
			:antialias="true"
			power-preference="high-performance"
			:shadows="true"
		>
			<suspense>
				<TresMesh
					:geometry="nodes.Object_4.geometry"
					:rotation="[Math.PI / 2, 0, 0]"
					:cast-shadow="true"
					:receive-shadow="true"
				>
					<MeshTransmissionMaterial />
				</TresMesh>
			</suspense>

			<suspense>
				<stickers />
			</suspense>

			<TresAmbientLight
				:intensity="1.5"
			/>
			<TresPerspectiveCamera
				visible
				:position="[0, 0, 6]"
				:fov="45"
			/>
			<OrbitControls
				:target="[0, 0, 0]"
			/>
			<suspense>
				<Environment
					:files="'/img/crafts/hybridly-stickers/env2.hdr'"
					:blur="0"
					:background="false"
				/>
			</suspense>
		</TresCanvas>
	</div>
</template>

Here you can find the stickers inside the main inside

<script setup lang="ts">
import { useTexture } from '@tresjs/core'
import { DoubleSide } from 'three'
const texture = await useTexture(['/img/crafts/hybridly-stickers/hybridly-foil.png'])
</script>
<template>
	<suspense>
		<TresMesh>
			<TresPlaneGeometry />
			<TresMeshPhysicalMaterial
				:map="texture"
				:transparent="true"
				:clearcoat="1"
				:roughness="1"
				:metalness="0.8"
				:side="DoubleSide"
			/>
		</TresMesh>
	</suspense>
</template>

And here the Transmision Material, a bit messy and buggy, so if you reading this and have a better solution, please lets get in touch! :p

<script setup lang="ts">

import { shallowRef } from 'vue'
import { MeshTransmissionMaterial, MeshDiscardMaterial } from '@pmndrs/vanilla'
import { useFBO } from '@tresjs/cientos'
import { useRenderLoop, useTresContext } from '@tresjs/core'
import { BackSide, NoToneMapping, DoubleSide } from 'three'
import type { TresColor, TresObject } from '@tresjs/core'
import type { Camera, Side, Texture, WebGLRenderTarget } from 'three'

const MeshTransmissionMaterialClass = shallowRef()
const { extend, scene, renderer, camera } = useTresContext()
const parent = shallowRef<TresObject>()

// Configs to move to props
const backside = true
const backsideThickness = 0
const thickness = 0
const backsideEnvMapIntensity = 0
const fboResolution = 1500

extend({ MeshTransmissionMaterial })

/**
 * Finds the parent mesh using the specified material UUID.
 *
 * @param {THREE.Scene} scene - The Three.js scene to search.
 * @param {string} materialUuid - The UUID of the material.
 * @returns {THREE.Mesh | undefined} - The mesh using the material, or undefined if not found.
 */
function findMeshByMaterialUuid(scene: TresObject, materialUuid: string): TresObject {
	let foundMesh

	scene.traverse((object: TresObject) => {
		if (object.isMesh && object.material && object.material.uuid === materialUuid) {
			foundMesh = object
		}
	})

	return foundMesh as unknown as TresObject
}

const discardMaterial = new MeshDiscardMaterial()

const { onLoop } = useRenderLoop()

onMounted(async() => {
	await nextTick()
	parent.value = findMeshByMaterialUuid(
		scene.value as unknown as TresObject,
		MeshTransmissionMaterialClass.value.uuid
	)
})

const fboBack = useFBO({ width: fboResolution, height: fboResolution }) as unknown as Ref<WebGLRenderTarget<Texture>>
const fboMain = useFBO({ width: fboResolution, height: fboResolution }) as unknown as Ref<WebGLRenderTarget<Texture>>

let oldBg
let oldEnvMapIntensity
let oldTone

onLoop(({ elapsed }) => {
	MeshTransmissionMaterialClass.value.time = elapsed

	if (MeshTransmissionMaterialClass.value.buffer === fboMain.value.texture) {
		if (parent.value) {
			// Save defaults
			oldTone = renderer.value.toneMapping
			oldBg = scene.value.background
			oldEnvMapIntensity = MeshTransmissionMaterialClass.value.envMapIntensity

			// Switch off tonemapping lest it double tone maps
			// Save the current background and set the HDR as the new BG
			// Use discardmaterial, the parent will be invisible, but it's shadows will still be cast
			// renderer.value.toneMapping = NoToneMapping
			// TODO: Check this with props
			if (false) {
				scene.value.background = null
			}

			parent.value.material = discardMaterial
			// throw new Error('Stop')

			if (backside) {
				// Render into the backside buffer
				renderer.value.setRenderTarget(fboBack.value)
				renderer.value.render(scene.value, camera.value as Camera)
				// And now prepare the material for the main render using the backside buffer
				parent.value.material = MeshTransmissionMaterialClass.value
				// TODO: This also causes some errors
				// parent.value.material.buffer = fboBack.value.texture
				parent.value.material.thickness = backsideThickness
				parent.value.material.side = BackSide
				parent.value.material.envMapIntensity = backsideEnvMapIntensity
			}

			// Render into the main buffer
			renderer.value.setRenderTarget(fboMain.value)
			renderer.value.render(scene.value, camera.value as Camera)

			parent.value.material = MeshTransmissionMaterialClass.value
			parent.value.material.thickness = thickness
			parent.value.material.side = DoubleSide
			// TODO: For some reason this makes the material really shinny, and i dont know why.
			// parent.value.material.buffer = fboMain.value.texture
			parent.value.material.envMapIntensity = oldEnvMapIntensity

			// Set old state back
			scene.value.background = oldBg
			renderer.value.setRenderTarget(null)
			renderer.value.toneMapping = oldTone
		}
	}
})

defineExpose({ root: MeshTransmissionMaterialClass, constructor: MeshTransmissionMaterial })

</script>

<template>
	<TresMeshTransmissionMaterial
		ref="MeshTransmissionMaterialClass"
		:buffer="fboMain.texture"
		:transmission="0"
		:_transmission="1"
		:anisotropic-blur="0.1"
		:thickness="0"
		:side="DoubleSide"
	/>
</template>

React Three Fiber implementationSection titled React Three Fiber implementation

And here you can find the same demo but with React Three Fiber, while the Vue implementation looks really close to this one, i cannot seem to get the same results, maybe im missing some configuration, or its just the way React Three Fiber handles the materials & defaults.

import { Environment, MeshTransmissionMaterial, OrbitControls, useGLTF, useTexture } from '@react-three/drei'
import { Canvas } from '@react-three/fiber'
import React from 'react'
import { DoubleSide } from 'three'

const Stickers = () => {
	const texture = useTexture('/img/crafts/hybridly-stickers/hybridly-foil.png')
	return (
		<>
			<mesh>
				<planeGeometry />
				<meshPhysicalMaterial
					map={texture}
					transparent
					clearcoat={1}
					roughness={0}
					side={DoubleSide}
				/>
			</mesh>
		</>
	)
}

export const Scene = () => {
	const { nodes } = useGLTF('/img/crafts/hybridly-stickers/pack.glb')

	return (
		<div className="relative size-[300px] lg:size-[600px]">
			<Canvas
				shadows
				camera={{ position: [0, 0, 6], fov: 45 }}
			>
				<OrbitControls
					target={[0, 0, 0]}
				/>
				<group dispose={null}>
					<mesh
						castShadow
						receiveShadow
						geometry={nodes.Object_4.geometry}
						rotation={[Math.PI / 2, 0, 0]}
					>
						<MeshTransmissionMaterial />
					</mesh>
				</group>

				<Stickers />

				<ambientLight intensity={2} />
				<Environment preset="city" blur={0}/>
			</Canvas>
		</div>
	)
}

export default Scene

Once again, such a nice challenge & learning experience, i hope you enjoyed this post, and if you have any suggestions or tips, please let me know! All credits to Baptiste Adrien & Vercel for the amazing ideia!