0
votes

I am new on threejs, and I don't know this question is absolutely basic or it is too advanced. So, I want to put some objects to the top left corner of the screen. The next object is always goes to the next right position. I need to every object looks like same size. How can I calculate their 3d positions and scales (if need)

I find a solution, which maybe good for me: Three.js: Show world coordinate axes in corner of scene but I think the second canvas is too big overhead - and it cover the original canvas.

I think create a sphere which position is the camera position, and the radius is bigger than screen-camera distance, and with a raycaster find the intersection, and that is the point. Is this a good way?

My objects are arrows (poly and extruded) and they shows the selected directions for my user. So this helper is an information box, but I don't want to cover the original scene. For example 10-12 arrow at the top of the screen. And it need to same size to be beauty.

2

2 Answers

7
votes

If you want to create a heads-up display (HUD), one way is to overlay a second scene of sprites, rendered with an orthographic camera.

The coding pattern to use is like so:

camera = new THREE.PerspectiveCamera( ... );
cameraOrtho = new THREE.OrthographicCamera( ... );

scene = new THREE.Scene();
sceneOrtho = new THREE.Scene(); // overlay scene

renderer = new THREE.WebGLRenderer();
renderer.autoClear = false; // to allow overlay

And, in the render loop:

renderer.clear();
renderer.render( scene, camera );
renderer.clearDepth();
renderer.render( sceneOrtho, cameraOrtho );

See http://threejs.org/examples/webgl_sprites.html.

three.js r.83

0
votes

The following is a working example that has a VisualLayers class for managing any number of layers, and it uses the renderer.autoClear = false and clearing-depth technique.

This approach is nice because renderOrder of objects is not being modified (that's another approach) and thus will not introduce other different issues.

Try playing with the options in the UI to see what it does:

// @ts-check

////////////////////////
// LAYER SYSTEM
////////////////////////

/** @typedef {{name: string, backingScene: THREE.Scene, order: number}} Layer */

class VisualLayers {
    /**
     * @type {Array<Layer>}
     * @private
     */
    __layers = [];

    constructor(
        /** @private @type {THREE.WebGLRenderer} */ __renderer,
        /** @private @type {typeof THREE.Scene} */ __Scene = THREE.Scene
    ) {
        this.__renderer = __renderer;
        this.__Scene = __Scene;
    }

    defineLayer(/** @type {string} */ name, /** @type {number=} */ order = 0) {
        const layer = this.__getLayer(name);

        // The default layer always has order 0.
        const previousOrder = layer.order;
        layer.order = name === "default" ? 0 : order;

        // Sort only if order changed.
        if (previousOrder !== layer.order)
            this.__layers.sort((a, b) => a.order - b.order);

        return layer;
    }

    /**
     * Get a layer by name (if it doesn't exist, creates it with default order 0).
     * @private
     */
    __getLayer(/** @type {string} */ name) {
        let layer = this.__layers.find((l) => l.name === name);

        if (!layer) {
            layer = { name, backingScene: new this.__Scene(), order: 0 };
            layer.backingScene.autoUpdate = false;
            this.__layers.push(layer);
        }

        return layer;
    }

    removeLayer(/** @type {string} */ name) {
        const index = this.__layers.findIndex((l) => {
            if (l.name === name) {
                l.backingScene.children.length = 0;
                return true;
            }

            return false;
        });

        if (index >= 0) this.__layers.splice(index, 1);
    }

    hasLayer(/** @type {string} */ name) {
        return this.__layers.some((l) => l.name === name);
    }

    /** @readonly */
    get layerCount() {
        return this.__layers.length;
    }

    addObjectToLayer(
        /** @type {THREE.Object3D} */ obj,
        /** @type {string | string[]} */ layers
    ) {
        if (Array.isArray(layers)) {
            for (const name of layers) this.__addObjectToLayer(obj, name);
            return;
        }

        this.__addObjectToLayer(obj, layers);
    }

    addObjectsToLayer(
        /** @type {THREE.Object3D[]} */ objects,
        /** @type {string | string[]} */ layers
    ) {
        for (const obj of objects) {
            this.addObjectToLayer(obj, layers);
        }
    }

    /** @private @readonly */
    __emptyArray = Object.freeze([]);

    /** @private */
    __addObjectToLayer(
        /** @type {THREE.Object3D} */ obj,
        /** @type {string} */ name
    ) {
        const layer = this.__getLayer(name);
        const proxy = Object.create(obj, {
            children: { get: () => this.__emptyArray }
        });
        layer.backingScene.children.push(proxy);
    }

    removeObjectFromLayer(
        /** @type {THREE.Object3D} */ obj,
        /** @type {string | string[]} */ nameOrNames
    ) {
        if (Array.isArray(nameOrNames)) {
            for (const name of nameOrNames) {
                const layer = this.__layers.find((l) => l.name === name);
                if (!layer) continue;
                this.__removeObjectFromLayer(obj, layer);
            }
            return;
        }

        const layer = this.__layers.find((l) => l.name === nameOrNames);
        if (!layer) return;
        this.__removeObjectFromLayer(obj, layer);
    }

    /** @private */
    __removeObjectFromLayer(
        /** @type {THREE.Object3D} */ obj,
        /** @type {Layer} */ layer
    ) {
        const children = layer.backingScene.children;
        const index = children.findIndex(
            (proxy) => /** @type {any} */ (proxy).__proto__ === obj
        );

        if (index >= 0) {
            children[index] = children[children.length - 1];
            children.pop();
        }
    }

    removeObjectsFromAllLayers(/** @type {THREE.Object3D[]} */ objects) {
        for (const layer of this.__layers) {
            for (const obj of objects) {
                this.__removeObjectFromLayer(obj, layer);
            }
        }
    }

    render(
        /** @type {THREE.Camera} */ camera,
        /** @type {(layerName: string) => void} */ beforeEach,
        /** @type {(layerName: string) => void} */ afterEach
    ) {
        for (const layer of this.__layers) {
            beforeEach(layer.name);
            this.__renderer.render(layer.backingScene, camera);
            afterEach(layer.name);
        }
    }
}

//////////////////////
// VARS
//////////////////////

let camera, stats, geometry, material, object, object2, root;
let time = 0;
/** @type {THREE.Scene} */
let scene;
/** @type {THREE.WebGLRenderer} */
let renderer;
/** @type {VisualLayers} */
let visualLayers;
const clock = new THREE.Clock();
const greenColor = "#27ae60";
const options = {
    useLayers: true,
    showMiddleBox: true,
    rotate: true,
    layer2Order: 2
};

//////////////////////
// INIT
//////////////////////

~(function init() {
    setup3D();
    renderLoop();
})();

////////////////////////////////
// SETUP 3D
////////////////////////////////

function setup3D() {
    const container = document.createElement("div");
    container.id = "container";
    document.body.appendChild(container);

    // CAMERA
    camera = new THREE.PerspectiveCamera(
        70,
        window.innerWidth / window.innerHeight,
        1,
        10000
    );
    camera.position.x = 0;
    camera.position.z = 500;
    camera.position.y = 0;

    scene = new THREE.Scene();

    // RENDERERS

    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setClearColor(0x111111);
    container.appendChild(renderer.domElement);

    // LAYERS

    visualLayers = new VisualLayers(renderer);
    // Layers don't have to be defined. Adding an object to a layer will
    // automatically create the layer with order 0. But let's define layers with
    // order values.
    visualLayers.defineLayer("layer1", 1);
    visualLayers.defineLayer("layer2", 2);
    visualLayers.defineLayer("layer3", 3);

    // LIGHTS

    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
    directionalLight.position.set(300, 0, 300);
    scene.add(directionalLight);
    visualLayers.addObjectToLayer(directionalLight, [
        "layer1",
        "layer2",
        "layer3"
    ]);

    const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambientLight);
    visualLayers.addObjectToLayer(ambientLight, ["layer1", "layer2", "layer3"]);

    // GEOMETRY

    root = new THREE.Object3D();
    scene.add(root);

    geometry = new THREE.BoxGeometry(100, 100, 100);
    material = new THREE.MeshPhongMaterial({
        color: greenColor,
        transparent: false,
        opacity: 1
    });

    object = new THREE.Mesh(geometry, material);
    root.add(object);
    visualLayers.addObjectToLayer(object, "layer1");
    object.position.y = 80;
    object.position.z = -20;
    // object.rotation.y = -Math.PI / 5

    object2 = new THREE.Mesh(geometry, material);
    object.add(object2);
    visualLayers.addObjectToLayer(object2, "layer2");
    object2.position.y -= 80;
    object2.position.z = -20;
    object2.rotation.y = -Math.PI / 5;

    const object3 = new THREE.Mesh(geometry, material);
    object2.add(object3);
    visualLayers.addObjectToLayer(object3, "layer3");
    object3.position.y -= 80;
    object3.position.z = -20;
    object3.rotation.y = -Math.PI / 5;

    // GUI

    const pane = new Tweakpane({
        title: "VisualLayers"
    });
    pane.addInput(options, "useLayers", { label: "use layers" });
    pane.addInput(options, "showMiddleBox", { label: "show middle box" });
    pane.addInput(options, "rotate");
    pane
        .addInput(options, "layer2Order", {
            label: "layer2 order",
            options: {
                0: 0,
                2: 2,
                4: 4
            }
        })
        .on("change", () => visualLayers.defineLayer("layer2", options.layer2Order));

    // STATS
    // SEE: https://github.com/mrdoob/stats.js

    stats = new Stats();
    stats.domElement.style.position = "absolute";
    stats.domElement.style.left = "0px";
    stats.domElement.style.top = "0px";
    stats.setMode(0);
    document.body.appendChild(stats.domElement);
}

//////////////////////
// RESIZE
//////////////////////

(window.onresize = function (event) {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
})();

//////////////////////
// RAF RENDER LOOP
//////////////////////

function render() {
    stats.begin();

    if (options.rotate) {
        time += clock.getDelta();
        object.rotation.y += 0.02;
        root.rotation.y = Math.PI / 2 + (Math.PI / 6) * Math.sin(time * 0.001);
    }

    object2.visible = options.showMiddleBox;

    if (options.useLayers) {
        scene.updateWorldMatrix(true, true);
        renderer.autoClear = false;
        renderer.clear();
        visualLayers.render(camera, beforeEachLayerRender, afterEachLayerRender);
    } else {
        renderer.autoClear = true;
        renderer.render(scene, camera);
    }

    stats.end();
}

function renderLoop() {
    render();
    requestAnimationFrame(renderLoop);
}

function beforeEachLayerRender(layer) {}
function afterEachLayerRender(layer) {
    renderer.clearDepth();
}
html,
body,
#container {
    margin: 0px;
    padding: 0px;
    width: 100%;
    height: 100%;
}

canvas {
    background: transparent;
    display: block;
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
    top: 0;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/tweakpane.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/stats.js/r11/Stats.min.js"></script>
<script src="//unpkg.com/[email protected]/build/three.min.js"></script>
<script src="//unpkg.com/[email protected]/build/postprocessing.js"></script>

(example on codepen)