Hi all,
I’m new to threejs and doing my best to learn. My first project was to create a pyramid, apply a different images on each face and add annotation on click/tap of each face.
I’ve managed to complete most of it, however I can’t get my image to display properly on one of the triangles that make up the base. When I load a different image, it updates on the pyramid so that leaves me to believe it could be a uv mapping issue?
Here is a screenshot of what I’m seeing
And here is my code
import { OrbitControls } from "https://unpkg.com/three@0.112/examples/jsm/controls/OrbitControls.js";
// Basic setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Orbit controls for rotation (mouse and touch support for dragging)
const controls = new OrbitControls(camera, renderer.domElement);
// Create textures
const texture1 = new THREE.TextureLoader().load('/maths1.jpg');
const texture2 = new THREE.TextureLoader().load('/maths2.jpg');
const texture3 = new THREE.TextureLoader().load('/maths3.jpg');
const texture4 = new THREE.TextureLoader().load('/maths4.jpg');
const textureBase = new THREE.TextureLoader().load('/maths5.jpg');
console.log('flip Y before');
textureBase.flipY = false;
console.log(textureBase.flipY);
// Adjust the texture's scale to fit the face. These make no difference if they are commented out or not
textureBase.wrapS = THREE.ClampToEdgeWrapping;
textureBase.wrapT = THREE.ClampToEdgeWrapping;
textureBase.repeat.set(1, 1); // 1x1 tiling, change values to scale texture
textureBase.offset.set(0, 0); // Start at (0,0)
// Pyramid geometry (square base)
const pyramidGeometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
// Apex
0, 2, 0, // Vertex 0: Apex
// Base vertices for side faces
-1, 0, 1, // Vertex 1: Base front-left (for sides)
1, 0, 1, // Vertex 2: Base front-right (for sides)
1, 0, -1, // Vertex 3: Base back-right (for sides)
-1, 0, -1, // Vertex 4: Base back-left (for sides)
// Base vertices (duplicated for the base face)
-1, 0, 1, // Vertex 5: Base front-left (for base)
1, 0, 1, // Vertex 6: Base front-right (for base)
1, 0, -1, // Vertex 7: Base back-right (for base)
-1, 0, -1 // Vertex 8: Base back-left (for base)
]);
const indices = [
0, 1, 2, // Front face
0, 2, 3, // Right face
0, 3, 4, // Back face
0, 4, 1, // Left face
// Base face (using the new base vertices)
5, 8, 7, // Base face 1
5, 7, 6 // Base face 2
];
// UV coordinates for each vertex of the pyramid faces
const uvs = new Float32Array([
// Front face (0, 1, 2)
0.5, 1.0, // Apex
0.0, 0.0, // Base front-left
1.0, 0.0, // Base front-right
// Right face (0, 2, 3)
0.0, 0.0, // Apex
1.0, 0.0, // Base front-right
1.0, 0.0, // Base back-right
// Back face (0, 3, 4)
0.0, 0.0, // Apex
1.0, 1.0, // Base front-right
1.0, 1.0, // Base back-right
// Left face (0, 4, 1)
0.5, 1.0, // Apex
0.0, 0.0, // Base front-right
0.5, 1.0, // Base back-right
// Base face 1 (5, 8, 7)
0.0, 0.0, // Base front-left (vertex 5)
0.0, 1.0, // Base back-left (vertex 8)
1.0, 1.0, // Base back-right (vertex 7)
// Base face 2 (5, 7, 6)
0.0, 0.0, // Base front-left (vertex 5)
1.0, 1.0, // Base back-right (vertex 7)
1.0, 0.0 // Base front-right (vertex 6)
]);
// Set vertices and indices for the geometry
pyramidGeometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
pyramidGeometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2)); // Setting UV coordinates
pyramidGeometry.setIndex(indices);
pyramidGeometry.computeVertexNormals();
// Create groups for different faces
pyramidGeometry.clearGroups();
pyramidGeometry.addGroup(0, 3, 0); // Front face uses material 0
pyramidGeometry.addGroup(3, 3, 1); // Right face uses material 1
pyramidGeometry.addGroup(6, 3, 2); // Back face uses material 2
pyramidGeometry.addGroup(9, 3, 3); // Left face uses material 3
pyramidGeometry.addGroup(12, 6, 4); // Base uses material 4
// Create materials
const material1 = new THREE.MeshBasicMaterial({ map: texture1 });
const material2 = new THREE.MeshBasicMaterial({ map: texture2 });
const material3 = new THREE.MeshBasicMaterial({ map: texture3 });
const material4 = new THREE.MeshBasicMaterial({ map: texture4 });
const material5 = new THREE.MeshBasicMaterial({ map: textureBase });
// Create mesh with material
const pyramid = new THREE.Mesh(pyramidGeometry, [material1, material2, material3, material4, material5]);
scene.add(pyramid);
// Raycaster for interaction
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// Camera position
camera.position.z = 5;
// Annotation setup
const annotationsDiv = document.getElementById('annotation');
const annotations = [
{ face: 0, text: "Front Face" },
{ face: 1, text: "Right Face" },
{ face: 2, text: "Back Face" },
{ face: 3, text: "Left Face" },
{ face: 4, text: "Bottom Face 1" },
{ face: 5, text: "Bottom Face 2" }
];
// Handle mouse and touch events
function onPointerMove(event) {
event.preventDefault();
// Normalize coordinates based on event type
if (event.clientX !== undefined && event.clientY !== undefined) { // Mouse event
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
} else if (event.touches && event.touches.length > 0) { // Touch event
const touch = event.touches[0];
mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (touch.clientY / window.innerHeight) * 2 + 1;
}
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObject(pyramid);
if (intersects.length > 0) {
const faceIndex = intersects[0].faceIndex;
const annotation = annotations.find(anno => anno.face === faceIndex);
if (annotation) {
showAnnotation(event.clientX || event.touches[0].clientX, event.clientY || event.touches[0].clientY, annotation.text);
}
}
}
function showAnnotation(x, y, text) {
annotationsDiv.innerHTML = `<div class="annotation" style="left:${x}px; top:${y}px;">${text}</div>`;
}
window.addEventListener('click', onPointerMove, false);
window.addEventListener('touchstart', onPointerMove, false);
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
// Animation loop
function animate() {
requestAnimationFrame(animate);
controls.update(); // Required for damping
renderer.render(scene, camera);
}
animate();
Appreciate any help and advice, thank you!