devwiki:three_js

Quick Start

Browse Support

  • some feature using WebGL 2.0 and some old browser or some mobile browser may not support it,
  • you can use code to detect webgl2 support and dynamic change the way of coding to support other browser
  • check webgl2 support
    // check webgl2 support for multi sample version
    const gl = document.createElement('canvas').getContext('webgl2');
    if (!gl) {
      alert('your browser/OS/drivers do not support WebGL2');
      console.log('your browser/OS/drivers do not support WebGL2');
    } else {
      console.log('webgl2 works!');
    }

Tools

Three.js Concept

  • Object3D (position, rotation, scale, quaternion rotation): Mesh, Light, Camera, Group, AxesHelper
  • Mesh = Geometry + Material/Shader
  • HTML Element Content ⇐(Result Target) Renderer.render ⇐ Mesh + Light + Camera

example's js vs jsm

  • jsm : js module, it is for use with three.js as module style,
    • like in code “import { OrbitControls } from './path/OrbitControls.js”
    • and use its class like “new OrbitControls( main_cam, main_render.domElement );”
  • js: the traditional js way,
    • load under THREE like “<script src='./path/OrbitControls.js'></script>”
    • and use its class like “new THREE.OrbitControls( main_cam, main_render.domElement );”

3D Format for Web

  • GLTF: GL transmission format, support geo/mat/cam/lgt/scene/ani/bone/morph, (json, binary, binary compressed, embed texture)
    • gltf_obj.scene.children (array) > [] > cam/mesh/light

Common Code

  • matcap material (fake light material without light)
    const brown_mctex = main_texLoader.load('./img/matcap/746761_291C19_AB9385_3C2B27.jpg')
    const brown_mat = new THREE.MeshMatcapMaterial({matcap:brown_mctex})
  • color material
    const white_mat = new THREE.MeshStandardMaterial({
        color:'#54c1c8',
        roughness: 0.3,
        metalness: 0.2,
        side: THREE.DoubleSide
        }) 
  • helper indicator
    // axis
    const main_axis = new THREE.AxesHelper()
    scene.add(main_axis)

Events

  • mouse tracking
    //# == add event: mouse position tracking
    let mouse = {
        x: 0,
        y: 0
    }
    window.addEventListener('mousemove', (event)=>{
        //console.log(event.clientX) // from top left corner
        // top/left to center coordinate
        mouse.x = (event.clientX / output_size.width)*2-1
        mouse.y = -(event.clientY / output_size.height)*2+1
    })
  • double click full window
    window.addEventListener('dblclick', ()=>{
        const full_screen_element = document.fullscreenElement || document.webkitFullscreenElement
        //output.requestFullScreen()
        if (!full_screen_element){
            if (output.requestFullscreen){
                // normal browse support
                output.requestFullscreen()
            } else if (output.webkitRequestFullscreen){
                // safari
                output.webkitRequestFullscreen()
            }
        }else{
            if (document.exitFullscreen){
                // normal browse support
                document.exitFullscreen()
            } else if (document.webkitExitFullscreen){
                // safari
                document.webkitExitFullscreen()
            }
        }
    })
  • simple key down event
    const xSpeed = 0.1;
    const ySpeed = 0.1;
     
    window.addEventListener("keydown", onDocumentKeyDown, false);
    function onDocumentKeyDown(event) {
        const keyCode = event.which;
        console.log(keyCode)
        if (keyCode == 40) {
            boat_obj.position.z += ySpeed;
        } else if (keyCode == 38) {
            boat_obj.position.z -= ySpeed;
        } else if (keyCode == 37) {
            boat_obj.position.x -= xSpeed;
        } else if (keyCode == 39) {
            console.log('forward')
            boat_obj.position.x += xSpeed;
        } 
        else if (keyCode == 32) {
            boat_obj.position.set(0, 0, 0);
        }
    };
  • extra offset key event for tick()
    const offset_step = 0.02
     
    if ( keyboard.pressed("w") ){ 
        main_boat_offset.position.y -= offset_step 
        console.log(main_boat_offset.position.y)
    }
    if ( keyboard.pressed("s") ){ 
        main_boat_offset.position.y += offset_step
        console.log(main_boat_offset.position.y)
    }
    if ( keyboard.pressed("e") ){ 
        main_boat_offset.position.z -= offset_step
        console.log(main_boat_offset.position.z)
    }
    if ( keyboard.pressed("q") ){ 
        main_boat_offset.position.z += offset_step
        console.log(main_boat_offset.position.z)
    }
    if ( keyboard.pressed("d") ){ 
        main_boat_offset.position.x -= offset_step
        console.log(main_boat_offset.position.x)
    }
    if ( keyboard.pressed("a") ){ 
        main_boat_offset.position.x += offset_step 
        console.log(main_boat_offset.position.x)
    }

GUI

  • simple gui control
    // -- gui
    const gui = new dat.GUI({ closed:true, width: 300})
    dat.GUI.toggleHide();
    const global_ctrl = {
        boat: {
            x: 0,
            y: 0,
            z: 0,
            color:0x00ff00,
            slide: ()=>{
                let tl = gsap.timeline();
                const cur_x = green_box_mesh.position.x
                tl.to(green_box_mesh.position, {duration: 2, x: cur_x+2})
                .to(green_box_mesh.position, {duration: 1, x: cur_x})
            },
        }
    }
     
    gui.add(global_ctrl.boat, 'x', -10, 10, 0.1).name('posX').onChange( ()=> {
        boat_obj.position.x = global_ctrl.boat.x
    } )
    gui.add(global_ctrl.boat, 'y', -10, 10, 0.1).name('posY').onChange( ()=> {
        boat_obj.position.y = global_ctrl.boat.y
    } )
    gui.add(global_ctrl.boat, 'z', -50, 50, 0.1).name('posZ').onChange( ()=> {
        boat_obj.position.z = global_ctrl.boat.z
    } )
     
    gui.addColor(global_ctrl.boat, 'color').onChange(
        ()=>{
            green_box_mesh.material.color.set(global_ctrl.green_box.color)
        }
    )
    gui.add(global_ctrl.boat, 'slide')

font

  • text
    //-- font 
    const text_mat = new THREE.MeshMatcapMaterial({matcap:brown_mctex})
     
    let text_mesh = undefined
    const main_fontLoader = new THREE.FontLoader(load_manager)
    main_fontLoader.load(
        './font/helvetiker_regular.typeface.json',
        (result_font)=>{
            console.log('font loaded')
            const bevel_a = 0.01 //2
            const bevel_b = 0.01 //3
            const text_geo = new THREE.TextBufferGeometry(
                'Drive Through to Next Scene > ',{
                    font: result_font,
                    size: 0.2,
                    height: .05,
                    curveSegments: 3,
                    // bevelEnabled: true,
                    // bevelThickness: bevel_b,
                    // bevelSize: bevel_a,
                    // bevelOffset: 0, 
                    // bevelSegments: 2
                }
            )
            text_geo.computeBoundingBox() // max3, min3
            text_geo.center()
     
            text_mesh = new THREE.Mesh(text_geo, white_2_mat) // text_mat
            text_mesh.position.y = 1
            text_mesh.position.x = 123
            scene.add(text_mesh)
        }
    )

Three.js Problem and Solution

  • limit the maximum view angle on width or basically auto adjust vFOV given hFOV (basically check width and fov dynamically):
    // ref: https://discourse.threejs.org/t/responsive-renderer-with-limits/4401/16
    renderer.setSize( window.innerWidth, window.innerHeight );
    camera.aspect = window.innerWidth / window.innerHeight
    const viewAspect = viewWidth / viewHeight 
    const special = camera.aspect > viewAspect
    uniform.value = special ? 1 : 0
     
    if(special){
      const camH = Math.tan(THREE.Math.degToRad(myFov/2))
      const ratio = camera.aspect / viewAspect
      const newH = camH / ratio
      const newFov = THREE.Math.radToDeg(Math.atan(newH)) * 2
      camera.fov = newFov
     } else {
      camera.fov = myFov
     }
    camera.updateProjectionMatrix()
    //https://github.com/mrdoob/three.js/issues/15968
    const hFOV = 50; // desired horizontal fov, in degrees
    camera.fov = Math.atan( Math.tan( hFOV * Math.PI / 360 ) / camera.aspect ) * 360 / Math.PI; // degrees
    camera.updateProjectionMatrix();
  • Handle cloud like soft transparent texture geo properly
    const tmp_map = child.material.map
    const tmp_mat = new THREE.MeshBasicMaterial({
        map: child.material.map,
        side: THREE.DoubleSide,
        transparent: true
    })
    child.material = tmp_mat
  • Handle leaf like transparent texture geo properly:
    • code
      // reuse use the png texture, create basic material replace with blend mode to alpha clip and clip at 0.5
      const tmp_map = child.material.map
      const tmp_mat = new THREE.MeshBasicMaterial({
          map: child.material.map,
          side: THREE.DoubleSide,
          alphaMode:"MASK",
          alphaTest:0.5
      })
      child.material = tmp_mat
       
      // or set properly in blender before bring into threejs
      // ref: https://www.youtube.com/watch?v=03isI0_FGLU&t=61s
          inside blender, 
          transparent node->1-> mix shader
          use map-c> diffuse node ->2-> mix shader 
          use map-a> mix shader.fac
          mix shader -> out.surface
          then side panel:
          make material option : 
          * blend mode: alpha clip
          * and set roughness to 1
  • set texture node filter and optionally disable mip map use
    child.material.map.inFilter = THREE.NearestFilter
    child.material.map.generateMipmaps = false
  • make object ignore camera frustum culling, so that when camera move closer, object won't disappear
    if(child.name.startsWith("sch_word_")){
        child.frustumCulled = false
    }
  • video as texture
    // ref: https://stemkoski.github.io/Three.js/
    // https://threejs.org/docs/#api/en/textures/VideoTexture
     
    // 1. define global variable for controls
    let video = undefined
    let videoImage = undefined
    let videoImageContext = undefined
    let videoTexture = undefined
    let mov_mat = undefined
     
    // 2. (after all your model loaded event) create video material
    video = document.createElement( 'video' );
    video.src = "video/my_video.mp4";
    video.load();
    // video.play(); // (don't play on start)
    videoImage = document.createElement( 'canvas' );
    videoImage.width = 640;
    videoImage.height = 480;
    videoImageContext = videoImage.getContext( '2d' );
    // background color if no video present
    videoImageContext.fillStyle = '#000000';
    videoImageContext.fillRect( 0, 0, videoImage.width, videoImage.height );
    videoTexture = new THREE.Texture( videoImage );
    videoTexture.minFilter = THREE.LinearFilter;
    videoTexture.magFilter = THREE.LinearFilter;
    mov_mat = new THREE.MeshBasicMaterial( { map: videoTexture, side:THREE.DoubleSide } ); // overdraw: true,
     
    // 3. assign to geo
    my_video_plane_geo.material = mov_mat;
     
    // 4. call video_update() during render each frame
    function video_update(){
        if ( video.readyState === video.HAVE_ENOUGH_DATA ) 
        {
            videoImageContext.drawImage( video, 0, 0 );
            if ( videoTexture ) 
                videoTexture.needsUpdate = true;
        }
    }
     
    // 5. support functions for call from other event
    function video_start(){
        //bgm_sound.pause(); // option to mute bgm sound if you have any
        video.currentTime = 0;
        video.play();
    }
    function video_stop(){
        //bgm_sound.play(); // option to on bgm sound if you have any
        video.pause();
        video.currentTime = 0;
    }
  • language based texture switch
    // method 1: (works in firefox, not in chrome)
    if(child.name == "lang_texture_geo") ){
        lang_texture_geo = child
        if(main_lang=="cn"){
            lang_texture_geo.material.map.image.src = "./img/cn_texture.jpg";
        }
    }
    // method 2: (works in both firefox and chrome)
    const main_texLoader = new THREE.TextureLoader(load_manager)
    const cn_text_ctex = main_texLoader.load("./img/cn_texture.jpg");
    cn_text_ctex.flipY = false;
    if(child.name == "lang_texture_geo") ){
        lang_texture_geo = child
        if(main_lang == "cn"){
            child.material.map = cn_text_ctex;
        }
    }

Template

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<style>
body{
    padding: 0px 0px;
    margin: 0px 0px;
}
</style>
</head>
<body>
<!--
https://www.cdnpkg.com/
https://github.com/dataarts/dat.gui/tree/master/build
https://threejs.org/
-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.1/gsap.min.js"></script>
<script type="text/javascript" src="./js/dat.gui.min.js"></script>
<script type="module">
import * as THREE from './js/three.module.js';
import { OrbitControls } from './js/OrbitControls.js';
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
 
// render setting
renderer.setSize(innerWidth, innerHeight)
renderer.setPixelRatio(devicePixelRatio)
document.body.appendChild(renderer.domElement)
 
// content - geo
const box_geo = new THREE.BoxGeometry(1, 1, 1)
const plane_geo = new THREE.PlaneGeometry(5,5,10,10)
 
// content - mat
const main_mat = new THREE.MeshBasicMaterial({color:0x00FF00})
const plane_mat = new THREE.MeshBasicMaterial({color:0xFF0000, side: THREE.DoubleSide }) // like surface shader
const phong_mat = new THREE.MeshPhongMaterial({color:0xff0000, side: THREE.DoubleSide }) // need light
const phong_2_mat = new THREE.MeshPhongMaterial({color:0xff0000, side: THREE.DoubleSide, flatShading: THREE.FlatShading }) // has shades
const phong_vtx_mat = new THREE.MeshPhongMaterial({side: THREE.DoubleSide, flatShading: THREE.FlatShading, vertexColors: true }) // has shades
// content - mesh
const plane_mesh = new THREE.Mesh(plane_geo, phong_vtx_mat)
const main_mesh = new THREE.Mesh(box_geo, main_mat)
scene.add(main_mesh)
scene.add(plane_mesh)
 
console.log(plane_mesh)
//console.log(plane_mesh.geometry.vertices[0])
 
 
// content - light
const main_light = new THREE.DirectionalLight(0xffffff, 1)
main_light.position.set(0,1,1)
scene.add(main_light)
 
const back_light = new THREE.DirectionalLight(0xffffff, 1)
back_light.position.set(0, 0, -1)
scene.add(back_light) // fill in bottom
 
// camera and frame render
 
camera.position.z = 5;
 
const controls = new OrbitControls( camera, renderer.domElement );
 
// raycaster
const raycaster = new THREE.Raycaster()
 
 
 
const gui = new dat.GUI()
const world = {
    plane: {
        width: 10,
        height: 10,
        w_seg: 10,
        h_seg: 10,
    }
}
// mesh randomizer
//console.log(plane_mesh.geometry.attributes.position.array.length) // old structure
//console.log(pos_list[i]+', ' + pos_list[i+1] +','+ pos_list[i+2]) // old structure as x1,y1,z1,x2,y2,z2
 
gui.add(world.plane, 'width', 1, 20).onChange( ()=> {
    make_plane()
} )
 
gui.add(world.plane, 'height', 1, 20).onChange( ()=> {
    //console.log(world.plane.width)
    make_plane()
} )
gui.add(world.plane, 'w_seg', 1, 20).onChange( ()=> {
    make_plane()
} )
 
gui.add(world.plane, 'h_seg', 1, 20).onChange( ()=> {
    //console.log(world.plane.width)
    make_plane()
} )
 
function make_plane(){
    plane_mesh.geometry.dispose()
    plane_mesh.geometry = new THREE.PlaneGeometry(world.plane.width, world.plane.height, world.plane.w_seg, world.plane.h_seg)
 
    const pos_list=plane_mesh.geometry.attributes.position.array
    //plane_mesh.geometry.vertices
 
    for (let i = 0; i<pos_list.length; i+=3){
        const z = pos_list[i+2]
        //pos_list[i].z
        pos_list[i+2] = z + Math.random()*0.5
    }
    // make random
    const random_info = []
    // make color
    const vtx_cnt = plane_mesh.geometry.attributes.position.count
    const color_info = []
    for (let i = 0; i< vtx_cnt; i++){
        color_info.push(0,0.19,0.4) // initial color
        random_info.push(Math.random()-0.5) // initial color
    }
    plane_mesh.geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(color_info),3) )
    plane_mesh.geometry.attributes.position.originalPosition = plane_mesh.geometry.attributes.position.array
    plane_mesh.geometry.attributes.position.randomValues = random_info
}
make_plane()
 
// event
const mouse = {
    x: undefined,
    y: undefined
}
addEventListener('mousemove', (event)=>{
    //console.log(event.clientX) // from top left corner
    // top/left to center coordinate
    mouse.x = (event.clientX / innerWidth)*2-1
    mouse.y = -(event.clientY / innerHeight)*2+1
    //console.log(mouse)
 
})
let frame = 0
function animate(){
    frame += 1
    requestAnimationFrame(animate)
    renderer.render(scene, camera)
    controls.update()
    main_mesh.rotation.x += 0.01
    //plane_mesh.rotation.x += 0.01*0.5
 
    // -- color variation motion by mouse
    raycaster.setFromCamera(mouse, camera)
    const x_points = raycaster.intersectObject(plane_mesh)
    //console.log(x_points)
    if (x_points.length>0){
        //console.log('got point')
        const affected_vtx_id_list = [ x_points[0].face.a,  x_points[0].face.b,  x_points[0].face.c ]
 
        const tmp_color = x_points[0].object.geometry.attributes.color
        //const {color} = x_points[0].object.geometry.attributes // name of color must be the same as it's under attribute
 
 
        x_points[0].object.geometry.attributes.color.needsUpdate = true
        const init_color = {r:0,g:.19, b: 0.4} // return back to this color after hover
        const hover_color = {r:0.1,g:.5, b: 1}
        gsap.to(hover_color, {
            r: init_color.r,
            g: init_color.g,
            b: init_color.b,
            duration: 1,
            onUpdate: () => { 
                //console.log(hover_color)
                // first tmp_color.setX(affected_vtx_id_list[0],hover_color.r) // first interaction
                tmp_color.setX(affected_vtx_id_list[0],hover_color.r) // first interaction
                tmp_color.setY(affected_vtx_id_list[0],hover_color.g) 
                tmp_color.setZ(affected_vtx_id_list[0],hover_color.b) 
 
                tmp_color.setX(affected_vtx_id_list[1],hover_color.r) 
                tmp_color.setY(affected_vtx_id_list[1],hover_color.g) 
                tmp_color.setZ(affected_vtx_id_list[1],hover_color.b) 
 
                tmp_color.setX(affected_vtx_id_list[2],hover_color.r)
                tmp_color.setY(affected_vtx_id_list[2],hover_color.g) 
                tmp_color.setZ(affected_vtx_id_list[2],hover_color.b) 
 
                }
        })
    }
 
    // -- position variation motion by time
    const pos_list=plane_mesh.geometry.attributes.position.array
    const pos_list_orig =plane_mesh.geometry.attributes.position.originalPosition
    const pos_list_rand =plane_mesh.geometry.attributes.position.randomValues
    for (let i = 0; i<pos_list.length; i+=3){
        pos_list[i+2] = pos_list_orig[i+2] + Math.cos(frame*0.03+pos_list_rand[i/3]*100)*0.001
    }
    plane_mesh.geometry.attributes.position.needsUpdate = true
}
 
 
animate()
 
</script>
<!-- info: tailwind css-->
<div id='mainTxt'>
    <div style='position:absolute;color:white;top:30%;left:50%;transform: translate(-50%,-50%);text-align:center;-webkit-font-smoothing-:antialiased'>
    <h1>Good Animation</h1>
    <p>Good One</p>
    </div>
</div>
 
</body>
</html>

template 2

<!DOCTYPE HTML>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
    <title>Three</title>
    <style>
body, canvas{
    margin: 0px 0px;
    padding: 0px 0px;
}
canvas{
    /* fix outline and gaps */
    outline: none;
    position: fixed;
}
html,body{
    overflow: hidden; /*stop mac over drag*/
}
.loadbar{
    position: absolute;
    top:50%;
    width:100%;
    height: 2px;
    background: #ffffff;
    transform: scaleX(0);
    transform-origin: top left;
    transition: transform 0.5s;
    will-change: transform;
}
.loadbar.ended{
    transform-origin: top right;
    transition: transform 1s ease-in-out;
}
.point{
position: absolute;
top:50%;
left:50%;
}
.point.visible .label{
    transform: scale(1,1);
}
.point .label{
    position: absolute'
    top:-20px;
    left:-20px;
    width: 40px;
    height: 40px;
    border-radius:50%;
    color: #ffffff;
    background:#00000077;
    font-family: Helvetica, Arial, sans-serif;
    text-align: center;
    line-height: 40px;
    font-weight: 100;
    font-size: 14px;
    cursor: help;
    transform: scale(0,0);
    transition: transform 0.3s;
}
.point:hover .text{
    opacity: 1;
 
}
.point .text{
    position: absolute;
    top:30px;
    left: -120px;
    width: 200px;
    padding: 20px;
    border-radius: 4px;
    background: #00000077;
    color: #ffffff;
    line-height: 1.3em;
    font-family: Helvetica, Arial, sans-serif;
    font-weight: 100;
    text-align: center;
    font-size: 14px;
    opacity: 0;
    transition: opacity 0.3s;
    pointer-events: none;
}
    </style>
</head>
<body>
    <canvas class='view'></canvas>
    <div class='loadbar'></div>
 
    <div class='point point-0'>
        <div class='label'>1</div>
        <div class='text'>Some Description Here</div>
 
    </div>
 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.7.1/gsap.min.js"></script>
    <script src='./script/three.min.js'></script>
    <script src='./script/OrbitControls.js'></script>
    <script src='./script/GLTFLoader.js'></script>
    <script src="./script/dat.gui.min.js"></script>
    <script src='./script.js'></script>
    <script>
    </script>
</body>
</html>
//import './style.css'
 
//import * as THREE from 'three'
//import { OrbitControls } from './script/OrbitControls.js'
 
 
console.log('Hello Three.js')
//console.log(THREE)
 
const scene = new THREE.Scene()
 
//-- loading manager
const load_ui = document.querySelector('.loadbar')
const load_manager = new THREE.LoadingManager(
    // loaded
    () => {
        gsap.delayedCall(0.5, () =>{
            console.log('loaded')
            gsap.to(cover_mesh.material.uniforms.uAlpha, {duration: 3, value: 0 })
            load_ui.classList.add('ended')
            load_ui.style.transform = ''    
        })
 
 
 
    },
    // progress
    (itemUrl, itemLoaded, itemTotal) => {
        console.log('loading')
        const progress_ratio = itemLoaded / itemTotal
        load_ui.style.transform = `scaleX(${progress_ratio})`
 
    }
)
 
// -- cover-object
// original 3d space =
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0)
const cover_mesh = new THREE.Mesh(
    new THREE.PlaneBufferGeometry(2,2,1,1),
    new THREE.ShaderMaterial({
        // wireframe: true,
        transparent: true,
        uniforms:{
            uAlpha:{value:1}
        },
        vertexShader:`
            void main(){
                gl_Position = vec4(position, 1.0);
            }
        `,
        fragmentShader: `
            uniform float uAlpha;
            void main(){
                gl_FragColor = vec4(0,0,0, uAlpha);
            }
        `
    })
    )
scene.add(cover_mesh)
 
//--points
const points = [
    {
        position: new THREE.Vector3(1.55, 0.3, -0.6),
        element: document.querySelector('.point-0')
 
    }
]
 
// Mesh = Geo + Shader
const box_geo = new THREE.BoxGeometry(1,2,1)
 
// -- mat (0xff0000, 'red', '#ff0000', 'rgb(255,0,0)', 'rgb(100%,0%,0%)',)
// -- 'hsl(0,100%,50%)', 1,0,0
const default_mat = new THREE.MeshBasicMaterial( {color: '#ff0000'} )
 
// -- mesh
const box_mesh = new THREE.Mesh(box_geo, default_mat)
box_mesh.position.x=2
box_mesh.scale.set(2,1,1)
 
const blue_box_mesh = new THREE.Mesh(new THREE.SphereBufferGeometry(.5,16,16), new THREE.MeshBasicMaterial( {color: '#0000ff'}) )
scene.add(blue_box_mesh)
 
const green_box_mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(1,1,1,2,2,2), new THREE.MeshBasicMaterial( {color: '#00ff00', wireframe: true}) )
scene.add(green_box_mesh)
green_box_mesh.position.x = 1
green_box_mesh.position.z = 1
green_box_mesh.rotation.reorder('YXZ')
 
// -- custom geo
 
const pos_array = new Float32Array([
    0,0,0,
    0,4,0,
    4,0,0
])
const pos_attr = new THREE.BufferAttribute(pos_array, 3)
const pos_geo = new THREE.BufferGeometry()
pos_geo.setAttribute('position', pos_attr)
const cust_mesh = new THREE.Mesh(pos_geo, new THREE.MeshBasicMaterial( {color: 0x00ff00, wireframe: true}) )
scene.add(cust_mesh)
 
//box_mesh.rotation.reorder('YXZ') // set order first if needed
box_mesh.rotation.y=Math.PI*0.5 // pi value radient value
 
//scene.add(box_mesh)
 
// -- texture
// ----- step method
/*
const door_img_file = new Image()
const door_img_tex = new THREE.Texture(door_img_file)
door_img_file.onload = () => {
    door_img_tex.needsUpdate = true
}
door_img_file.src = './img/door/color.jpg'
default_mat.map = door_img_tex
default_mat.color = undefined
*/
// ----- simple method
 
//const loading_manager = new THREE.LoadingManager() 
//loading_manager.onStart = ()=>{}
const door_texLoader = new THREE.TextureLoader(load_manager) // optional parent loading_manager
const door_ctex = door_texLoader.load('./img/door/color.jpg')
const door_atex = door_texLoader.load('./img/door/alpha.jpg')
const door_htex = door_texLoader.load('./img/door/height.jpg')
const door_ntex = door_texLoader.load('./img/door/normal.jpg')
const door_aotex = door_texLoader.load('./img/door/ambientOcclusion.jpg')
const door_mtex = door_texLoader.load('./img/door/metalness.jpg')
const door_rtex = door_texLoader.load('./img/door/roughness.jpg')
 
const particle_ctex = door_texLoader.load('./img/sprite/2.png')
 
 
// const brown_mctex = door_texLoader.load('./img/matcap/7B5254_E9DCC7_B19986_C8AC91.jpg')
const brown_mctex = door_texLoader.load('./img/matcap/746761_291C19_AB9385_3C2B27.jpg')
/*
door_ctex.repeat.x = 1.5
door_ctex.repeat.y = 1.5
door_ctex.wrapS = THREE.MirroredRepeatWrapping
door_ctex.wrapT = THREE.MirroredRepeatWrapping
door_ctex.offset.x = .5
door_ctex.offset.y = .5
door_ctex.center.x = 0.5 // for rotateCenter
door_ctex.center.y = 0.5 
door_ctex.rotation = Math.PI*.25
*/
//door_ctex.generateMipmaps = false // for those blocky style texture
//door_ctex.minFilter = THREE.NearestFilter // linearFilter: blur 
//door_ctex.magFilter = THREE.NearestFilter // NearestFilter: sharp blocky pixel
default_mat.map = door_ctex // after intialized
default_mat.color = undefined
// default_mat.color = new THREE.Color('pink')
default_mat.transparent = true
// default_mat.opacity = 0.5
door_atex.minFilter = THREE.NearestFilter // fix distance tex blur issue
default_mat.alphaMap = door_atex
 
default_mat.side = THREE.DoubleSide
 
 
//blue_box_mesh.material.flatShading = true 
// - change to lambert
// blue_box_mesh.material = new THREE.MeshLambertMaterial({color:'blue'}) // need light
// blue_box_mesh.material = new THREE.MeshPhongMaterial({color:'blue'}) // need light
// blue_box_mesh.material = new THREE.MeshToonMaterial({color:'blue'}) // need light, can take gradientMap
blue_box_mesh.material = new THREE.MeshStandardMaterial({color:'grey'}) // need light, more PBR map options
 
// -- light
const main_light = new THREE.DirectionalLight(0xffffff, 1)
main_light.position.set(1,1,1)
scene.add(main_light)
// ----- environment method
const env_cubeTexLoader = new THREE.CubeTextureLoader(load_manager)
const env_tex = env_cubeTexLoader.load([
    './img/env/4r/px.png',
    './img/env/4r/nx.png',
    './img/env/4r/py.png',
    './img/env/4r/ny.png',
    './img/env/4r/pz.png',
    './img/env/4r/nz.png'
])
scene.background = env_tex
scene.environment = env_tex // global mesh envMap setup
 
blue_box_mesh.material.envMap = env_tex
blue_box_mesh.material.metalness= .8
blue_box_mesh.material.roughness= .1
 
const white_mat = new THREE.MeshStandardMaterial({
    color:'#eee',
    roughness: 0.3,
    metalness: 0.2,
    side: THREE.DoubleSide
    }) 
const base_floor_mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(5,5,2,2), white_mat)
base_floor_mesh.rotation.x = Math.PI*0.5
base_floor_mesh.position.y = -2
scene.add(base_floor_mesh)
base_floor_mesh.receiveShadow = true
 
 
const base_box_mesh = new THREE.Mesh(new THREE.BoxBufferGeometry(0.8,0.8,2,2), white_mat)
base_box_mesh.position.y = -1.5
scene.add(base_box_mesh)
base_box_mesh.castShadow = true
 
const base_areaLight = new THREE.RectAreaLight(0x4e00ff,5, 1, 1)
base_areaLight.position.set(1.5,-1,1.5)
base_areaLight.lookAt(base_box_mesh.position)
scene.add(base_areaLight)
 
const base_spotLight = new THREE.SpotLight(0x78fff00,0.5, 4, Math.PI*0.25, .25, 1) // 2nd last, softness
base_spotLight.position.set(-1.5,-1,1.5)
scene.add(base_spotLight)
scene.add(base_spotLight.target)
base_spotLight.target.position.set(0,-1.5,0)
base_spotLight.castShadow = true
base_spotLight.shadow.mapSize.width = 1024
base_spotLight.shadow.mapSize.height = 1024
base_spotLight.shadow.radius = 10 // softness
// for smooth object, to avoid self shadow casting, you need to tweak per light shadow map
//base_spotLight.shadow.normalBias = 0.05 // so push shadow calcultion into geo to avoid geo self shadow
// for flat object, try shadow.bias
// shadow also can be limited but clipping plane from lgiht
 
const base_spotLight_helper = new THREE.SpotLightHelper(base_spotLight,0xffffff)
scene.add(base_spotLight_helper)
 
 
 
// -- mesh instance
// group
const main_grp = new THREE.Group()
scene.add(main_grp)
main_grp.add(box_mesh)
main_grp.position.x=1
 
 
//-- font 
const text_mat = new THREE.MeshMatcapMaterial({matcap:brown_mctex})
text_mat.wireframe = false
// text_mat.flatShading = true
 
const main_fontLoader = new THREE.FontLoader(load_manager)
main_fontLoader.load(
    './font/helvetiker_regular.typeface.json',
    (result_font)=>{
        console.log('font loaded')
        const bevel_a = 0.02
        const bevel_b = 0.03
        const text_geo = new THREE.TextBufferGeometry(
            'Shining & Lucy',{
                font: result_font,
                size: 0.5,
                height: .05,
                curveSegments: 3,
                bevelEnabled: true,
                bevelThickness: bevel_b,
                bevelSize: bevel_a,
                bevelOffset: 0, 
                bevelSegments: 2
            }
        )
        text_geo.computeBoundingBox() // max3, min3
        text_geo.center()
 
        const text_mesh = new THREE.Mesh(text_geo, text_mat)
        // text_mesh.position.y = -2
        scene.add(text_mesh)
    }
)
// -- random geo
console.time('randomGeo')
for (let i=0; i<100;i++){
    const tmp_geo = new THREE.TorusBufferGeometry(0.3,0.2, 20,45)
    const tmp_mesh = new THREE.Mesh(tmp_geo, text_mat)
 
    tmp_mesh.position.x = (Math.random()-0.5)*10
    tmp_mesh.position.y = (Math.random()-0.5)*10
    tmp_mesh.position.z = (Math.random()-0.5)*10
    tmp_mesh.rotation.x = (Math.random()-0.5)*Math.PI
    tmp_mesh.rotation.y = (Math.random()-0.5)*Math.PI
    tmp_s = Math.random()
    tmp_mesh.scale.set(tmp_s, tmp_s, tmp_s)
 
    scene.add(tmp_mesh)
}
console.timeEnd('randomGeo')
// camera (x,Y,z) , same coordinate system as Maya, 
// (x for right, z for close, Y for up)
/*
const output_size = {
    width: 800,
    height: 400
}
*/
 
// -- particle
const particle_geo = new THREE.SphereBufferGeometry(1,32,32)
 
const particle_mat = new THREE.PointsMaterial({
    size: 0.02,
    sizeAttenuation: true
})
const particle_sys = new THREE.Points(particle_geo, particle_mat)
scene.add(particle_sys)
// - color
const color_up = new THREE.Color(0xff0000)
const color_dn = new THREE.Color(0x0000ff)
console.log(particle_geo.attributes.position.array[1])
const ramp_color_list = new Float32Array(particle_geo.attributes.position.count*3)
for (let i = 0; i<particle_geo.attributes.position.count;i++){
    const mixedColor = color_dn.clone()
    const cur_y = particle_geo.attributes.position.array[i*3+1]
    mixedColor.lerp(color_up, (cur_y+1)/2)
    ramp_color_list[i*3] = mixedColor.r
    ramp_color_list[i*3+1] = mixedColor.g
    ramp_color_list[i*3+2] = mixedColor.b
}
particle_geo.setAttribute('color', new THREE.BufferAttribute(ramp_color_list,3))
particle_mat.vertexColors = true
 
// -- particle random
const particle_random_geo = new THREE.BufferGeometry()
const count = 500
const random_pos_list = new Float32Array(count*3)
for (let i = 0; i<count*3;i++){
    random_pos_list[i] = (Math.random()-0.5)*10
}
particle_random_geo.setAttribute('position', new THREE.BufferAttribute(random_pos_list,3))
const particle_random_mat = new THREE.PointsMaterial({
    size: 0.2,
    sizeAttenuation: true,
    // color: "#ff88cc",
    //map: particle_ctex,
    transparent: true,
    alphaMap: particle_ctex,
    // alphaTest: 0.01 // edge pixel killer
    // depthTest: false // render just overlaps and no edge render
 
    //aditional blending method
    blending : THREE.AdditiveBlending,
    depthWrite: false, // looks ok
 
})
const particle_random_sys = new THREE.Points(particle_random_geo,particle_random_mat)
scene.add(particle_random_sys)
// delete particles
 
//particle_random_sys.geometry.dispose()
//particle_random_sys.material.dispose()
//scene.remove(particle_random_sys)
 
 
//--- random particle color
const random_color_list = new Float32Array(count*3)
for (let i = 0; i<count*3;i++){
    random_color_list[i] = Math.random()
}
particle_random_geo.setAttribute('color', new THREE.BufferAttribute(random_color_list,3))
particle_random_mat.vertexColors = true
 
 
// -- load model
const model_loader = new THREE.GLTFLoader(load_manager)
const mixer = null
model_loader.load('./model/boat.gltf',(the_gltf)=>{
    console.log(the_gltf)
    const boat_obj = the_gltf.scene.children[2]
    boat_obj.scale.set(0.5,0.5,0.5)
    boat_obj.position.y= 2
    boat_obj.material = new THREE.MeshStandardMaterial({color:'white'})
 
    // this is for per mat env setup, use global scene env instead
    // boat_obj.material.envMap = env_tex 
 
    scene.add(boat_obj) // it add to our scene and remove from gltf scene
    console.log(boat_obj)
 
    /*
    // loop method
    while(the_gltf.scene.children.length){
        scene.add(the_gltf.scene.children[0])
    }
    // copy method
    const child_list = [...the_gltf.scene.children]
    for (const child of child_list){
        scene.add(child)
    }
    // animation, animation is under each node
    mixer = new THREE.AnimationMixer(the_gltf.scene)
    const action = mixer.ClipAction(the_gltf.animation[0])
    action.play()
 
    */
 
    //updateAllMaterial()
 
 
    })
 
// update all mat
const updateAllMaterial = ()=>{
    scene.traverse((child)=>{
        if (child instanceof THREE.Mesh && child.material instanceof THREE.MeshStandardMaterial){
            child.castShadow = true
            child.receiveShadow = true
            // child.needsUpdate = true
        }
 
    })
}
// full screen
const output_size = {
    width: window.innerWidth,
    height: window.innerHeight
}
let ratio = output_size.width / output_size.height
const main_cam = new THREE.PerspectiveCamera(75, ratio )
 
 
// main_cam.position.z = 5
main_cam.position.set(0,0,5)
scene.add(main_cam)
 
// aim Action, like Maya aimConstraint and delete
// new THREE.Vector3(0,0,0)
main_cam.lookAt(box_mesh.position)
 
// axis
const main_axis = new THREE.AxesHelper()
scene.add(main_axis)
 
// main_render
const output = document.querySelector('canvas.view')
const main_render = new THREE.WebGLRenderer(
    {
        canvas: output,
        antialias: true, // need to be setup initially
 
    }
)
main_render.setSize(output_size.width, output_size.height)
main_render.setPixelRatio(Math.min(window.devicePixelRatio, 2) ) // limit to 2x render
// physical light style
main_render.physicallyCorrectLights = true // abit dimmer
// render encoding, default linear 
main_render.outputEncoding = THREE.sRGBEncoding
// tone map (NoToneMapping, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping)
// main_render.toneMapping = THREE.ACESFilmicToneMapping
// main_render.toneMappingExposure = 3
 
 
 
// - enable shadow globally
// -- mesh,light who make shadow needs Enabled (point, direct, spot)
// -- mesh receive shadow need enabled
// main_render.shadowMap.enabled = true
// shadow draw method 
//(PCFShadowMap, fast:BasicShadowMap, better:PCFSoftShadowMap)
// main_render.shadowMap.type = THREE.PCFSoftShadowMap // no shadow blur option
 
// event
let mouse = {
    x: 0,
    y: 0
}
window.addEventListener('mousemove', (event)=>{
    //console.log(event.clientX) // from top left corner
    // top/left to center coordinate
    mouse.x = (event.clientX / output_size.width)*2-1
    mouse.y = -(event.clientY / output_size.height)*2+1
 
})
 
window.addEventListener('resize', ()=>{
    //console.log("new width:"+window.innerWidth)
    output_size.width = window.innerWidth
    output_size.height = window.innerHeight
    // update cam
    ratio = output_size.width / output_size.height
    main_cam.aspect = ratio
    main_cam.updateProjectionMatrix()
    // update render
    main_render.setSize(output_size.width, output_size.height)
})
 
window.addEventListener('dblclick', ()=>{
    console.log()
    const full_screen_element = document.fullscreenElement || document.webkitFullscreenElement
    //output.requestFullScreen()
    if (!full_screen_element){
        if (output.requestFullscreen){
            // normal browse support
            output.requestFullscreen()
        } else if (output.webkitRequestFullscreen){
            // safari
            output.webkitRequestFullscreen()
        }
    }else{
        if (document.exitFullscreen){
            // normal browse support
            document.exitFullscreen()
        } else if (document.webkitExitFullscreen){
            // safari
            document.webkitExitFullscreen()
        }
    }
 
})
 
// -- gui
const gui = new dat.GUI({ closed:true, width: 300})
const global_ctrl = {
    green_box: {
        y: 0,
        color:0x00ff00,
        slide: ()=>{
            let tl = gsap.timeline();
            const cur_x = green_box_mesh.position.x
            tl.to(green_box_mesh.position, {duration: 2, x: cur_x+2})
            .to(green_box_mesh.position, {duration: 1, x: cur_x})
        },
        deleteParticle: ()=>{
            particle_random_sys.geometry.dispose()
            scene.remove(particle_random_sys)
        }
 
    }
}
gui.add(global_ctrl.green_box, 'y', 0, 5, 0.1).name('posY').onChange( ()=> {
    green_box_mesh.position.y = global_ctrl.green_box.y
} )
gui.addColor(global_ctrl.green_box, 'color').onChange(
    ()=>{
        green_box_mesh.material.color.set(global_ctrl.green_box.color)
    }
)
gui.add(global_ctrl.green_box, 'slide')
gui.add(global_ctrl.green_box, 'deleteParticle')
gui.add(green_box_mesh, 'visible')
gui.add(green_box_mesh.material, 'wireframe')
// -- cam ctrl
const orbit_ctrl = new THREE.OrbitControls( main_cam, main_render.domElement );
orbit_ctrl.enableDamping = true // enable ease after mouse movement
 
// -- animation gsap
gsap.to(blue_box_mesh.position, {duration: 2, delay: 2, x:-4})
gsap.to(blue_box_mesh.position, {duration: 2, delay: 1, x:2})
// -- animation
var frame = 1
let start_time = Date.now() // js clock
const main_clock = new THREE.Clock() // three clock
let pre_time_3 = 0
const tick = () => {
 
    if (frame%60 == 0){
        //console.log(frame)
        const cur_time = Date.now()
        const pass_time = cur_time - start_time
        //console.log(pass_time/1000)
        const pass_time_3 = main_clock.getElapsedTime()
        const delta_time_3 = pass_time_3 - pre_time_3
        pre_time_3 = pass_time_3
        //console.log('clock:'+pass_time_3)
 
        //console.log(mouse)
        //console.log('pixel: '+window.devicePixelRatio)
    }
 
    frame = frame + 1
    // update
    if (box_mesh.position.x<5){
        box_mesh.position.x+=0.01
    }
    box_mesh.rotation.y+=0.01
 
    // update green by mouse
    green_box_mesh.rotation.y = Math.PI*mouse.x*0.5
    green_box_mesh.rotation.x = Math.PI*mouse.y*-0.25
    // time
 
    orbit_ctrl.update()
 
    // light helper update
    base_spotLight_helper.update()
    // areaLightHelper more
    /*
    base_areaLight_helper.position.copy(base_areaLight.position)
    base_areaLight_helper.quaternion.copy(base_areaLight.quaternion)
    base_areaLight_helper.update()
    */
    /*
    // animation
    if (mixer!== null){
        mixer.update(delta_time_3)
    }
    */
    // update points
    for(const point of points){
        const screen_pos = point.position.clone()
        screen_pos.project(main_cam)
        const tx = screen_pos.x * output_size.width * 0.5
        const ty = screen_pos.y * output_size.height * 0.5 * -1
        point.element.style.transform = `translate(${tx}px, ${ty}px)`
 
    }
 
    // re-render
    main_render.render(scene, main_cam)
    window.requestAnimationFrame(tick) // loop for next frame
}
tick()
 
// distanceTo built-in
console.log("ObjToCam: "+box_mesh.position.distanceTo(main_cam.position))
  • devwiki/three_js.txt
  • Last modified: 2022/07/17 11:49
  • by ying