RETROEXCAVADORA 3D INTERACTIVA

Descarga gratis un modelo 3D de una retroexcavadora en formato GLB, junto con un proyecto completo en Three.js para crear una interacción 3D para web. Incluye código fuente, animación con Shape Keys y archivos para adaptar el modelo a tus propios proyectos.

RETROEXCAVADORA 3D INTERACTIVA

Interacción 3D para web

¿Quieres experimentar con interacciones 3D en tus propios proyectos?

En esta página puedes descargar gratuitamente el modelo 3D, el código y los archivos necesarios para reproducir esta interacción y adaptarla a tus propios modelos.

No necesitas crear todo desde cero: puedes utilizar el proyecto como base y modificar el modelo, la animación o la interacción según tus necesidades.

Antes de empezar

Si no tienes experiencia programando, no te preocupes. El proyecto está pensado para que puedas experimentar con él, pero hay un detalle importante:

Debes ejecutar el proyecto mediante Live Server en Visual Studio Code.

Esto es necesario para que el modelo .glb pueda cargarse y renderizarse correctamente en el navegador. Si simplemente abres el archivo index.html directamente, es posible que el modelo 3D no aparezca.

¿Qué incluye?

  • Modelo 3D .glb

  • Código HTML, CSS y JavaScript

  • Integración con Three.js

  • Animación mediante Shape Keys

  • Interacción con el movimiento del mouse

Puedes utilizar este proyecto como punto de partida y reemplazar el modelo por uno propio siguiendo la misma lógica de interacción.

Descarga el proyecto, experimenta y crea tu propia versión.

Código (HTML + CSS+ JAVASCRIPT)

				
					<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.threejs-container {
  width: 100%;
  height: 100vh;
  position: relative;
  overflow: hidden;
}
#threejs-canvas {
  width: 100%;
  height: 100%;
}
#intro-overlay {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  background: #000;
  z-index: 10;
  pointer-events: none;
  opacity: 1;
  transition: opacity 1s cubic-bezier(.7,0,.2,1);
}
#intro-overlay.fade-out {
  opacity: 0;
}
</style>

<div class="threejs-container">
  <div id="intro-overlay"></div>
  <div id="threejs-canvas"></div>
</div>

<script type="module">
import * as THREE from 'https://esm.sh/three@0.162.0';
import { GLTFLoader } from 'https://esm.sh/three@0.162.0/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'https://esm.sh/three@0.162.0/examples/jsm/loaders/DRACOLoader.js';
import { RectAreaLightUniformsLib } from 'https://esm.sh/three@0.162.0/examples/jsm/lights/RectAreaLightUniformsLib.js';

RectAreaLightUniformsLib.init();

// ─── Estado ─────────────────────────────────────────────────────────────────
let container, renderer, scene, camera, overlay;

let morphMeshes    = [];   // meshes con morph targets
let solidMaterials = [];   // materiales del modelo sólido (cacheados)
let wireMaterials  = [];   // materiales wireframe (cacheados)

let wireframeOpacity = 1;
let modelOpacity     = 0;
let currentValue     = 1;
let targetValue      = 0;
let introProgress    = 0;

// render-on-demand: solo renderiza cuando algo cambió
let needsRender = true;
function markDirty() { needsRender = true; }

const startCameraPos = new THREE.Vector3(-15, -15, 45);
const endCameraPos   = new THREE.Vector3(-15, -10, 18);
const startRot       = new THREE.Euler(-0.05, 6.3, 0);
const endRot         = new THREE.Euler(0, 5.85, 0);

// ─── Init ────────────────────────────────────────────────────────────────────
init();

function init() {
  overlay   = document.getElementById('intro-overlay');
  container = document.getElementById('threejs-canvas');

  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x0f0f0f);

  camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 100);
  camera.position.copy(startCameraPos);
  camera.rotation.copy(startRot);

  renderer = new THREE.WebGLRenderer({ antialias: window.devicePixelRatio < 2 });
  renderer.setSize(container.clientWidth, container.clientHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  renderer.toneMapping = THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure = 1;
  renderer.physicallyCorrectLights = true;
  renderer.shadowMap.enabled = false; // sombras desactivadas completamente
  container.appendChild(renderer.domElement);

  // ── Iluminación original intacta ─────────────────────────────────────────
  const keyLight = new THREE.RectAreaLight(0xffffff, 18, 10, 10);
  keyLight.position.set(-3, -8, 15);
  keyLight.lookAt(0, 0, 0);
  scene.add(keyLight);

  // DirectionalLight de relleno sin ninguna propiedad de sombra
  const fillLight = new THREE.DirectionalLight(0xffffff, 0.35);
  fillLight.position.set(6, 15, 10);
  scene.add(fillLight);

  const rimLight = new THREE.RectAreaLight(0xffa95a, 10, 10, 10);
  rimLight.position.set(0, 6, -8);
  rimLight.lookAt(0, 0, 0);
  scene.add(rimLight);

  scene.add(new THREE.AmbientLight(0xffffff, 0.15));

  // ── Carga ────────────────────────────────────────────────────────────────
  const dracoLoader = new DRACOLoader();
  dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');

  const loader = new GLTFLoader();
  loader.setDRACOLoader(dracoLoader);
  loader.load('ExcavadoraFinal.glb', onModelLoaded);

  window.addEventListener('resize', onResize);
  window.addEventListener('mousemove', onMouseMove);
  animate();
}

// ─── Carga del modelo ────────────────────────────────────────────────────────
function onModelLoaded(gltf) {
  const model = gltf.scene;

  const box = new THREE.Box3().setFromObject(model);
  model.position.sub(box.getCenter(new THREE.Vector3()));
  model.position.x = 4;

  // Traversal ÚNICO al cargar — nunca en el loop de animación
  model.traverse(child => {
    if (!child.isMesh) return;

    child.castShadow    = false;
    child.receiveShadow = false;

    const mat = child.material;
    mat.transparent = true;
    mat.opacity     = 0;
    solidMaterials.push(mat);

    if (child.morphTargetInfluences) morphMeshes.push(child);

    // Wireframe 
    const wireMat = new THREE.MeshBasicMaterial({
      color:       0xffffff,
      wireframe:   true,
      transparent: true,
      opacity:     1,
      blending:    THREE.AdditiveBlending,
      depthWrite:  false,
    });

    const wireMesh = new THREE.Mesh(child.geometry, wireMat);

    if (child.morphTargetInfluences) {
      wireMesh.morphTargetInfluences = child.morphTargetInfluences;
      wireMesh.morphTargetDictionary = child.morphTargetDictionary;
    }

    child.parent.add(wireMesh);
    wireMesh.position.copy(child.position);
    wireMesh.rotation.copy(child.rotation);
    wireMesh.scale.copy(child.scale);

    wireMaterials.push(wireMat);
  });

  scene.add(model);
  markDirty();
  setTimeout(() => overlay.classList.add('fade-out'), 100);
}

// ─── Resize ──────────────────────────────────────────────────────────────────
function onResize() {
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(container.clientWidth, container.clientHeight);
  markDirty();
}

// ─── Mouse ───────────────────────────────────────────────────────────────────
function onMouseMove(e) {
  const rect = container.getBoundingClientRect();
  targetValue = 1 - (e.clientX - rect.left) / rect.width;
  markDirty();
}

// ─── Loop de animación ───────────────────────────────────────────────────────
function animate() {
  requestAnimationFrame(animate);

  if (introProgress < 0.9999) markDirty();
  if (Math.abs(targetValue - currentValue) > 0.0001) markDirty();

  if (!needsRender) return;
  needsRender = false;

  // Intro cámara
  if (introProgress < 1) {
    introProgress += (1 - introProgress) * 0.008;
    camera.position.lerpVectors(startCameraPos, endCameraPos, introProgress);
    camera.rotation.x = THREE.MathUtils.lerp(startRot.x, endRot.x, introProgress);
    camera.rotation.y = THREE.MathUtils.lerp(startRot.y, endRot.y, introProgress);
    camera.rotation.z = THREE.MathUtils.lerp(startRot.z, endRot.z, introProgress);
  }

  // Morph (brazo)
  currentValue += (targetValue - currentValue) * 0.1;
  const morphVal = 1 - currentValue;
  for (let i = 0; i < morphMeshes.length; i++) {
    morphMeshes[i].morphTargetInfluences[0] = morphVal;
  }

  // Transición wireframe → sólido
  if (introProgress > 0.5) {
    wireframeOpacity += (0 - wireframeOpacity) * 0.06;
    modelOpacity     += (1 - modelOpacity)     * 0.04;

    for (let i = 0; i < solidMaterials.length; i++) solidMaterials[i].opacity = modelOpacity;
    for (let i = 0; i < wireMaterials.length;  i++) wireMaterials[i].opacity  = wireframeOpacity;
  }

  renderer.render(scene, camera);
}
</script>
				
			

Démosle forma a tu idea

Cada proyecto comienza con una intuición. Queremos entender la tuya para dar una estructura visual con propósito, claridad y coherencia.

[ninja_form id=2]