To eliminate distortion when resizing a Three.js window, you must listen for the browser's resize event and simultaneously update both the camera's aspect ratio and the renderer's dimensions. Calling
camera.updateProjectionMatrix() after adjusting the aspect ensures the projection is recalculated correctly, while
renderer.setSize() adjusts the canvas to match the new viewport size without stretching the scene geometry.
Understanding the Resize Problem
When a web page loads, your Three.js application initializes with a specific canvas width and height. The perspective camera uses these values to calculate its field of view and aspect ratio. If the user manually drags the browser window edge, the underlying HTML canvas does not automatically change size unless explicitly told to do so. Without intervention, the 3D scene remains rendered at the original resolution, or worse, it stretches to fill the new space, causing objects to appear squashed or elongated. This visual artifact is known as distortion. Programmatically handling this requires intercepting the window's resize event and triggering a refresh of the rendering pipeline.
Setting Up the Event Listener
The first step is to attach an event listener to the window object that targets the 'resize' event. This listener should be added during the initialization phase of your Three.js application, typically right after you create the renderer and camera. It is crucial to ensure that the handler function has access to the necessary scope variables, such as the camera and renderer instances. You can define a dedicated function for this logic to keep your code modular and readable.
window.addEventListener('resize', onWindowResize);
By separating the resize logic into its own function, you maintain clean architecture and make debugging easier. If you are using modern module bundlers, ensure that this listener is attached in the main entry point or the component where the Three.js scene is mounted. Avoid attaching multiple listeners to the same event, as this can lead to performance issues and unexpected behavior during rapid resizing.
Updating Camera and Renderer Dimensions
Inside your resize handler, the immediate priority is to update the camera's aspect ratio. The aspect ratio is calculated by dividing the current window width by the current window height. Once you assign this new value to the camera's aspect property, you must call
updateProjectionMatrix(). This method rebuilds the internal mathematical matrix used for projecting 3D coordinates onto the 2D screen. Skipping this step will result in a distorted view even if the renderer size is correct.
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
Simultaneously, you need to update the renderer's size. The
setSize method takes two arguments: the new width and the new height. Passing
window.innerWidth and
window.innerHeight ensures the canvas fills the available viewport exactly. This action clears the previous render buffer and prepares the context for drawing the next frame at the correct resolution. Combining these two updates within the same event handler guarantees synchronization between the optical properties of the camera and the physical dimensions of the output canvas.
Handling High-DPI Displays
Modern devices often feature high-density screens where the CSS pixel count differs from the device pixel count. To maintain sharp visuals, you should account for the device pixel ratio. When setting the renderer size, you can multiply the window dimensions by
window.devicePixelRatio. However, you must also inform the renderer about this scaling factor to prevent blurry textures.
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
This configuration ensures that the WebGL context uses the maximum number of physical pixels available. Without this adjustment, scenes may appear fuzzy on Retina displays or other high-DPI monitors. Note that extremely high pixel ratios can impact performance on mobile devices, so consider implementing a threshold or allowing users to toggle this setting if performance becomes a concern. For more details on managing global state and imports efficiently, which often complements this setup, see
Efficient JavaScript Imports with Global Scope.
Common Pitfalls and Debugging
A frequent mistake is forgetting to call
updateProjectionMatrix(). Developers sometimes assume that changing the aspect ratio is sufficient, but the camera matrix is cached and only updated when explicitly requested. Another issue arises when the resize handler is defined before the camera or renderer variables are initialized. Ensure that your closure captures the correct references or pass them as parameters to the handler function.
If you experience jittery resizing, consider debouncing the resize event. Rapid successive resize events can overwhelm the render loop. Using a simple timeout or a library like lodash to debounce the function can smooth out the user experience. Additionally, verify that your CSS does not force the canvas to a fixed size, which would conflict with the JavaScript adjustments.
Frequently Asked Questions
Why does my scene look stretched after resizing?
This happens because the camera's aspect ratio was not updated to match the new window dimensions. Always recalculate
camera.aspect and call
camera.updateProjectionMatrix() inside the resize handler to fix the projection distortion.
Can I use requestAnimationFrame for resizing?
No, resizing should be handled synchronously within the event listener. Using
requestAnimationFrame introduces unnecessary delay and may cause flickering. The resize event fires immediately when the window changes size, requiring instant response.
How do I handle resizing in a React component?
In React, attach the resize listener in the
useEffect hook during the mount phase and remove it in the cleanup function. This prevents memory leaks and ensures the listener is removed when the component unmounts.
Does setSize clear the canvas?
Yes,
renderer.setSize() clears the current framebuffer and resets the viewport rectangle. This is necessary to prepare the WebGL context for drawing the new frame at the updated dimensions.
What if I want to maintain a specific aspect ratio?
If you need to lock the aspect ratio, calculate the new width or height based on the locked ratio rather than using raw window dimensions. Adjust the other dimension accordingly to fit within the available viewport without distorting the content.