@react-three/drei
Version:
useful add-ons for react-three-fiber
376 lines (367 loc) • 14.7 kB
JavaScript
import _extends from '@babel/runtime/helpers/esm/extends';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { Vector3, DoubleSide, OrthographicCamera, PerspectiveCamera, Vector2 } from 'three';
import { useThree, useFrame } from '@react-three/fiber';
const v1 = /* @__PURE__ */new Vector3();
const v2 = /* @__PURE__ */new Vector3();
const v3 = /* @__PURE__ */new Vector3();
const v4 = /* @__PURE__ */new Vector2();
function defaultCalculatePosition(el, camera, size) {
const objectPos = v1.setFromMatrixPosition(el.matrixWorld);
objectPos.project(camera);
const widthHalf = size.width / 2;
const heightHalf = size.height / 2;
return [objectPos.x * widthHalf + widthHalf, -(objectPos.y * heightHalf) + heightHalf];
}
function isObjectBehindCamera(el, camera) {
const objectPos = v1.setFromMatrixPosition(el.matrixWorld);
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld);
const deltaCamObj = objectPos.sub(cameraPos);
const camDir = camera.getWorldDirection(v3);
return deltaCamObj.angleTo(camDir) > Math.PI / 2;
}
function isObjectVisible(el, camera, raycaster, occlude) {
const elPos = v1.setFromMatrixPosition(el.matrixWorld);
const screenPos = elPos.clone();
screenPos.project(camera);
v4.set(screenPos.x, screenPos.y);
raycaster.setFromCamera(v4, camera);
const intersects = raycaster.intersectObjects(occlude, true);
if (intersects.length) {
const intersectionDistance = intersects[0].distance;
const pointDistance = elPos.distanceTo(raycaster.ray.origin);
return pointDistance < intersectionDistance;
}
return true;
}
function objectScale(el, camera) {
if (camera instanceof OrthographicCamera) {
return camera.zoom;
} else if (camera instanceof PerspectiveCamera) {
const objectPos = v1.setFromMatrixPosition(el.matrixWorld);
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld);
const vFOV = camera.fov * Math.PI / 180;
const dist = objectPos.distanceTo(cameraPos);
const scaleFOV = 2 * Math.tan(vFOV / 2) * dist;
return 1 / scaleFOV;
} else {
return 1;
}
}
function objectZIndex(el, camera, zIndexRange) {
if (camera instanceof PerspectiveCamera || camera instanceof OrthographicCamera) {
const objectPos = v1.setFromMatrixPosition(el.matrixWorld);
const cameraPos = v2.setFromMatrixPosition(camera.matrixWorld);
const dist = objectPos.distanceTo(cameraPos);
const A = (zIndexRange[1] - zIndexRange[0]) / (camera.far - camera.near);
const B = zIndexRange[1] - A * camera.far;
return Math.round(A * dist + B);
}
return undefined;
}
const epsilon = value => Math.abs(value) < 1e-10 ? 0 : value;
function getCSSMatrix(matrix, multipliers, prepend = '') {
let matrix3d = 'matrix3d(';
for (let i = 0; i !== 16; i++) {
matrix3d += epsilon(multipliers[i] * matrix.elements[i]) + (i !== 15 ? ',' : ')');
}
return prepend + matrix3d;
}
const getCameraCSSMatrix = (multipliers => {
return matrix => getCSSMatrix(matrix, multipliers);
})([1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1]);
const getObjectCSSMatrix = (scaleMultipliers => {
return (matrix, factor) => getCSSMatrix(matrix, scaleMultipliers(factor), 'translate(-50%,-50%)');
})(f => [1 / f, 1 / f, 1 / f, 1, -1 / f, -1 / f, -1 / f, -1, 1 / f, 1 / f, 1 / f, 1, 1, 1, 1, 1]);
function isRefObject(ref) {
return ref && typeof ref === 'object' && 'current' in ref;
}
const Html = /* @__PURE__ */React.forwardRef(({
children,
eps = 0.001,
style,
className,
prepend,
center,
fullscreen,
portal,
distanceFactor,
sprite = false,
transform = false,
occlude,
onOcclude,
castShadow,
receiveShadow,
material,
geometry,
zIndexRange = [16777271, 0],
calculatePosition = defaultCalculatePosition,
as = 'div',
wrapperClass,
pointerEvents = 'auto',
...props
}, ref) => {
const {
gl,
camera,
scene,
size,
raycaster,
events,
viewport
} = useThree();
const [el] = React.useState(() => document.createElement(as));
const root = React.useRef(null);
const group = React.useRef(null);
const oldZoom = React.useRef(0);
const oldPosition = React.useRef([0, 0]);
const transformOuterRef = React.useRef(null);
const transformInnerRef = React.useRef(null);
// Append to the connected element, which makes HTML work with views
const target = (portal == null ? void 0 : portal.current) || events.connected || gl.domElement.parentNode;
const occlusionMeshRef = React.useRef(null);
const isMeshSizeSet = React.useRef(false);
const isRayCastOcclusion = React.useMemo(() => {
return occlude && occlude !== 'blending' || Array.isArray(occlude) && occlude.length && isRefObject(occlude[0]);
}, [occlude]);
React.useLayoutEffect(() => {
const el = gl.domElement;
if (occlude && occlude === 'blending') {
el.style.zIndex = `${Math.floor(zIndexRange[0] / 2)}`;
el.style.position = 'absolute';
el.style.pointerEvents = 'none';
} else {
el.style.zIndex = null;
el.style.position = null;
el.style.pointerEvents = null;
}
}, [occlude]);
React.useLayoutEffect(() => {
if (group.current) {
const currentRoot = root.current = ReactDOM.createRoot(el);
scene.updateMatrixWorld();
if (transform) {
el.style.cssText = `position:absolute;top:0;left:0;pointer-events:none;overflow:hidden;`;
} else {
const vec = calculatePosition(group.current, camera, size);
el.style.cssText = `position:absolute;top:0;left:0;transform:translate3d(${vec[0]}px,${vec[1]}px,0);transform-origin:0 0;`;
}
if (target) {
if (prepend) target.prepend(el);else target.appendChild(el);
}
return () => {
if (target) target.removeChild(el);
currentRoot.unmount();
};
}
}, [target, transform]);
React.useLayoutEffect(() => {
if (wrapperClass) el.className = wrapperClass;
}, [wrapperClass]);
const styles = React.useMemo(() => {
if (transform) {
return {
position: 'absolute',
top: 0,
left: 0,
width: size.width,
height: size.height,
transformStyle: 'preserve-3d',
pointerEvents: 'none'
};
} else {
return {
position: 'absolute',
transform: center ? 'translate3d(-50%,-50%,0)' : 'none',
...(fullscreen && {
top: -size.height / 2,
left: -size.width / 2,
width: size.width,
height: size.height
}),
...style
};
}
}, [style, center, fullscreen, size, transform]);
const transformInnerStyles = React.useMemo(() => ({
position: 'absolute',
pointerEvents
}), [pointerEvents]);
React.useLayoutEffect(() => {
isMeshSizeSet.current = false;
if (transform) {
var _root$current;
(_root$current = root.current) == null || _root$current.render(/*#__PURE__*/React.createElement("div", {
ref: transformOuterRef,
style: styles
}, /*#__PURE__*/React.createElement("div", {
ref: transformInnerRef,
style: transformInnerStyles
}, /*#__PURE__*/React.createElement("div", {
ref: ref,
className: className,
style: style,
children: children
}))));
} else {
var _root$current2;
(_root$current2 = root.current) == null || _root$current2.render(/*#__PURE__*/React.createElement("div", {
ref: ref,
style: styles,
className: className,
children: children
}));
}
});
const visible = React.useRef(true);
useFrame(gl => {
if (group.current) {
camera.updateMatrixWorld();
group.current.updateWorldMatrix(true, false);
const vec = transform ? oldPosition.current : calculatePosition(group.current, camera, size);
if (transform || Math.abs(oldZoom.current - camera.zoom) > eps || Math.abs(oldPosition.current[0] - vec[0]) > eps || Math.abs(oldPosition.current[1] - vec[1]) > eps) {
const isBehindCamera = isObjectBehindCamera(group.current, camera);
let raytraceTarget = false;
if (isRayCastOcclusion) {
if (Array.isArray(occlude)) {
raytraceTarget = occlude.map(item => item.current);
} else if (occlude !== 'blending') {
raytraceTarget = [scene];
}
}
const previouslyVisible = visible.current;
if (raytraceTarget) {
const isvisible = isObjectVisible(group.current, camera, raycaster, raytraceTarget);
visible.current = isvisible && !isBehindCamera;
} else {
visible.current = !isBehindCamera;
}
if (previouslyVisible !== visible.current) {
if (onOcclude) onOcclude(!visible.current);else el.style.display = visible.current ? 'block' : 'none';
}
const halfRange = Math.floor(zIndexRange[0] / 2);
const zRange = occlude ? isRayCastOcclusion //
? [zIndexRange[0], halfRange] : [halfRange - 1, 0] : zIndexRange;
el.style.zIndex = `${objectZIndex(group.current, camera, zRange)}`;
if (transform) {
const [widthHalf, heightHalf] = [size.width / 2, size.height / 2];
const fov = camera.projectionMatrix.elements[5] * heightHalf;
const {
isOrthographicCamera,
top,
left,
bottom,
right
} = camera;
const cameraMatrix = getCameraCSSMatrix(camera.matrixWorldInverse);
const cameraTransform = isOrthographicCamera ? `scale(${fov})translate(${epsilon(-(right + left) / 2)}px,${epsilon((top + bottom) / 2)}px)` : `translateZ(${fov}px)`;
let matrix = group.current.matrixWorld;
if (sprite) {
matrix = camera.matrixWorldInverse.clone().transpose().copyPosition(matrix).scale(group.current.scale);
matrix.elements[3] = matrix.elements[7] = matrix.elements[11] = 0;
matrix.elements[15] = 1;
}
el.style.width = size.width + 'px';
el.style.height = size.height + 'px';
el.style.perspective = isOrthographicCamera ? '' : `${fov}px`;
if (transformOuterRef.current && transformInnerRef.current) {
transformOuterRef.current.style.transform = `${cameraTransform}${cameraMatrix}translate(${widthHalf}px,${heightHalf}px)`;
transformInnerRef.current.style.transform = getObjectCSSMatrix(matrix, 1 / ((distanceFactor || 10) / 400));
}
} else {
const scale = distanceFactor === undefined ? 1 : objectScale(group.current, camera) * distanceFactor;
el.style.transform = `translate3d(${vec[0]}px,${vec[1]}px,0) scale(${scale})`;
}
oldPosition.current = vec;
oldZoom.current = camera.zoom;
}
}
if (!isRayCastOcclusion && occlusionMeshRef.current && !isMeshSizeSet.current) {
if (transform) {
if (transformOuterRef.current) {
const el = transformOuterRef.current.children[0];
if (el != null && el.clientWidth && el != null && el.clientHeight) {
const {
isOrthographicCamera
} = camera;
if (isOrthographicCamera || geometry) {
if (props.scale) {
if (!Array.isArray(props.scale)) {
occlusionMeshRef.current.scale.setScalar(1 / props.scale);
} else if (props.scale instanceof Vector3) {
occlusionMeshRef.current.scale.copy(props.scale.clone().divideScalar(1));
} else {
occlusionMeshRef.current.scale.set(1 / props.scale[0], 1 / props.scale[1], 1 / props.scale[2]);
}
}
} else {
const ratio = (distanceFactor || 10) / 400;
const w = el.clientWidth * ratio;
const h = el.clientHeight * ratio;
occlusionMeshRef.current.scale.set(w, h, 1);
}
isMeshSizeSet.current = true;
}
}
} else {
const ele = el.children[0];
if (ele != null && ele.clientWidth && ele != null && ele.clientHeight) {
const ratio = 1 / viewport.factor;
const w = ele.clientWidth * ratio;
const h = ele.clientHeight * ratio;
occlusionMeshRef.current.scale.set(w, h, 1);
isMeshSizeSet.current = true;
}
occlusionMeshRef.current.lookAt(gl.camera.position);
}
}
});
const shaders = React.useMemo(() => ({
vertexShader: !transform ? /* glsl */`
/*
This shader is from the THREE's SpriteMaterial.
We need to turn the backing plane into a Sprite
(make it always face the camera) if "transfrom"
is false.
*/
#include <common>
void main() {
vec2 center = vec2(0., 1.);
float rotation = 0.0;
// This is somewhat arbitrary, but it seems to work well
// Need to figure out how to derive this dynamically if it even matters
float size = 0.03;
vec4 mvPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );
vec2 scale;
scale.x = length( vec3( modelMatrix[ 0 ].x, modelMatrix[ 0 ].y, modelMatrix[ 0 ].z ) );
scale.y = length( vec3( modelMatrix[ 1 ].x, modelMatrix[ 1 ].y, modelMatrix[ 1 ].z ) );
bool isPerspective = isPerspectiveMatrix( projectionMatrix );
if ( isPerspective ) scale *= - mvPosition.z;
vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale * size;
vec2 rotatedPosition;
rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;
rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;
mvPosition.xy += rotatedPosition;
gl_Position = projectionMatrix * mvPosition;
}
` : undefined,
fragmentShader: /* glsl */`
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
}
`
}), [transform]);
return /*#__PURE__*/React.createElement("group", _extends({}, props, {
ref: group
}), occlude && !isRayCastOcclusion && /*#__PURE__*/React.createElement("mesh", {
castShadow: castShadow,
receiveShadow: receiveShadow,
ref: occlusionMeshRef
}, geometry || /*#__PURE__*/React.createElement("planeGeometry", null), material || /*#__PURE__*/React.createElement("shaderMaterial", {
side: DoubleSide,
vertexShader: shaders.vertexShader,
fragmentShader: shaders.fragmentShader
})));
});
export { Html };