three.js - Haunted House

  • 基础场景:some lights、no shadow、a Dat.GUI panel
  // 导入three.js
  import * as THREE from 'three'
  // 导入轨道控制器
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  // 导入gui
  import * as dat from 'dat.gui'

  /**
   * Scene
  **/
  const scene = new THREE.Scene()

  /**
   * Camera
  **/
  const camera = new THREE.PerspectiveCamera(
    75, // 视角
    window.innerWidth / window.innerHeight, // 视椎体长宽比
    0.1, // 近端面
    100 // 远端面
  )
  camera.position.set(4, 2, 5)

  /**
   * Renderer
  **/
  const renderer = new THREE.WebGLRenderer()
  renderer.setSize(window.innerWidth, window.innerHeight)
  document.body.appendChild(renderer.domElement)

  /**
    * 坐标轴
  **/
  const axesHelper = new THREE.AxesHelper(5) // 坐标轴线段长度
  scene.add(axesHelper)

  /**
    控制器(使相机围绕目标运动)
  **/
  const controls = new OrbitControls(camera, renderer.domElement)
  controls.enableDamping = true // 添加轨道阻尼效果

  /**
    * 渲染
  **/
  const clock = new THREE.Clock()
  function animate () {
    let elapsedTime = clock.getElapsedTime()

    controls.update()
    requestAnimationFrame(animate)
    renderer.render(scene, camera)
  }
  animate()
  /**
    * Lights
  **/
  // AmbientLight
  const ambientLight = new THREE.AmbientLight('#fff', 0.8)
  scene.add(ambientLight)
  /**
    * gui
  **/
  const gui = new dat.GUI()
  gui.add(ambientLight, 'intensity').name('AmbientLight').min(0).max(1).step(0.001)
  • House
    • Floor
    /**
      * House
    **/
    // Floor
    const floor = new THREE.Mesh(
      new THREE.PlaneGeometry(20, 20),
      new THREE.MeshStandardMaterial({
        color: '#a9c388',
    })
    floor.rotation.x = - Math.PI * 0.5
    floor.position.y = 0
    scene.add(floor)
    
    • create a House group 以便于需要整体调整House的位置、大小等
    /**
      * House
    **/
    // Group
    const house = new THREE.Group()
    scene.add(house)
    
    ...
    
    /**
      * floor
    **/
    ...
    
    • create the Walls
    /**
      * House
    **/
    // Group
    const house = new THREE.Group()
    scene.add(house)
    
    // Walls
    const walls = new THREE.Mesh(
      new THREE.BoxGeometry(4, 2.5, 4),
      new THREE.MeshStandardMaterial({
          color: '#ac8e82'
      })
    )
    walls.position.y = 2.5 / 2   // 立方体初始有一半的部分是在坐标轴以下的,也就是在floor的下面
    house.add(walls) // 后续house的部分都添加在house上面
    
    ...
    
    // foor
    ...
    
    • create the Roof, with a pyramid 棱锥体
    // Roof
    const roof = new THREE.Mesh(
      new THREE.ConeGeometry(3.5, 1, 4), 
      new THREE.MeshStandardMaterial({
        color: '#b35f45'
      })
    )
    roof.rotation.y = Math.PI * 0.25
    roof.position.y = 2.5 + 0.5 // walls的高 + 自身高的一半
    house.add(roof)
    
    • creat the Door with a plane
    // Door
    const door = new THREE.Mesh(
      new THREE.PlaneGeometry(2, 2),
      new THREE.MeshStandardMaterial({
        color: '#aa7b7b', 
      })
    )
    door.position.y = 1
    door.position.z = 2 + 0.01 // walls的深度 + 0.01,+0.01避免处在同一层级
    house.add(door)
    
    • add Bushes and use the same geometry and the same material for every bushes
    // Bushes
    const bushGeometry = new THREE.SphereGeometry(1, 16, 16)
    const bushMaterial = new THREE.MeshStandardMaterial({
      color: '#89c854'
    })
    
    const bush1 = new THREE.Mesh(bushGeometry, bushMaterial)
    bush1.scale.set(0.5, 0.5, 0.5)
    bush1.position.set(0.8, 0.2, 2.2)
    
    const bush2 = new THREE.Mesh(bushGeometry, bushMaterial)
    bush2.scale.set(0.25, 0.25, 0.25)
    bush2.position.set(1.4, 0.1, 2.1)
    
    const bush3 = new THREE.Mesh(bushGeometry, bushMaterial)
    bush3.scale.set(0.4, 0.4, 0.4)
    bush3.position.set(-0.8, 0.1, 2.2)
    
    const bush4 = new THREE.Mesh(bushGeometry, bushMaterial)
    bush4.scale.set(0.15, 0.15, 0.15)
    bush4.position.set(-1, 0.05, 2.6)  
    
    house.add(bush1, bush2, bush3, bush4)
    
    • Graves
      1. instead of placing each grave manually, we are going to create and place them procedurally
      2. grave可出现的范围需要被限制在floor的范围内,同时围绕在house的外部出现(环形)
    /**
      * Graves
    **/
    const graves = new THREE.Group()
    scene.add(graves)
    
    const graveGeometry = new THREE.BoxGeometry(0.6, 0.8, 0.2)
    const graveMaterial = new THREE.MeshStandardMaterial({
      color: '#b2b6b1'
    })
    
    for(let i = 0; i < 50; i++) {
      const angle = Math.random() * Math.PI * 2 // 360°
      const radius = 3 + Math.random() * 6 // 随机半径, 3-9
      // 分别设置x z轴的坐标,使grave围绕house画一个圈
      const x = Math.sin(angle) * radius
      const z = Math.cos(angle) * radius
    
      const grave = new THREE.Mesh(graveGeometry, graveMaterial)
      grave.position.set(x, 0.3, z) // y值小于高度的一半,避免z轴出现旋转后底部悬空
      // 旋转随机参数减去0.5,范围-0.5至0.5,确保旋转方向不一致
      grave.rotation.y = (Math.random() - 0.5) * 0.6
      grave.rotation.z = (Math.random() - 0.5) * 0.6
      grave.rotation.x = (Math.random() - 0.5) * 0.6
      graves.add(grave)
    }
    
    • Lights
      1. dim the ambient and moon lights
      2. give those a more blue-ish color 青调
      3. add a warm PointLight above the door and add it to the house
    /*
      * Lights
    */
    // AmbientLight
    const ambientLight = new THREE.AmbientLight('#b9d5ff', 0.12)
    scene.add(ambientLight)
    
    // DirectionalLight
    const moonLight = new THREE.DirectionalLight('#b9d5ff', 0.12)
    moonLight.position.set(4, 5, -2)
    scene.add(moonLight)
    
    // Door Light
    const doorLight = new THREE.PointLight('#ff7d46', 1, 7)
    doorLight.position.set(0, 2.2, 2.7)
    house.add(doorLight)
    
  • 完成House部分后,我们应该会得到如下图所示
    基础结构.png
  • Fog
    • 当前我们看到的floor的边缘部分过于清晰
    • 使用three.js本身就支持的 Fog
    • new THREE.Fog(color, near, far)nearfar分别指的是开始应用雾的最小距离和最大距离,这里的“距离”是指距离camera的远近
  /**
    * Fog
  **/
  const fog = new THREE.Fog('#262837', 1, 15) // color 距离camera的near 距离camera的far
  scene.fog = fog
  • Background
    • 继续模糊边缘,使floor和背景融为一体
    • to fix the background, use the same color as Fog
renderer.setClearColor('#262837')
添加FOG和重置背景色之后的效果.png
  • Textures
    /**
      * Textures
    **/
    const textureLoader = new THREE.TextureLoader()
    
    • door
    const doorColorTexture = textureLoader.load('/imgs/haunted-house/door/color.jpg')
    const doorAlphaTexture = textureLoader.load('/imgs/haunted-house/door/alpha.jpg')
    const doorAmbientOcclusionTexture = textureLoader.load('/imgs/haunted-house/door/ambientOcclusion.jpg')
    const doorHeightTexture = textureLoader.load('/imgs/haunted-house/door/height.jpg')
    const doorNormalTexture = textureLoader.load('/imgs/haunted-house/door/normal.jpg')
    const doorMetalnessTexture = textureLoader.load('/imgs/haunted-house/door/metalness.jpg')
    const doorRoughnessTexture = textureLoader.load('/imgs/haunted-house/door/roughness.jpg')
    
    // Door
    const door = new THREE.Mesh(
      new THREE.PlaneGeometry(2.2, 2.2, 100, 100), // displacementMap会移动顶点实现立体感
      new THREE.MeshStandardMaterial({
        // color: '#aa7b7b', // 添加texture之前作为替代
    
        map: doorColorTexture, // 将简单的纹理作为颜色
    
        transparent: true,
        alphaMap: doorAlphaTexture, // 需要与transparent同时使用
    
        aoMap: doorAmbientOcclusionTexture, // 需要提供uv2坐标支持
    
        displacementMap: doorHeightTexture, // 使door更加立体,不仅仅是一个平面
        displacementScale: 0.1, // 减小移动顶点的高度效应
    
        normalMap: doorNormalTexture, // 法线贴图
    
        metalnessMap: doorMetalnessTexture,
    
        roughnessMap: doorRoughnessTexture
      })
    )
    // support aoMap
    // door.geometry.attributes.uv   自动创建的uv坐标,2个值组成一个坐标值
    door.geometry.setAttribute(
      'uv2', 
      new THREE.Float32BufferAttribute(door.geometry.attributes.uv.array, 2)
    )
    door.position.y = 1
    door.position.z = 2 + 0.01 // walls的深度 + 0.01,+0.01避免处在同一层级
    house.add(door)
    
    • walls
    const bricksColorTexture = textureLoader.load('/imgs/haunted-house/bricks/color.jpg')
    const bricksAmbientTexture = textureLoader.load('/imgs/haunted-house/bricks/ambientOcclusion.jpg')
    const bricksNormalTexture = textureLoader.load('/imgs/haunted-house/bricks/normal.jpg')
    const bricksRoughnessTexture = textureLoader.load('/imgs/haunted-house/bricks/roughness.jpg')
    
    // Walls
    const walls = new THREE.Mesh(
      new THREE.BoxGeometry(4, 2.5, 4),
      new THREE.MeshStandardMaterial({
        map: bricksColorTexture,
    
        aoMap: bricksAmbientTexture, // uv2
    
        normalMap: bricksNormalTexture,
    
        roughnessMap: bricksRoughnessTexture
      })
    )
    // support aoMap
    walls.geometry.setAttribute(
      'uv2', 
      new THREE.Float32BufferAttribute(walls.geometry.attributes.uv.array, 2)
    )
    walls.position.y = 2.5 / 2
    house.add(walls)
    
    • floor
    const grassColorTexture = textureLoader.load('/imgs/haunted-house/grass/color.jpg')
    const grassAmbientTexture = textureLoader.load('/imgs/haunted-house/grass/ambientOcclusion.jpg')
    const grassNormalTexture = textureLoader.load('/imgs/haunted-house/grass/normal.jpg')
    const grassRoughnessTexture = textureLoader.load('/imgs/haunted-house/grass/roughness.jpg')
    
    // 仅添加以上纹理后会发现,grass的大小和house不匹配,继续做如下优化
    
    // repeat
    grassColorTexture.repeat.set(8, 8)
    grassAmbientTexture.repeat.set(8, 8)
    grassNormalTexture.repeat.set(8, 8)
    grassRoughnessTexture.repeat.set(8, 8)
    
    // 避免repeat时拉伸最后一个像素,change the wrapS and wrapT properties
    grassColorTexture.wrapS = THREE.RepeatWrapping
    grassAmbientTexture.wrapS = THREE.RepeatWrapping
    grassNormalTexture.wrapS = THREE.RepeatWrapping
    grassRoughnessTexture.wrapS = THREE.RepeatWrapping
    
    grassColorTexture.wrapT = THREE.RepeatWrapping
    grassAmbientTexture.wrapT = THREE.RepeatWrapping
    grassNormalTexture.wrapT = THREE.RepeatWrapping
    grassRoughnessTexture.wrapT = THREE.RepeatWrapping
    
     /**
     * Floor
    **/
    const floor = new THREE.Mesh(
      new THREE.PlaneGeometry(20, 20),
      new THREE.MeshStandardMaterial({
        map: grassColorTexture,
    
        aoMap: grassAmbientTexture, // uv2
    
        normalMap: grassNormalTexture,
    
        roughnessMap: grassRoughnessTexture
      })
    )
    // support aoMap
    floor.geometry.setAttribute(
      'uv2', 
      new THREE.Float32BufferAttribute(floor.geometry.attributes.uv.array, 2)
    )
    floor.rotation.x = - Math.PI * 0.5
    floor.position.y = 0
    scene.add(floor)
    
添加textures之后的效果.png
  • Ghosts
    • we are going to represent them with simple lights floating around the house and passing through the ground and graves
    • animate these lights 让ghost以不同的频率围绕house旋转并上下起伏
    • 这里的动画思路可以参考上面的graves的部分
  /*
    * Ghosts
  */
  const ghost1 = new THREE.PointLight('#ff00ff', 2, 3)
  scene.add(ghost1)

  const ghost2 = new THREE.PointLight('#00ffff', 2, 3)
  scene.add(ghost2)

  const ghost3 = new THREE.PointLight('#ffff00', 2, 3)
  scene.add(ghost3)
  /*
    * 渲染
  */
  const clock = new THREE.Clock()
  function animate () {
    let elapsedTime = clock.getElapsedTime()

    // Ghosts
    const ghost1Angle = elapsedTime * 0.5
    ghost1.position.x = Math.cos(ghost1Angle) * 4
    ghost1.position.z = Math.sin(ghost1Angle) * 4
    ghost1.position.y = Math.sin(elapsedTime * 3) // 上下起伏
    
    const ghost2Angle = - elapsedTime * 0.32 // 设为负数,与ghost1反向动画
    ghost2.position.x = Math.cos(ghost2Angle) * 5
    ghost2.position.z = Math.sin(ghost2Angle) * 5
    ghost2.position.y = Math.sin(elapsedTime * 4) + Math.sin(elapsedTime * 2.5)

    const ghost3Angle = - elapsedTime * 0.18
    ghost3.position.x = Math.cos(ghost3Angle) * (7 + Math.sin(elapsedTime * 0.32)) // 不固定旋转半径
    ghost3.position.z = Math.sin(ghost3Angle) * (7 + Math.sin(elapsedTime * 0.5)) // 不固定旋转半径
    ghost3.position.y = Math.sin(elapsedTime * 4) + Math.sin(elapsedTime * 2.5)


    controls.update()
    requestAnimationFrame(animate)
    renderer.render(scene, camera)
  }
  animate()
添加ghosts之后会随机出现的PointLight.png
  • Shadows
    • 我们将阴影设置统一放在一起方便管理
    /**
     * Renderer
    **/
    ...
    ...
    
    // Shadows
    ... 以下部分为shadows
    
    
    • activate the shadow map on the renderer
    // 开启场景中的阴影贴图
    renderer.shadowMap.enabled = true
    
    • activate the shadows on the lights that should cast shadows
    moonLight.castShadow = true
    doorLight.castShadow = true
    ghost1.castShadow = true
    ghost2.castShadow = true
    ghost3.castShadow = true
    
    • go through each objects of your scene and decide if that object can cast and/or receive shadows
    walls.castShadow = true
    bush1.castShadow = true
    bush2.castShadow = true
    bush3.castShadow = true
    bush4.castShadow = true
    
    /**
      * Graves
    **/
    for(let i = 0; i < 50; i++) {
      ...
      ...
      grave.castShadow = true
      ...
    }
    
    floor.receiveShadow = true // receive shadow
    
    • optimize the shadow maps
    doorLight.shadow.mapSize.width = 256
    doorLight.shadow.mapSize.height = 256
    doorLight.shadow.camera.far = 7
    
    ghost1.shadow.mapSize.width = 256
    ghost1.shadow.mapSize.height = 256
    ghost1.shadow.camera.far = 7
    
    ghost2.shadow.mapSize.width = 256
    ghost2.shadow.mapSize.height = 256
    ghost2.shadow.camera.far = 7
    
    ghost3.shadow.mapSize.width = 256
    ghost3.shadow.mapSize.height = 256
    ghost3.shadow.camera.far = 7
    
    • 改变阴影映射的算法
    renderer.shadowMap.type = THREE.PCFSoftShadowMap
    
最终静态效果.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容