Skip to content
This repository was archived by the owner on Feb 1, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"build": "vite build",
"release": "release-it",
"lint": "eslint .",
"typecheck": "vue-tsc --noEmit",
"lint:fix": "eslint . --fix",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
Expand All @@ -70,10 +71,10 @@
},
"devDependencies": {
"@release-it/conventional-changelog": "^10.0.1",
"@tresjs/core": "5.0.0-next.2",
"@tresjs/core": "5.0.0-next.5",
"@tresjs/eslint-config": "^1.4.0",
"@types/node": "^22.10.5",
"@types/three": "^0.176.0",
"@types/node": "^24.0.3",
"@types/three": "^0.177.0",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"@vitejs/plugin-vue": "^5.2.4",
Expand Down
2 changes: 1 addition & 1 deletion playground/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tresjs/core": "5.0.0-next.2",
"@tresjs/core": "https://pkg.pr.new/@tresjs/core@bb01f3d",
"vue-router": "^4.5.0"
},
"devDependencies": {
Expand Down
324 changes: 171 additions & 153 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/core/abstractions/ScreenSizer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const size = computed(() => ({ width: sizes.width.value, height: sizes.height.va

useLoop().onBeforeRender(({ camera }) => {
const obj = innerRef.value
if (!obj) { return }
const sf = calculateScaleFactor(obj.getWorldPosition(worldPos), 1, camera, size.value)
if (!obj || !camera.value) { return }
const sf = calculateScaleFactor(obj.getWorldPosition(worldPos), 1, camera.value, size.value)
obj.scale.setScalar(sf)
})

Expand Down
6 changes: 3 additions & 3 deletions src/core/abstractions/useSurfaceSampler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from 'three'
import { MeshSurfaceSampler } from 'three-stdlib'
import { ref } from 'vue'
import type { InstancedMesh, Mesh } from 'three'
import type { InstancedMesh, Mesh, Object3DEventMap } from 'three'

export interface useSurfaceSamplerProps {
/*
Expand Down Expand Up @@ -67,7 +67,7 @@ type TransformPayload = SamplePayload & {
* This object's matrix will be updated after transforming & it will be used
* to set the instance's matrix.
*/
dummy: Object3D
dummy: Object3D<Object3DEventMap>
/**
* The mesh that's initially passed to the sampler.
* Use this if you need to apply transforms from your mesh to your instances
Expand Down Expand Up @@ -101,7 +101,7 @@ export const useSurfaceSampler = (
const position = new Vector3()
const normal = new Vector3()
const color = new Color()
const dummy = new Object3D()
const dummy = new Object3D<Object3DEventMap>()

mesh.updateMatrixWorld(true)

Expand Down
6 changes: 3 additions & 3 deletions src/core/loaders/useFBX/component.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { TresObject } from '@tresjs/core'
import { Mesh } from 'three'
import { useFBX } from '.'

export interface FBXModelProps {
Expand Down Expand Up @@ -52,8 +52,8 @@ defineExpose({
const model = await useFBX(props.path as string)

if (props.castShadow || props.receiveShadow) {
model.traverse((child: TresObject) => {
if (child.isMesh) {
model.traverse((child) => {
if (child instanceof Mesh) {
child.castShadow = props.castShadow
child.receiveShadow = props.receiveShadow
}
Expand Down
10 changes: 5 additions & 5 deletions src/core/loaders/useGLTF/component.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { watch } from 'vue'
import type { TresObject } from '@tresjs/core'
import { type Group, Mesh } from 'three'
import { useGLTF } from '.'

export interface GLTFModelProps {
Expand Down Expand Up @@ -77,14 +77,14 @@ const { state, isLoading } = useGLTF(props.path as string, {
decoderPath: props.decoderPath,
})

let modelObject: TresObject | null = null
let modelObject: Group | null = null

watch(state, (newVal) => {
if (newVal?.scene) {
modelObject = newVal.scene
if (props.castShadow || props.receiveShadow) {
modelObject.traverse((child: TresObject) => {
if (child.isMesh) {
if ((props.castShadow || props.receiveShadow) && modelObject) {
modelObject.traverse((child) => {
if (child instanceof Mesh) {
child.castShadow = props.castShadow
child.receiveShadow = props.receiveShadow
}
Expand Down
7 changes: 3 additions & 4 deletions src/core/loaders/useGLTF/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TresLoader, TresLoaderOptions } from '@tresjs/core'
import type { TresLoader, TresLoaderOptions, TresObject } from '@tresjs/core'
import { buildGraph, useLoader } from '@tresjs/core'

import { computed, type MaybeRef } from 'vue'
Expand Down Expand Up @@ -51,13 +51,12 @@ export function useGLTF(path: MaybeRef<string>, options?: UseGLTFOptions) {
}

const result = useLoader(GLTFLoader, path, useLoaderOptions)

const nodes = computed(() => {
return result.state.value?.scene ? buildGraph(result.state.value?.scene).nodes : {}
return result.state.value?.scene ? buildGraph(result.state.value?.scene as unknown as TresObject).nodes : {}
})

const materials = computed(() => {
return result.state.value?.scene ? buildGraph(result.state.value?.scene).materials : {}
return result.state.value?.scene ? buildGraph(result.state.value?.scene as unknown as TresObject).materials : {}
})

return {
Expand Down
188 changes: 96 additions & 92 deletions src/core/materials/meshReflectionMaterial/index.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<!-- eslint-disable vue/attribute-hyphenation -->
<script setup lang="ts">
import { logWarning, useLoop, useTres } from '@tresjs/core'
import type {
Texture,
} from 'three'
import {
Color,
DepthTexture,
Expand All @@ -14,19 +17,14 @@ import {
Vector2,
Vector3,
Vector4,
WebGLRenderer,
WebGLRenderTarget,
} from 'three'
import { computed, onBeforeUnmount, shallowRef, watch } from 'vue'
import { computed, onBeforeUnmount, shallowRef, toValue, watch } from 'vue'
import type { TresColor } from '@tresjs/core'
import type {
Camera,
Object3D,
Scene,
Texture,
WebGLRenderer,
} from 'three'
import { BlurPass } from './BlurPass'
import { MeshReflectionMaterial } from './material'
import { WebGPURenderer } from 'three/webgpu'

export interface MeshReflectionMaterialProps {

Expand Down Expand Up @@ -204,87 +202,6 @@ const fboBlur = new WebGLRenderTarget(
},
)

function onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, object: Object3D) {
invalidate()

const currentXrEnabled = renderer.xr.enabled
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate

state.reflectorWorldPosition.setFromMatrixPosition(object.matrixWorld)
state.cameraWorldPosition.setFromMatrixPosition(camera.matrixWorld as Matrix4)
state.rotationMatrix.extractRotation(object.matrixWorld)
state.normal.set(0, 0, 1)
state.normal.applyMatrix4(state.rotationMatrix)
state.reflectorWorldPosition.addScaledVector(state.normal, props.reflectorOffset)
state.view.subVectors(state.reflectorWorldPosition, state.cameraWorldPosition)

// NOTE: Avoid rendering when reflector is facing away
if (state.view.dot(state.normal) > 0) { return }

// NOTE: Avoid re-rendering the reflective object.
object.visible = false

state.view.reflect(state.normal).negate()
state.view.add(state.reflectorWorldPosition)
state.rotationMatrix.extractRotation(camera.matrixWorld as Matrix4)
state.lookAtPosition.set(0, 0, -1)
state.lookAtPosition.applyMatrix4(state.rotationMatrix)
state.lookAtPosition.add(state.cameraWorldPosition)
state.target.subVectors(state.reflectorWorldPosition, state.lookAtPosition)
state.target.reflect(state.normal).negate()
state.target.add(state.reflectorWorldPosition)
state.virtualCamera.position.copy(state.view)
state.virtualCamera.up.set(0, 1, 0)
state.virtualCamera.up.applyMatrix4(state.rotationMatrix)
state.virtualCamera.up.reflect(state.normal)
state.virtualCamera.lookAt(state.target)
state.virtualCamera.far = (camera as PerspectiveCamera).far
state.virtualCamera.updateMatrixWorld()
state.virtualCamera.projectionMatrix.copy((camera as PerspectiveCamera).projectionMatrix)

// NOTE: Update the texture matrix
state.textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0)
state.textureMatrix.multiply(state.virtualCamera.projectionMatrix)
state.textureMatrix.multiply(state.virtualCamera.matrixWorldInverse)
state.textureMatrix.multiply(object.matrixWorld)

// NOTE: Now update projection matrix with new clip reflectorPlane, implementing code from: http://www.terathon.com/code/oblique.html
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
state.reflectorPlane.setFromNormalAndCoplanarPoint(state.normal, state.reflectorWorldPosition)
state.reflectorPlane.applyMatrix4(state.virtualCamera.matrixWorldInverse)
state.clipPlane.set(
state.reflectorPlane.normal.x,
state.reflectorPlane.normal.y,
state.reflectorPlane.normal.z,
state.reflectorPlane.constant,
)
const projectionMatrix = state.virtualCamera.projectionMatrix
state.q.x = (Math.sign(state.clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0]
state.q.y = (Math.sign(state.clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5]
state.q.z = -1.0
state.q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]
// NOTE: Calculate the scaled reflectorPlane vector
state.clipPlane.multiplyScalar(2.0 / state.clipPlane.dot(state.q))
// NOTE: Replacing the third row of the projection matrix
projectionMatrix.elements[2] = state.clipPlane.x
projectionMatrix.elements[6] = state.clipPlane.y
projectionMatrix.elements[10] = state.clipPlane.z + 1.0
projectionMatrix.elements[14] = state.clipPlane.w

renderer.shadowMap.autoUpdate = false
renderer.setRenderTarget(fboSharp)
if (!renderer.autoClear) { renderer.clear() }

renderer.render(scene, state.virtualCamera)
blurpass.render(renderer, fboSharp, fboBlur)

// NOTE: Restore the previous render target and material
renderer.xr.enabled = currentXrEnabled
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate
object.visible = true
renderer.setRenderTarget(null)
}

watch(
() => [props.resolution],
() => {
Expand Down Expand Up @@ -360,12 +277,99 @@ onBeforeUnmount(() => {
fboBlur.dispose()
blurpass.dispose()
})
const { onBeforeRender } = useLoop()

useLoop().onBeforeRender(({ renderer, scene, camera }) => {
onBeforeRender(({ renderer, scene, camera }) => {
const parent = (materialRef.value as any)?.__tres?.parent
if (!parent) { return }
onBeforeRender(renderer, scene, camera, parent)
invalidate()
if (renderer instanceof WebGPURenderer) {
console.warn('MeshReflectionMaterial: WebGPURenderer is not supported yet')
return
}
if (renderer instanceof WebGLRenderer) {
invalidate()

const currentXrEnabled = renderer.xr.enabled
const currentShadowAutoUpdate = renderer.shadowMap.autoUpdate

state.reflectorWorldPosition.setFromMatrixPosition(parent.matrixWorld)
state.cameraWorldPosition.setFromMatrixPosition(camera.value?.matrixWorld as Matrix4)
state.rotationMatrix.extractRotation(parent.matrixWorld)
state.normal.set(0, 0, 1)
state.normal.applyMatrix4(state.rotationMatrix)
state.reflectorWorldPosition.addScaledVector(state.normal, props.reflectorOffset)
state.view.subVectors(state.reflectorWorldPosition, state.cameraWorldPosition)

// NOTE: Avoid rendering when reflector is facing away
if (state.view.dot(state.normal) > 0) { return }

// NOTE: Avoid re-rendering the reflective object.
parent.visible = false

state.view.reflect(state.normal).negate()
state.view.add(state.reflectorWorldPosition)
state.rotationMatrix.extractRotation(camera.value?.matrixWorld as Matrix4)
state.lookAtPosition.set(0, 0, -1)
state.lookAtPosition.applyMatrix4(state.rotationMatrix)
state.lookAtPosition.add(state.cameraWorldPosition)
state.target.subVectors(state.reflectorWorldPosition, state.lookAtPosition)
state.target.reflect(state.normal).negate()
state.target.add(state.reflectorWorldPosition)
state.virtualCamera.position.copy(state.view)
state.virtualCamera.up.set(0, 1, 0)
state.virtualCamera.up.applyMatrix4(state.rotationMatrix)
state.virtualCamera.up.reflect(state.normal)
state.virtualCamera.lookAt(state.target)
state.virtualCamera.far = (camera.value as PerspectiveCamera).far
state.virtualCamera.updateMatrixWorld()
state.virtualCamera.far = (camera.value as PerspectiveCamera).far
state.virtualCamera.projectionMatrix.copy((camera.value as PerspectiveCamera).projectionMatrix)

// NOTE: Update the texture matrix
state.textureMatrix.set(0.5, 0.0, 0.0, 0.5, 0.0, 0.5, 0.0, 0.5, 0.0, 0.0, 0.5, 0.5, 0.0, 0.0, 0.0, 1.0)
state.textureMatrix.multiply(state.virtualCamera.projectionMatrix)
state.textureMatrix.multiply(state.virtualCamera.matrixWorldInverse)
state.textureMatrix.multiply(parent.matrixWorld)

// NOTE: Now update projection matrix with new clip reflectorPlane, implementing code from: http://www.terathon.com/code/oblique.html
// Paper explaining this technique: http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
state.reflectorPlane.setFromNormalAndCoplanarPoint(state.normal, state.reflectorWorldPosition)
state.reflectorPlane.applyMatrix4(state.virtualCamera.matrixWorldInverse)
state.clipPlane.set(
state.reflectorPlane.normal.x,
state.reflectorPlane.normal.y,
state.reflectorPlane.normal.z,
state.reflectorPlane.constant,
)
const projectionMatrix = state.virtualCamera.projectionMatrix
state.q.x = (Math.sign(state.clipPlane.x) + projectionMatrix.elements[8]) / projectionMatrix.elements[0]
state.q.y = (Math.sign(state.clipPlane.y) + projectionMatrix.elements[9]) / projectionMatrix.elements[5]
state.q.z = -1.0
state.q.w = (1.0 + projectionMatrix.elements[10]) / projectionMatrix.elements[14]
// NOTE: Calculate the scaled reflectorPlane vector
state.clipPlane.multiplyScalar(2.0 / state.clipPlane.dot(state.q))
// NOTE: Replacing the third row of the projection matrix
projectionMatrix.elements[2] = state.clipPlane.x
projectionMatrix.elements[6] = state.clipPlane.y
projectionMatrix.elements[10] = state.clipPlane.z + 1.0
projectionMatrix.elements[14] = state.clipPlane.w

renderer.shadowMap.autoUpdate = false
renderer.setRenderTarget(fboSharp)
if (!renderer.autoClear) { renderer.clear() }

renderer.render(toValue(scene), state.virtualCamera)
if (renderer instanceof WebGLRenderer) {
blurpass.render(renderer, fboSharp, fboBlur)
}

// NOTE: Restore the previous render target and material
renderer.xr.enabled = currentXrEnabled
renderer.shadowMap.autoUpdate = currentShadowAutoUpdate
parent.visible = true
renderer.setRenderTarget(null)
invalidate()
}
})
defineExpose({ instance: materialRef })
</script>
Expand Down
7 changes: 5 additions & 2 deletions src/core/misc/BakeShadows.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTres } from '@tresjs/core'
import { WebGLRenderer } from 'three'
import { defineComponent, watchEffect } from 'vue'

export const BakeShadows = defineComponent({
Expand All @@ -8,8 +9,10 @@ export const BakeShadows = defineComponent({
const { renderer } = useTres()

watchEffect(() => {
renderer.shadowMap.autoUpdate = false
renderer.shadowMap.needsUpdate = true
if (renderer instanceof WebGLRenderer) {
renderer.shadowMap.autoUpdate = false
renderer.shadowMap.needsUpdate = true
}
})
},
})
Loading