"use strict"; // 參考 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode //////////////////////////////////////////////////////////////////////////////// // 模型變形: // 你要修改 Vertex Shader 程式,在 vertexShader 變數。 //////////////////////////////////////////////////////////////////////////////// var camera, scene, renderer; var cameraControls; var effectController; var clock = new THREE.Clock(); var tess = 7, newTess = tess; var ambientLight, light; var teapot; var phongBalancedMaterial; var vertexShader = ` varying vec3 vNormal; varying vec3 vViewPosition; uniform float uSphereRadius2; // 經過平方 void main() { // 學生作業:修改位置,你要先複製到暫存向量,然後修改暫存向量 // 不要忘記接下來的程式都要用暫存向量 gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); vNormal = normalize( normalMatrix * normal ); vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 ); vViewPosition = -mvPosition.xyz; } ` var fragmentShader = ` uniform vec3 uMaterialColor; uniform vec3 uSpecularColor; uniform vec3 uDirLightPos; uniform vec3 uDirLightColor; uniform vec3 uAmbientLightColor; uniform float uKd; uniform float uKs; uniform float shininess; varying vec3 vNormal; varying vec3 vViewPosition; void main() { // 環境光 gl_FragColor = vec4( uAmbientLightColor, 1.0 ); // 計算往光源的方向 vec4 lDirection = viewMatrix * vec4( uDirLightPos, 0.0 ); vec3 lVector = normalize( lDirection.xyz ); vec3 normal = normalize( vNormal ); // 漫射:N * L。法向量必須正規化,因為經過內插計算 float diffuse = max( dot( normal, lVector ), 0.0); // 這樣可能會切斷高光,但是比奇怪的閃爍好 if (diffuse <= 0.0) { return; } gl_FragColor.rgb += uKd * uMaterialColor * uDirLightColor * diffuse; // 鏡面反射:N * H 的指數。H 是往光的向量加上往攝影機的向量正規化 vec3 viewPosition = normalize( vViewPosition ); vec3 pointHalfVector = normalize( lVector + viewPosition ); float pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 ); float specular = uKs * pow( pointDotNormalHalf, shininess ); specular *= diffuse*(2.0 + shininess)/8.0; gl_FragColor.rgb += uDirLightColor * uSpecularColor * specular; } ` function init() { canvasWidth = window.innerWidth; canvasHeight = window.innerHeight; var canvasRatio = canvasWidth/canvasHeight; // 光源 ambientLight = new THREE.AmbientLight(0x333333); // 0.2 light = new THREE.DirectionalLight(0xffffff, 1.0); // 方向由 UI 操控 // RENDERER renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(canvasWidth, canvasHeight); renderer.setClearColorHex(0xAAAAAA, 1.0); renderer.gammaInput = true; renderer.gammaOutput = true; // 攝影機 camera = new THREE.PerspectiveCamera(45, canvasRatio, 0.001, 80); camera.position.set(-1.41, 0.14, -0.56); // 控制 cameraControls = new THREE.OrbitAndPanControls(camera, renderer.domElement); cameraControls.target.set(0, -0.45, 0); // 材質 // 筆記:將「逐像素」關閉不會影響鏡面高光 // 只會影響「往光的方向」會不會逐像素計算 var materialColor = new THREE.Color(); materialColor.setRGB(1.0, 0.8, 0.6); phongBalancedMaterial = createShaderMaterial("phongBalanced", light, ambientLight); phongBalancedMaterial.uniforms.uMaterialColor.value.copy(materialColor); phongBalancedMaterial.side = THREE.DoubleSide; } function createShaderMaterial(id, light, ambientLight) { var shaderTypes = { 'phongBalanced' : { uniforms: { "uDirLightPos": { type: "v3", value: new THREE.Vector3() }, "uDirLightColor": { type: "c", value: new THREE.Color( 0xffffff ) }, "uAmbientLightColor": { type: "c", value: new THREE.Color( 0x050505 ) }, "uMaterialColor": { type: "c", value: new THREE.Color( 0xffffff ) }, "uSpecularColor": { type: "c", value: new THREE.Color( 0xffffff ) }, uKd: { type: "f", value: 0.7 }, uKs: { type: "f", value: 0.3 }, shininess: { type: "f", value: 100.0 }, uSphereRadius2: { type: "f", value: 1.0 } } } }; var shader = shaderTypes[id]; var u = THREE.UniformsUtils.clone(shader.uniforms); var vs = vertexShader; var fs = fragmentShader; var material = new THREE.ShaderMaterial({ uniforms: u, vertexShader: vs, fragmentShader: fs }); material.uniforms.uDirLightPos.value = light.position; material.uniforms.uDirLightColor.value = light.color; material.uniforms.uAmbientLightColor.value = ambientLight.color; return material; } function setupGui() { effectController = { sphereRadius: 1.0, shininess: 20.0, ka: 0.2, kd: 0.4, ks: 0.35, metallic: false, hue: 0.09, saturation: 0.46, lightness: 0.7, lhue: 0.04, lsaturation: 0.0, llightness: 0.7, // 奇怪的是如果你以負數初始化,這些滑桿不會顯示小數位 lx: 0.01, ly: 1, lz: 0.01 }; var h; var gui = new dat.GUI(); // 材質(屬性) h = gui.addFolder("材質控制"); h.add(effectController, "sphereRadius", 0.7072, 10.0, 1.0).name("球半徑"); h.add(effectController, "shininess", 1.0, 100.0, 1.0).name("閃亮程度"); h.add(effectController, "ka", 0.0, 1.0, 0.025).name("環境光係數"); h.add(effectController, "kd", 0.0, 1.0, 0.025).name("漫射係數"); h.add(effectController, "ks", 0.0, 1.0, 0.025).name("閃亮程度係數"); h.add(effectController, "metallic").name("金屬色澤"); // TODO: 加 tess 給鑲嵌 // 材質(顏色) h = gui.addFolder("材質顏色"); h.add(effectController, "hue", 0.0, 1.0, 0.025).name("色相"); h.add(effectController, "saturation", 0.0, 1.0, 0.025).name("飽和度"); h.add(effectController, "lightness", 0.0, 1.0, 0.025).name("明度"); // 光源(顏色) h = gui.addFolder("光源顏色"); h.add(effectController, "lhue", 0.0, 1.0, 0.025).name("色相"); h.add(effectController, "lsaturation", 0.0, 1.0, 0.025).name("飽和度"); h.add(effectController, "llightness", 0.0, 1.0, 0.025).name("明度"); // 光源(方向) h = gui.addFolder("光源方向"); h.add(effectController, "lx", -1.0, 1.0, 0.025).name("X"); h.add(effectController, "ly", -1.0, 1.0, 0.025).name("Y"); h.add(effectController, "lz", -1.0, 1.0, 0.025).name("Z"); } function animate() { requestAnimationFrame(animate); render(); } function render() { var delta = clock.getDelta(); cameraControls.update(delta); if (newTess !== tess ) { tess = newTess; fillScene(); } phongBalancedMaterial.uniforms.uSphereRadius2.value = effectController.sphereRadius*effectController.sphereRadius; phongBalancedMaterial.uniforms.shininess.value = effectController.shininess; phongBalancedMaterial.uniforms.uKd.value = effectController.kd; phongBalancedMaterial.uniforms.uKs.value = effectController.ks; var materialColor = new THREE.Color(); materialColor.setHSL(effectController.hue, effectController.saturation, effectController.lightness); phongBalancedMaterial.uniforms.uMaterialColor.value.copy(materialColor); if (!effectController.metallic) { materialColor.setRGB(1, 1, 1); } phongBalancedMaterial.uniforms.uSpecularColor.value.copy(materialColor); // 環境光只是材質的顏色乘上 ka,無關光源的顏色 ambientLight.color.setHSL(effectController.hue, effectController.saturation, effectController.lightness * effectController.ka); light.position.set(effectController.lx, effectController.ly, effectController.lz); light.color.setHSL(effectController.lhue, effectController.lsaturation, effectController.llightness); renderer.render(scene, camera); } function fillScene() { scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x808080, 2000, 4000); scene.add(camera); scene.add(ambientLight); scene.add(light); var solidGround = new THREE.Mesh( new THREE.PlaneGeometry( 1, 1, 100,100 ), phongBalancedMaterial ); solidGround.rotation.x = - Math.PI / 2; scene.add( solidGround ); scene.add(teapot); } function addToDOM() { var container = document.getElementById('container'); var canvas = container.getElementsByTagName('canvas'); if (canvas.length>0) { container.removeChild(canvas[0]); } container.appendChild( renderer.domElement ); } document.addEventListener("DOMContentLoaded", function () { try { init(); fillScene(); setupGui(); addToDOM(); animate(); } catch(e) { var errorReport = "你的程式遇到不可復原的錯誤,無法繪製 Canvas。錯誤是:<br/><br/>"; $('#container').append(errorReport+e); } })