Texturing the square base of a pyramid made of two triangles

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!

I suspect the error to be in the addGroup parameters ‘start’ and ‘count’, as you are clearly using an indexed geometry.

Each group is an object of the form: { start: Integer, count: Integer, materialIndex: Integer } where start specifies the first element in this draw call – the first vertex for non-indexed geometry, otherwise the first triangle index. Count specifies how many vertices (or [triangle] indices) are included, and materialIndex specifies the material array index to use.

See this for reference.

According to the above, try changing you code to this:

// Create groups for different faces
pyramidGeometry.clearGroups();
pyramidGeometry.addGroup(0, 1, 0); // Front face uses material 0
pyramidGeometry.addGroup(1, 1, 1); // Right face uses material 1
pyramidGeometry.addGroup(2, 1, 2); // Back face uses material 2
pyramidGeometry.addGroup(3, 1, 3); // Left face uses material 3
pyramidGeometry.addGroup(4, 2, 4); // Base uses material 4
1 Like

Thank you for your reply @vielzutun.ch, I truly appreciate it. It’s difficult when I’m trying to learn and I use all my resources and get stuck.

While your suggestion didn’t work exactly, it led me trying to add the triangle in question to its own group and redefining the base face vertices. This is where I got to - which is a step forward

I’m now doing a bit of trial and error to get the textures to line up so they appear seamless. Would you have any suggestions on what I could do?

Thanks again in advance

One thing that didn’t catch my attention the 1st time around:

the whole point of using indexed geometries is to save on the number of vertices which need to be transformed, while retaining the ability, to use each single vertex in the context of multiple faces. So you’ll explicitely want to avoid duplicating vertices, which you did with vertices 5 … 8.

That said, you only need the first five vertices, indices 0 … 4.

Of course, you can only reference the vertices you have, when building triangular faces. For the bottom faces those would be:

1, 4, 3
1, 3, 2

respectively.

As for the seamless joint:

try to cyclically rotate the vertices which define the face, e.g.: 1, 4, 3 => 4, 3, 1 or 3, 1, 4. All three variants define the same face, while the “leading edge” is different in all three cases.

Also experiment with changing the sense of rotation from CW to CCW, or vice-versa, by switching the positions of only two of the three components: 1, 4, 3 ==> 1, 3, 4.

1 Like

Just out of curiousity, why not merge several geometries?

Picture:

Demo: https://codepen.io/prisoner849/full/dyBErxv

3 Likes

One live animation saves a million words …

2 Likes

@vielzutun.ch @prisoner849 Thank you for your replies :pray:. I will be trying these tonight, let you know how I go

@vielzutun.ch @prisoner849 Just an update, I have managed to achieve my goal thanks to both your help. I used the merge geometries codepen as a base and it gave me a good foundation.

Thank you both for your support and suggestions, I truly appreciate it

3 Likes

Great, Awesome!