Glb exported from blender gets stretched/compressed

Hello,

I am currently developing an Angular application and I use ThreeJS to load a model that I created in Blender and exported as a .glb file in it (wild mix, I know…). In my Angular application the model is in a component thats 30% of the viewport width:

app-bot {
  height: 100%;
  width: 30%;
}

My problem is that the model is not its true width and gets compressed horizontally. This is how it should look (Screenshot from an online glTF viewer, so the model itself seems fine):

The amount of compression actually depends on the viewport width, meaning that if I resize my browser window and refresh it changes:

This is my implementation:

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/Addons.js';

@Component({
  selector: 'app-bot',
  imports: [],
  templateUrl: './bot.component.html',
  styleUrl: './bot.component.scss',
})
export class BotComponent implements AfterViewInit {
  // HTML Element: <canvas #rendererCanvas></canvas>
  @ViewChild('rendererCanvas') rendererCanvas!: ElementRef;

  private renderer!: THREE.WebGLRenderer;
  private scene!: THREE.Scene;
  private camera!: THREE.PerspectiveCamera;
  private mixer: THREE.AnimationMixer | null = null;
  private animationAction: THREE.AnimationAction | null = null;

  ngAfterViewInit() {
    this.initThree();
    this.loadModel();
  }

  private initThree() {
    this.scene = new THREE.Scene();

    this.camera = new THREE.PerspectiveCamera();
      // Also already tried
      // window.innerWidth / window.innerHeight,
      // and
      // width / height of parent container
      // as camera aspect ratio
    this.camera.position.set(0, 2.5, 5);

    this.renderer = new THREE.WebGLRenderer({
      canvas: this.rendererCanvas.nativeElement,
      antialias: true,
      alpha: true, // Enable alpha (transparency)
    });
    this.renderer.setClearColor(0x000000, 0); // Set clear color to transparent
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(window.devicePixelRatio);
    // I also already tried out this:
    // const width = this.rendererCanvas.nativeElement.clientWidth;
    // const height = this.rendererCanvas.nativeElement.clientHeight;
    // const aspectRatio = width / height; // Correct aspect ratio
    // this.renderer.setSize(width, height);
    // this.renderer.setPixelRatio(aspectRatio);

    // Add light to the scene
    const light = new THREE.DirectionalLight(0xffffff, 1);
    light.position.set(5, 5, 5);
    this.scene.add(light);

    // Add ambient light
    const ambientLight = new THREE.AmbientLight(0x404040, 15);
    this.scene.add(ambientLight);

    this.animate();
  }

  private loadModel() {
    const loader = new GLTFLoader();
    loader.load(
      'model/Kohmi_v3_waving_export.glb',
      (gltf) => {
        console.log('Model loaded:', gltf);
        const robot = gltf.scene;
        this.scene.add(robot);

        // Position the robot
        robot.position.set(0, 0, 0);

        // Rotate the robot to face the camera
        robot.rotation.y = Math.PI / 2 + Math.PI;

        // Set up animation
        if (gltf.animations.length > 0) {
          this.mixer = new THREE.AnimationMixer(robot);
          this.animationAction = this.mixer.clipAction(gltf.animations[0]);
          this.animationAction.setLoop(THREE.LoopOnce, 1);
          this.animationAction.clampWhenFinished = true;
        }

        // Add event listener for keypress
        document.addEventListener('keydown', this.onKeyDown.bind(this));
      },
      function (xhr) {
        console.log((xhr.loaded / xhr.total) * 100 + '% loaded');
      },
      function (error) {
        console.error('An error occurred:', error);
      }
    );
  }

  private animate() {
    requestAnimationFrame(() => this.animate());

    if (this.mixer) {
      this.mixer.update(0.016); // Update mixer (assuming 60fps)
    }

    this.renderer.render(this.scene, this.camera);
  }

  private onKeyDown(event: KeyboardEvent) {
    if (
      event.key === 'k' &&
      this.animationAction &&
      !this.animationAction.isRunning()
    ) {
      this.animationAction.reset().play();
    }
  }
}

Another weird thing is that when I remove the chat component and make the component with the model in it full width (or just dont specify any width) it gets stretched too, but when I load the model like this in a normal JS ThreeJS project it works perfectly fine… so maybe it has to do with Angular? Or did I miss to specify anything in my implementation?

You need some container resize handling logic.
and when the width/height of the container changes… the camera projection also needs to be updated to the new aspect ratio.

with a fullscreen app, its relatively straightforward to
renderer.setSize(window.clientWidth,window.clientHeight)

but for you app… you’ll need to get the bounds of the parent container, and renderer.setSize ( with those…
and you want to debounce it to avoid spamming the call.

Smth like this:


let {width, height} = canvas;

let clock=new THREE.Clock();
renderer.setAnimationLoop( (time) => {
    let dt = clock.getDelta();
    const container = renderer.domElement.parentElement;
    const w = container.clientWidth;
    const h = container.clientHeight

    if ((w !== width) || (h !== height)) {
        renderer.setSize(width = w, height = h, true);
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
    }
//... regular rendering here...
})
2 Likes

This worked! I tried something similiar before, but I never realized that I scaled before the model had fully loaded… anyways, thank you!

1 Like