Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              <div id="js-app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.20.0/polyfill.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/86/three.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/three.bas.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/OrbitControls_copy.js"></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://rawgit.com/josephg/noisejs/master/perlin.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script>(function(){var script=document.createElement('script');script.onload=function(){var stats=new Stats();document.body.appendChild(stats.dom);requestAnimationFrame(function loop(){stats.update();requestAnimationFrame(loop)});};script.src='//rawgit.com/mrdoob/stats.js/master/build/stats.min.js';document.head.appendChild(script);})()</script>

            
          
!
            
              body {
  margin: 0;
  padding: 0;
  background-color: #000;
}

canvas {
  display: block;
  margin: 0 auto;
}
            
          
!
            
              setTimeout(async () => {
  const mapTexture = await loadTexture('https://s3-us-west-2.amazonaws.com/s.cdpn.io/292951/winter.jpg')
  const entities = [
    new Plane(mapTexture),
    new THREE.AxisHelper(50)
  ]
  ReactDOM.render((
    <Simulation
        fov={45}
        cameraPosition={new THREE.Vector3(40, 18, -82)}
        {...{ entities }}
    />
  ), document.getElementById('js-app'))
}, 0)

const loadTexture = (assetUri) => {
  return new Promise((resolve) => {
    const textureLoader = new THREE.TextureLoader()
    textureLoader.crossOrigin = ''
    textureLoader.load(assetUri, image => resolve(image))
  })
}

const buildPoints = (edgeCount, rotationOffset = 0) => {
  const stepSize = (Math.PI * 2) / edgeCount;
  return Array(...(new Array(edgeCount))).map((_, edgeIndex) => [
    Math.cos(edgeIndex * stepSize + rotationOffset),
    Math.sin(edgeIndex * stepSize + rotationOffset)
  ]);
}

class Plane extends THREE.Mesh {
  uTime = null

  constructor (mapTexture) {
    const pointsUp = buildPoints(3, Math.PI * 1.5)
    const uvs = [
      pointsUp[0].map(i => (i + 1) / 2).map(i => 1 - i),
      pointsUp[1].map(i => (i + 1) / 2).map(i => 1 - i),
      pointsUp[2].map(i => (i + 1) / 2).map(i => 1 - i)
    ]
    const geometry = Plane.createGeometry(uvs)
    const material = Plane.createMaterial(mapTexture, uvs)
    material.uniforms.uMap.value = mapTexture
    super(geometry, material)
  }

  get time () {
    return this.uTime
  }

  set time (newTime) {
    this.uTime = newTime
  }

  static elevationGenerator (scaling = 20, s = Math.random()) {
    const colorLookup = {}
    const seed = 5625463739 * s
    return (vertex) => {
      const key = `${vertex.x}&${vertex.y}&${vertex.z}`
      if (colorLookup[key] === undefined) {
        const elevation = (noise.simplex3(
          (vertex.x + seed) / scaling,
          (vertex.y + seed) / scaling,
          (vertex.z + seed) / scaling
        ) + 1) / 2 // this will be a value between 0 and 1
        let elevationIndex = 0
        if (elevation > 0.3) { // ocean
          elevationIndex = 1
        }
        if (elevation > 0.45) { // sand
          elevationIndex = 2
        }
        if (elevation > 0.62) { // grass
          elevationIndex = 3
        }
        if (elevation > 0.8) { // dense grass
          elevationIndex = 4
        }
        colorLookup[key] = elevationIndex
      }
      return colorLookup[key]
    }
  }

  static createGeometry (uvs) {
    const getElevation = Plane.elevationGenerator()
    const model = new THREE.IcosahedronGeometry(20, 4)
    THREE.BAS.Utils.separateFaces(model) // this splits each face into its own 3 vertices, allowing us to add elevations to them (i think)
    const barycentricCombinations = [
      [1, 0, 0],
      [0, 1, 0],
      [0, 0, 1]
    ]
    const geometry = new THREE.BAS.ModelBufferGeometry(model)
    const aUv = geometry.createAttribute('aUv', 2)
    const aElevation = geometry.createAttribute('aElevation', 4)
    let offsetUv = 0
    let offsetElevation = 0
    ;[...new Array(geometry.faceCount)].forEach((_, faceIndex) => {
      const face = geometry.modelGeometry.faces[faceIndex]
      const {
        a,
        b,
        c
      } = face
      const {
        vertices
      } = geometry.modelGeometry
      const [
        elevationA,
        elevationB,
        elevationC
      ] = [a, b, c].map((vertexIndex) => {
        const vertex = vertices[vertexIndex]
        return getElevation(vertex)
      })
      const elevationD = Plane.getFaceCenterColor(elevationA, elevationB, elevationC)
      for (let i = 0; i < 3; i++) {
        aUv.array[offsetUv++] = uvs[i][0]
        aUv.array[offsetUv++] = uvs[i][1]
        aElevation.array[offsetElevation++] = elevationA
        aElevation.array[offsetElevation++] = elevationB
        aElevation.array[offsetElevation++] = elevationC
        aElevation.array[offsetElevation++] = elevationD
      }
    })
    return geometry
  }

  static getFaceCenterColor (elevationA, elevationB, elevationC) { // TODO refactor, probably a better way to do this
    return (elevationA + elevationB + elevationC) / 3
  }

  static createMaterial (mapTexture, uvs) {
    return new THREE.BAS.PhongAnimationMaterial({
      shading: THREE.FlatShading,
      side: THREE.FrontSide,
      uniforms: {
        uTime: { value: 0 },
        uMap: { value: null },
        uUvA: { value: uvs[0] },
        uUvB: { value: uvs[1] },
        uUvC: { value: uvs[2] }
      },
      uniformValues: {
      },
      vertexParameters: [`
        // vertexParamters
        attribute vec2 aUv;
        attribute vec3 aBarycentric;
        attribute vec4 aElevation;
        uniform float uTime;
      `],
      varyingParameters: [`
        // varyingParameters
        varying vec2 vUv;
        varying vec4 vElevation;
      `],
      vertexFunctions: [`
        // vertexFunctions
      `],
      vertexInit: [`
        // vertexInit
      `],
      vertexNormal: [`
        // vertexNormal
      `],
      vertexPosition: [`
        // vertexPosition
      `],
      vertexColor: [`
        // vertexColor
        vUv = aUv;
        vElevation = aElevation;
      `],
      fragmentParameters: [`
        // fragmentParameters
        uniform vec2 uUvA;
        uniform vec2 uUvB;
        uniform vec2 uUvC;
      `],
      fragmentFunctions: [`
        // fragmentFunctions
        float terrainShininess (float elev) {
          float landShininess = 10.0;
          float waterShininess = 100.0;
          if (elev >= 4.0) {
            return landShininess; // dense grass
          }
          if (elev >= 3.0) {
            return landShininess; // grass
          }
          if (elev >= 2.0) {
            return landShininess; // sand
          }
          if (elev >= 1.0) {
            return waterShininess; // ocean
          }
          return waterShininess; // deep ocean
        }

        vec3 terrainColor (float elev) {
          if (elev >= 4.0) {
            return vec3(0, 0.533, 0); // dense grass 008800 (0, 136, 0)
          }
          if (elev >= 3.0) {
            return vec3(0, 0.619, 0); // grass 009e00 (0, 158, 0)
          }
          if (elev >= 2.0) {
            return vec3(0.76, 0.698, 0.435); // sand c2b26f (194, 178, 111)
          }
          if (elev >= 1.0) {
            return vec3(0.188, 0.619, 0.752); // ocean 309ec0 (48, 158, 192)
          }
          return vec3(0.16, 0.533, 0.682); // deep ocean 2988ae (41, 136, 174)
        }

        vec3 cartesianToBarycentric (vec2 p1, vec2 p2, vec2 p3, vec2 p) { // http://totologic.blogspot.com/2014/01/accurate-point-in-triangle-test.html
          float denominator = ((p2.y - p3.y) * (p.x - p3.x) + (p3.x - p2.x) * (p1.y - p3.y));
          float a = ((p2.y - p3.y) * (p.x - p3.x) + (p3.x - p2.x) * (p.y - p3.y)) / denominator;
          float b = ((p3.y - p1.y) * (p.x - p3.x) + (p1.x - p3.x) * (p.y - p3.y)) / denominator;
          float c = 1.0 - a - b;
          return vec3(a, b, c);
        }

        float getPointElevation (vec4 elevation, vec3 barycentric) {
          if (barycentric.x > barycentric.y + barycentric.z) {
            return elevation.x;
          } else if (barycentric.y > barycentric.x + barycentric.z) {
            return elevation.y;
          } else if (barycentric.z > barycentric.x + barycentric.y) {
            return elevation.z;
          }
          return elevation.w;
        }

        vec3 perturbNormalArb (vec3 surf_pos, vec3 surf_norm, vec2 dHdxy) {
          vec3 vSigmaX = vec3(dFdx(surf_pos.x), dFdx(surf_pos.y), dFdx(surf_pos.z));
          vec3 vSigmaY = vec3(dFdy(surf_pos.x), dFdy(surf_pos.y), dFdy(surf_pos.z));
          vec3 vN = surf_norm;
          vec3 R1 = cross(vSigmaY, vN);
          vec3 R2 = cross(vN, vSigmaX);
          float fDet = dot(vSigmaX, R1);
          vec3 vGrad = sign(fDet) * (dHdxy.x * R1 + dHdxy.y * R2);
          return normalize(abs(fDet) * surf_norm - vGrad);
        }

        vec2 dHdxy_fwd (vec2 uvA, vec2 uvB, vec2 uvC, vec2 uv, vec4 elevation, float bumpScale) {
          vec2 dSTdx = dFdx(uv);
          vec2 dSTdy = dFdy(uv);
          float Hll = bumpScale * getPointElevation(elevation, cartesianToBarycentric(uvA, uvB, uvC, uv));
          float dBx = bumpScale * getPointElevation(elevation, cartesianToBarycentric(uvA, uvB, uvC, uv + dSTdx)) - Hll;
          float dBy = bumpScale * getPointElevation(elevation, cartesianToBarycentric(uvA, uvB, uvC, uv + dSTdy)) - Hll;
          return vec2(dBx, dBy);
        }
      `],
      fragmentInit: [`
        // fragmentInit
        vec4 elevation = ceil(vElevation + 0.5) - 1.0;
      `],
      fragmentDiffuse: [`
        // fragmentDiffuse
        vec3 barycentric = cartesianToBarycentric(uUvA, uUvB, uUvC, vUv);
        float pointElevation = getPointElevation(elevation, barycentric);
        diffuseColor.rgb = terrainColor(pointElevation);
      `],
      fragmentMap: [`
        // fragmentMap
      `],
      fragmentEmissive: [`
        // fragmentEmissive
        if (pointElevation > 1.0) {
          normal = perturbNormalArb(-vViewPosition, normal, dHdxy_fwd(uUvA, uUvB, uUvC, vUv, elevation, 0.5));
        }
      `],
      fragmentSpecular: [`
        // fragmentSpecular
        material.specularShininess = terrainShininess(pointElevation);
      `]
    })
  }
}

class Simulation extends React.Component {
  static defaultProps = {
    entities: [],
    fov: 80,
    cameraPosition: new THREE.Vector3(0, 0, 0)
  }

  shouldComponentUpdate () {
    return false
  }

  componentDidMount () {
    const camera = this.createCamera(
      this.props.fov,
      this.props.cameraPosition,
      window.innerWidth,
      window.innerHeight
    )
    camera.target = new THREE.Vector3(0, 0, 0)
    camera.lookAt(camera.target)
    const scene = new THREE.Scene()
    const renderer = new THREE.WebGLRenderer({
      antialias: false,
      alpha: false,
      canvas: this.canvas
    })
    const handleWindowResize = this.onWindowResize(camera, renderer)
    handleWindowResize()
    window.addEventListener('resize', handleWindowResize, false)
    const controls = this.createControls(camera)
    this.props.entities.forEach(e => scene.add(e))
    const {
      lights,
      pointLights
    } = this.createLights()
    lights.forEach(light => scene.add(light))
    this.animate(renderer, scene, camera, controls, this.props.entities, pointLights, +(new Date()))
  }

  createLights () {
    const ambientLight = new THREE.AmbientLight(0x333333)
    const pointLightA = new THREE.PointLight(0xffffff, 1, 200)
    const pointLightHelperA = new THREE.PointLightHelper(pointLightA, 1)
    const pointLightB = new THREE.PointLight(0xffffff, 1, 200)
    const pointLightHelperB = new THREE.PointLightHelper(pointLightB, 1)
    return {
      lights: [
        ambientLight,
        pointLightA,
        pointLightHelperA,
        // pointLightB,
        // pointLightHelperB
      ],
      pointLights: [
        pointLightA,
        // pointLightB
      ]
    }
  }

  createCamera (fov, pos, width, height) {
    const camera = new THREE.PerspectiveCamera(fov, width / height, 1, 1000)
    camera.position.x = pos.x
    camera.position.y = pos.y
    camera.position.z = pos.z
    return camera
  }

  createControls (camera) {
    const controls = new THREE.OrbitControls(camera)
    controls.target = camera.target
    controls.enableDamping = true
    controls.dampingFactor = 0.1
    controls.rotateSpeed = 0.1
    return controls
  }

  onWindowResize (camera, renderer) {
    return () => {
      const width = window.innerWidth
      const height = window.innerHeight
      camera.aspect = width / height
      camera.updateProjectionMatrix()
      renderer.setSize(width, height)
    }
  }

  animate (renderer, scene, camera, controls, entities, pointLights, lastTime) {
    const currentTime = +(new Date())
    const timeDelta = currentTime - lastTime
    entities.forEach(e => e.time += timeDelta / 1000)
    requestAnimationFrame(() => {
      this.animate(renderer, scene, camera, controls, entities, pointLights, currentTime)
    })
    pointLights.forEach((pointLight, index) => {
      index += 1
      const frequency = 1000 + index * 200
      const amplitude = 40
      const animationTime = Math.PI * frequency + currentTime * index;
      const cos = Math.cos(animationTime / frequency) * amplitude
      const sin = Math.sin(animationTime / frequency * 1) * amplitude / 1 // turn the 1s to 2s
      pointLight.position.set(
        sin,
        20,
        cos
      )
    })
    controls.update()
    renderer.render(scene, camera)
  }

  render () {
    return (
      <canvas ref={c => this.canvas = c} />
    )
  }
}
            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console