I’ve created a data visualisation using Three.js that shows thousands of images in 3D space and would like to only load the plane texture when the plane is within a threshold distance from the camera in order to save device resources. I can’t seem to get the texture switching to work when the plan is within camera view, I’ve even tried creating a different shader. The functions in question are updateMaterialsVisibility
, loadAndApplyTexture
, and isPlaneInCameraView
Here is the code I am using:
import React, { useEffect, useRef, useState, useCallback } from "react";
import * as THREE from "three";
const ImageVisualisation = () => {
const containerRef = useRef();
const [data, setData] = useState([]);
let textures;
let camera;
const isPlaneInCameraView = (plane, thresholdDistance) => {
const frustum = new THREE.Frustum();
const cameraViewProjectionMatrix = new THREE.Matrix4();
camera.updateMatrixWorld();
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
cameraViewProjectionMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(cameraViewProjectionMatrix);
if (
frustum.intersectsObject(plane) &&
plane.position.distanceTo(camera.position) <= thresholdDistance
) {
return true;
}
return false;
};
const loadAndApplyTexture = (mesh, imagePath) => {
return new Promise((resolve) => {
const textureLoader = new THREE.TextureLoader();
textureLoader.load(imagePath, (texture) => {
mesh.material.map = texture;
mesh.material.color.set(0xffffff);
mesh.material.needsUpdate = true;
resolve();
});
});
};
const updateMaterialsVisibility = (thresholdDistance) => {
if (!data || data.length === 0) return [];
const texturePromises = [];
for (let i = 0; i < textures.length; i++) {
const mesh = textures[i];
if (isPlaneInCameraView(mesh, thresholdDistance)) {
if (!mesh.material.map) {
const promise = loadAndApplyTexture(mesh, data[i].ImagePath);
texturePromises.push(promise);
}
} else {
if (mesh.material.map) {
mesh.material.map.dispose();
mesh.material.map = null;
mesh.material.color.set(0x0000ff);
mesh.material.needsUpdate = true;
}
}
}
return texturePromises;
};
useEffect(() => {
const fetchData = async () => {
const response = await fetch("data.json");
const jsonData = await response.json();
setData(jsonData);
init(jsonData);
};
let scene, renderer;
const init = (data) => {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
containerRef.current.appendChild(renderer.domElement);
textures = data.map((item, index) => {
const geometry = new THREE.PlaneGeometry(1, 1);
const material = blueMaterial;
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(
Math.random() * 50 - 25,
Math.random() * 50 - 25,
Math.random() * 50 - 25
);
scene.add(mesh);
return mesh;
});
camera.position.z = 100;
animate();
};
async function animate() {
requestAnimationFrame(animate);
textures.forEach((texture) => {
texture.rotation.y += 0.01;
});
const texturePromises = updateMaterialsVisibility(200);
await Promise.all(texturePromises);
// Move camera forward
camera.position.z -= 0.1;
renderer.render(scene, camera);
}
fetchData();
return () => {
if (renderer) {
containerRef.current.removeChild(renderer.domElement);
}
};
}, []);
return <div ref={containerRef}></div>;
};
export default ImageVisualisation;