Search code examples
three.jsrenderingviewport

Threejs 'view crop' using viewport and scissors


Overview:

  • I have a scene with a positioned perspective camera and multiple assets that produces the output that I want
  • I don't want to move / change the fov of the camera, as this will require me dynamically adjusting all elements in the scene (see debug camera view image)
  • I want a 'view crop' of the scene from whatever the full size is to a smaller size (not always in the same proportion. See resultant canvas image

examples

I have tried combinations of:

renderer.setScissor(0, 0, 320, 240)
renderer.setScissorTest(true)
renderer.setViewport(0, 0, 320, 240)
renderer.setSize(320, 240)
renderer.getContext().canvas.width = 320
renderer.getContext().canvas.height = 240
renderer.getContext().canvas.style.width = '320px'
renderer.getContext().canvas.style.height = '240px'
  • Applying the scissor gives me example what I want to see, because only those items are rendered, but the whole view is still the same size
  • Applying the viewport scales the WHOLE image down,
  • Adjusting the canvas crops relative to the whole image rather than the points I want to crop from.

I CAN do blitting (copy the exact pixels I want onto a separate canvas, but I was hoping there was another simpler way.

Any ideas?

I've added a codepen example here: https://codepen.io/faysvas/pen/KKzPQpa

const makeCube = (scene, color, x) => {
    const material = new THREE.MeshPhongMaterial({ color })
    const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material)
    scene.add(cube)
    cube.position.x = x
    return cube;
}
const addSceneContents = (scene) => {
    const light = new THREE.DirectionalLight(0xffffff, 1)
    light.position.set(-1, 2, 4)
    scene.add(light)
    return [
        makeCube(scene, 0x44aa88, 0),
        makeCube(scene, 0x8844aa, -2),
        makeCube(scene, 0xaa8844, 2)
    ]
}
const main = () => {
    const canvas = document.querySelector("#c")
    const renderer = new THREE.WebGLRenderer({ canvas })
    renderer.setSize(512, 512, false)
    const fov = 75
    const aspect = 1
    const near = 0.1
    const far = 5
    const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
    camera.position.z = 2

    const scene = new THREE.Scene()
    scene.background = new THREE.Color( 0xff0000 )
    let cubes = addSceneContents(scene)

    resizeTo320x240(renderer, canvas) 


    const render = (time) => {
        time *= 0.001
        cubes.forEach((cube, ndx) => {
            const speed = 1 + ndx * 0.1
            const rot = time * speed
            cube.rotation.x = rot
            cube.rotation.y = rot
        })
        renderer.render(scene, camera)
        requestAnimationFrame(render)
    }
    requestAnimationFrame(render)
}
/*
This function should 'crop' from the whole scene without 
distorting the perspective of the camera and ensuring the 
canvas is 320x240
e.g. I want the canvas to be the same size and output of 
red cropped view below. Eg, no black and the canvas (and 
it's red contents) should be in the top left of the corner 
of the screen
*/
const resizeTo320x240 = (renderer, canvas) => {
    console.log('code goes here')
    const desiredWidth = 320
    const desiredHeight = 240
    const currentSize = renderer.getSize(new THREE.Vector2())
    const x = (currentSize.x / 2) - (desiredWidth/2)
    const y = (currentSize.y / 2) - (desiredHeight/2)

    renderer.setScissor(x, y, desiredWidth, desiredHeight)
    renderer.setScissorTest(true)
    //renderer.setViewport(x, y, desiredWidth, desiredHeight)
}
main()
html, body {
   margin: 0;
   height: 100%;
   background-color: grey;
}
#c {
   display: block;
   background-color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r119/three.min.js"></script>
<canvas id="c"></canvas>


Solution

  • I figured it out. The renderer or the viewport is not the place to solve it, instead, the camera itself has the ability to offset or clip it's own output.

    const makeCube = (scene, color, x) => {
        const material = new THREE.MeshPhongMaterial({ color })
        const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material)
        scene.add(cube)
        cube.position.x = x
        return cube;
    }
    const addSceneContents = (scene) => {
        const light = new THREE.DirectionalLight(0xffffff, 1)
        light.position.set(-1, 2, 4)
        scene.add(light)
        return [
            makeCube(scene, 0x44aa88, 0),
            makeCube(scene, 0x8844aa, -2),
            makeCube(scene, 0xaa8844, 2)
        ]
    }
    const main = () => {
        const canvas = document.querySelector("#c")
        const renderer = new THREE.WebGLRenderer({ canvas })
        renderer.setSize(512, 512, false)
        const fov = 75
        const aspect = 1
        const near = 0.1
        const far = 5
        const camera = new THREE.PerspectiveCamera(fov, aspect, near, far)
        camera.position.z = 2
    
        const scene = new THREE.Scene()
        scene.background = new THREE.Color( 0xff0000 )
        let cubes = addSceneContents(scene)
    
        resizeTo320x240(renderer, canvas, camera) 
    
    
        const render = (time) => {
            time *= 0.001
            cubes.forEach((cube, ndx) => {
                const speed = 1 + ndx * 0.1
                const rot = time * speed
                cube.rotation.x = rot
                cube.rotation.y = rot
            })
            renderer.render(scene, camera)
            requestAnimationFrame(render)
        }
        requestAnimationFrame(render)
    }
    /*
    This function should 'crop' from the whole scene without 
    distorting the perspective of the camera and ensuring the 
    canvas is 320x240
    e.g. I want the canvas to be the same size and output of 
    red cropped view below. Eg, no black and the canvas (and 
    it's red contents) should be in the top left of the corner 
    of the screen
    */
    const resizeTo320x240 = (renderer, canvas, camera) => {
        console.log('code goes here')
        const desiredWidth = 320
        const desiredHeight = 240
        
        const currentSize = renderer.getSize(new THREE.Vector2())
        const x = (currentSize.x / 2) - (desiredWidth/2)
        const y = (currentSize.y / 2) - (desiredHeight/2)
        
        // Set the size of the renderer to the correct desired output size
        renderer.setSize(desiredWidth, desiredHeight, false)
      
        // The camera its is one that should be cropped
        // This is referred to as the view offset in three.js
        camera.setViewOffset(currentSize.x,currentSize.y,x,y, desiredWidth, desiredHeight)
    }
    main()
    html, body {
       margin: 0;
       height: 100%;
       background-color: grey;
    }
    #c {
       display: block;
       background-color: red;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r119/three.min.js"></script>
    <canvas id="c"></canvas>