dangarfield
dangarfield

Reputation: 2330

Threejs 'view crop' using viewport and scissors

Overview:

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'

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>

Upvotes: 2

Views: 1910

Answers (1)

dangarfield
dangarfield

Reputation: 2330

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>

Upvotes: 2

Related Questions