In the camera-controls source code, there is an example of a view offset. How can this be implemented using the CameraControls wrapped in '@react-three/drei'?
let offsetUpdated = false;
const viewOffset = new THREE.Vector2();
const cameraControls = new CameraControls( camera, renderer.domElement );
cameraControls._truckInternal = ( deltaX, deltaY ) => {
viewOffset.x += deltaX;
viewOffset.y += deltaY;
camera.setViewOffset(
width,
height,
viewOffset.x,
viewOffset.y,
width,
height,
);
camera.updateProjectionMatrix();
offsetUpdated = true;
}
the example source: view code
live demo: view demo
update:
@Łukasz-daniel-mastalerz Thank you very much! your code worked, but there is a weird shading issue with MeshReflectorMaterial
.
I forked your code and added more objects: demo of my scene
If I understand you correctly, you want to rewrite 1-1 the offset
logic from vanilla to Drei...
import React, { useRef, useEffect } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { CameraControls } from "@react-three/drei";
import * as THREE from "three";
import "./styles.css";
function OffSet() {
const controls = useRef(null);
const viewOffset = useRef(new THREE.Vector2(0, 0));
const { camera, size } = useThree();
useEffect(() => {
if (!controls.current) return;
const cc = controls.current;
cc._truckInternal = (deltaX, deltaY) => {
viewOffset.current.x += deltaX;
viewOffset.current.y += deltaY;
camera.setViewOffset(
size.width,
size.height,
viewOffset.current.x,
viewOffset.current.y,
size.width,
size.height
);
camera.updateProjectionMatrix();
};
}, [camera, size]);
useFrame((_, delta) => {
controls.current?.update(delta);
});
return (
<>
<CameraControls makeDefault ref={controls} />
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color="red" wireframe />
</mesh>
<gridHelper args={[50, 50]} position-y={-1} />
</>
);
}
export default function App() {
return (
<Canvas
className="fullscreen"
camera={{ position: [0, 0, 5], fov: 60, near: 0.01, far: 100 }}
>
<OffSet />
</Canvas>
);
}
EDIT
Generally speaking, it is a matter of custom offset. You use, as in the original example, setViewOffset
to "Pan" the scene.
I recreated the original example in the parameters of your example and the artifacts are also there:
CodePen with original
import * as THREE from "https://esm.sh/three";
import CameraControls from 'https://cdn.jsdelivr.net/npm/camera-controls@2.10.1/+esm';
import { Reflector } from "https://esm.sh/three/addons/objects/Reflector.js"
CameraControls.install( { THREE: THREE } );
const width = window.innerWidth;
const height = window.innerHeight;
const clock = new THREE.Clock();
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 60, width / height, 0.01, 100 );
camera.position.set( 0, 0, 5 );
const renderer = new THREE.WebGLRenderer();
renderer.setSize( width, height );
document.body.appendChild( renderer.domElement );
let offsetUpdated = false;
const viewOffset = new THREE.Vector2();
const cameraControls = new CameraControls( camera, renderer.domElement );
cameraControls._truckInternal = ( deltaX, deltaY ) => {
viewOffset.x += deltaX;
viewOffset.y += deltaY;
camera.setViewOffset(
width,
height,
viewOffset.x,
viewOffset.y,
width,
height,
);
camera.updateProjectionMatrix();
offsetUpdated = true;
}
const mesh = new THREE.Mesh(
new THREE.BoxGeometry( 1, 1, 1 ),
new THREE.MeshBasicMaterial( { color: 0xff0000, wireframe: false } )
);
mesh.position.y=0.6;
scene.add( mesh );
const loader = new THREE.TextureLoader();
const gridEmitTexture = loader.load("https://media.craiyon.com/2023-11-26/2ets6-v1SVGOdKAtPxmlMw.webp");
const reflector = new Reflector(
new THREE.PlaneGeometry(120, 120),
{
clipBias: 0.003,
textureWidth: 2048,
textureHeight: 2048,
color: 0xededed,
}
);
reflector.rotation.x = - Math.PI / 2;
reflector.position.y = 0;
const emissivePlane = new THREE.Mesh(
new THREE.PlaneGeometry(120, 120),
new THREE.MeshBasicMaterial({
map: gridEmitTexture,
transparent: true,
opacity: 0.8,
depthWrite: false
})
);
emissivePlane.rotation.x = - Math.PI / 2;
emissivePlane.position.y = 0.001;
scene.add(reflector, emissivePlane);
renderer.render( scene, camera );
( function anim () {
const delta = clock.getDelta();
const elapsed = clock.getElapsedTime();
const updated = cameraControls.update( delta );
requestAnimationFrame( anim );
if ( updated || offsetUpdated ) {
renderer.render( scene, camera );
offsetUpdated = false;
console.log( 'rendered' );
}
} )();
globalThis.THREE = THREE;
globalThis.cameraControls = cameraControls;
Through trial and error, I found a solution using monkey-patching, which eliminates artifacts. Monkey-patching the Reflector so that its own virtual camera always clears any view-offset before it draws into the reflection.
camera.setViewOffset( fullWidth, fullHeight, offsetX, offsetY, viewWidth, viewHeight );
camera.updateProjectionMatrix();
setViewOffset
alters the camera frustum, tells Three.js to render only a subregion of the full frustum the same wat affects everything using that camera.
Reflector’s virtualCamera
copies your camera... and then does
virtualCamera.projectionMatrix.copy( camera.projectionMatrix );
Copying the projection matrix includes your view‐offset, which often lies outside the mirrored view, thus stretching the reflection texture. That is making those artifacts.
CodePen with onBeforeRender
import * as THREE from "https://esm.sh/three";
import CameraControls from 'https://cdn.jsdelivr.net/npm/camera-controls@2.10.1/+esm';
import { Reflector } from "https://esm.sh/three/addons/objects/Reflector.js";
CameraControls.install({ THREE });
const width = window.innerWidth;
const height = window.innerHeight;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, width/height, 0.01, 100);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
const clock = new THREE.Clock();
const controls = new CameraControls(camera, renderer.domElement);
let viewOffset = new THREE.Vector2();
controls._truckInternal = (dx, dy) => {
viewOffset.x += dx;
viewOffset.y += dy;
camera.setViewOffset(
width, height,
viewOffset.x, viewOffset.y,
width, height
);
camera.updateProjectionMatrix();
};
const reflector = new Reflector(
new THREE.PlaneGeometry(120, 120),
{ clipBias: 0.003, textureWidth: 2048, textureHeight: 2048, color: 0xededed }
);
reflector.rotation.x = -Math.PI / 2;
reflector.position.y = 0;
const _orig = reflector.onBeforeRender;
reflector.onBeforeRender = function(renderer, scene, cam){
const saved = cam.view ? { ...cam.view } : null;
cam.clearViewOffset();
cam.updateProjectionMatrix();
_orig.call(this, renderer, scene, cam);
if (saved) {
cam.setViewOffset(
saved.fullWidth, saved.fullHeight,
saved.offsetX, saved.offsetY,
saved.width, saved.height
);
cam.updateProjectionMatrix();
}
};
scene.add(reflector);
const loader = new THREE.TextureLoader();
const gridTex = loader.load("https://media.craiyon.com/2023-11-26/2ets6-v1SVGOdKAtPxmlMw.webp");
const emissivePlane = new THREE.Mesh(
new THREE.PlaneGeometry(120, 120),
new THREE.MeshBasicMaterial({
map: gridTex,
transparent: true,
opacity: 0.8,
depthWrite: false
})
);
emissivePlane.rotation.x = -Math.PI/2;
emissivePlane.position.y = 0.001;
scene.add(emissivePlane);
const box = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshBasicMaterial({ color: 0xff0000 })
);
box.position.y = 0.6;
scene.add(box);
renderer.render(scene, camera);
function animate() {
const delta = clock.getDelta();
controls.update(delta);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
Rewrited code R3F
BTW keep in mind that „...R3F Reflector / MeshReflectorMaterial is not the same thing as three’s Reflector....” - you can get hard times to recreate code Vanilla → R3F 1-1...
Look more HERE
import React, { useRef, useEffect } from "react";
import { Canvas, useThree, useFrame, useLoader } from "@react-three/fiber";
import * as THREE from "three";
import CameraControls from "camera-controls";
import { TextureLoader } from "three";
import { Reflector } from "three/examples/jsm/objects/Reflector.js";
import "./styles.css";
CameraControls.install({ THREE });
function Offset() {
const controls = useRef();
const reflectorRef = useRef();
const { camera, gl, scene, size } = useThree();
const clock = useRef(new THREE.Clock());
const viewOffset = useRef(new THREE.Vector2(0, 0));
const gridTex = useLoader(
TextureLoader,
"https://media.craiyon.com/2023-11-26/2ets6-v1SVGOdKAtPxmlMw.webp"
);
useEffect(() => {
controls.current = new CameraControls(camera, gl.domElement);
controls.current._truckInternal = (dx, dy) => {
viewOffset.current.x += dx;
viewOffset.current.y += dy;
camera.setViewOffset(
size.width,
size.height,
viewOffset.current.x,
viewOffset.current.y,
size.width,
size.height
);
camera.updateProjectionMatrix();
};
return () => controls.current?.dispose();
}, [camera, gl, size]);
useEffect(() => {
const reflector = new Reflector(new THREE.PlaneGeometry(120, 120), {
clipBias: 0.003,
textureWidth: 2048,
textureHeight: 2048,
color: 0xededed,
});
reflector.rotation.x = -Math.PI / 2;
reflector.position.y = 0;
const orig = reflector.onBeforeRender;
reflector.onBeforeRender = function (renderer, scene, cam) {
const saved = cam.view ? { ...cam.view } : null;
cam.clearViewOffset();
cam.updateProjectionMatrix();
orig.call(this, renderer, scene, cam);
if (saved) {
cam.setViewOffset(
saved.fullWidth,
saved.fullHeight,
saved.offsetX,
saved.offsetY,
saved.width,
saved.height
);
cam.updateProjectionMatrix();
}
};
reflectorRef.current = reflector;
scene.add(reflector);
return () => {
scene.remove(reflector);
};
}, [scene]);
useFrame(() => {
const delta = clock.current.getDelta();
controls.current?.update(delta);
});
return (
<>
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.001, 0]}>
<planeGeometry args={[120, 120]} />
<meshBasicMaterial
map={gridTex}
transparent
opacity={0.8}
depthWrite={false}
/>
</mesh>
<mesh position={[0, 0.6, 0]}>
<boxGeometry args={[1, 1, 1]} />
<meshBasicMaterial color="red" />
</mesh>
</>
);
}
export default function App() {
return (
<Canvas
camera={{ position: [0, 0, 5], fov: 60, near: 0.01, far: 100 }}
gl={{ antialias: true }}
>
<Offset />
</Canvas>
);
}