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!