Quick Start
- link code: (ref: https://threejs.org/docs/index.html#manual/en/introduction/Installation )
<script type="module"> // Find the latest version by visiting https://cdn.skypack.dev/three. import * as THREE from 'https://cdn.skypack.dev/pin/three@v0.132.0-r3eaNU1utrmzS211mhdG/mode=imports,min/optimized/three.js'; const scene = new THREE.Scene(); </script>
- resource:
- video:
- example
- image plane fly-through: https://www.oculus.com/medal-of-honor/
- 3d pin ball: http://letsplay.ouigo.com/
- world switch: https://zen.ly/
- environment: https://chartogne-taillet.com/en
- wave:
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
- webgl2 support browser tabel: https://caniuse.com/webgl2
- 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
- node.js (other downloads for zip version): https://nodejs.org/en/download/
- gui js options:
- dat.GUI
- control-panel
- ControlKit
- Guify
- Oui
-
- prepare tip:
- use 2×1 ratio panorama with horizontal tile-able texture as base layer
- photoshop > spherical panorama > new panorama layer from selected layer to create a sphere texture
- paint the seam on pole area, (since already fix horizontal seam in 2D)
- once done, spherical panorama > export the result as 2×1 texture, then use above panorama to cubemap tool
- texture
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))