ThreeJS - Snippets

Aus Wikizone
Wechseln zu: Navigation, Suche

Links

ThreeJS
Three.js - advanced snippets

Helfer

Axes Helper

Koordinatenachsen anzeigen

const axesHelper = new THREE.AxesHelper( 5 );
scene.add( axesHelper );

Viewport Settings

Handle Viewport Resizing

window.addEventListener('resize', () =>{
  console.log('window resized')
  // Update sizes
  sizes.width = window.innerWidth
  sizes.height = window.innerHeight
  // Update camera
  camera.aspect = sizes.width/sizes.height
  camera.updateProjectionMatrix()
  // Update renderer
  renderer.setSize(sizes.width,sizes.height)
  renderer.setPixelRatio( Math.min(window.devicePixelRatio, 2) ) // in case monitor changed in double monitor settings
})

Handle Pixel Ratio Setting (Retina Displays)

Retina Displays haben eine Pixel Ratio von 2. D.h. das Display kann einen "Software"Bildpixel nochmal auf 4 physische Pixel verteilen und damit vor allem Vektoren nochmal schärfer darstellen. ThreeJS kann diese zusätzlichen Pixel ebenfalls nutzen wenn man dem renderer die Pixel Ratio mitgibt. Allerdings muss der Renderer auch mehr tun.

Moderne Handys haben Ratios bis zu 5, das ist allerdings sinnlos mehr als 2 oder 3 sehen wir bei normalem Betrachtungsabstand eh nicht. Deshalb setzen wir wenn möglich einen Ratio so hoch wie das Gerät kann aber nicht höher als 2 um die Performance zu erhalten.

renderer.setPixelRatio( Math.min(window.devicePixelRatio, 2) )

Handle Fullscreen Mode

// Handle Fullscreen 
// including safari (needs webkit prefix)
window.addEventListener('dblclick', () =>
{
    const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement

    if(!fullscreenElement)
    {
        if(canvas.requestFullscreen)
        {
            canvas.requestFullscreen()
        }
        else if(canvas.webkitRequestFullscreen)
        {
            canvas.webkitRequestFullscreen()
        }
    }
    else
    {
        if(document.exitFullscreen)
        {
            document.exitFullscreen()
        }
        else if(document.webkitExitFullscreen)
        {
            document.webkitExitFullscreen()
        }
    }
})

Animation Basics

Three.js - Animation

Timebased Tick / Loop Function

ThreeJS Clock Object

const clock = new THREE.Clock()
const tick = () =>
{
    // Hint: do NOT use clock.getDelta() - it can cause problems (buggy in end of 2021)
    const elapsedTime = clock.getElapsedTime()
    //console.log(elapsedTime)
    mesh.rotation.y = elapsedTime * Math.PI * 2 // one revolution / s
    camera.lookAt(mesh.position)
    camera.position.z = Math.sin(elapsedTime) // back and forth
    // Render
    renderer.render(scene, camera)
    window.requestAnimationFrame(tick) 
}
tick()

GSAP Animation

// GSAP has it's own requestAnimationFrame, thus no time calculation needed
// we just let gsap update our values and tick does render each frame
gsap.to(mesh.position,{ duration: 1, delay: 1, x: 2 })
gsap.to(mesh.position,{ duration: 1, delay: 1, x: 0 })
const tick = () =>
{
    // Render on each frame
    renderer.render(scene, camera)
    window.requestAnimationFrame(tick) 
}
// GO...
tick()

Nützliche Snippets für Animationen

Kreisbewegung / Circular Movement

myObject.position.y = Math.sin(elapsedTime) //(-1 -> 1 -> -1 -> ...)
myObject.position.x = Math.cos(elapsedTime)

Cursor auswerten

// Sizes
const sizes = { width: 800,  height: 600}
// Cursor
const cursor = {
    x: 0,
    y: 0
}
window.addEventListener('mousemove', (event) => 
{
    //cursor.x = event.clientX / sizes.width // 0 <= x <= 1
    cursor.x = event.clientX / sizes.width - 0.5// -0.5 <= x <= +0.5
    cursor.y = event.clientY / sizes.height - 0.5// -0.5 <= x <= +0.5
    console.log('x: ' + cursor.x)
    console.log('y: ' + cursor.y)
})
// ...
// Update camera with position
    camera.position.x = cursor.x * 10
    camera.position.y = cursor.y * 10

Kamera auf einer Kreisbahn

Das obige Beispiel läßt sich ausbauen. Die Mausbewegung gibt uns nun cursor.x Werte von -0.5 bis 0.5. Wenn wir auf zwei Achsen sinus und cosinus kombinieren bekommen wir eine Kreisbahn um den Mittelpunkt auf der Ebene dieser beiden Achsen. Eine volle Umdrehung bekommen wir wenn wir mit 2xPi multiplizieren. Den Abstand vergrößern wir wenn wir das Ergebnis mit irgendeinem Faktor multiplizieren.

    // Update camera
    camera.position.x = Math.sin(cursor.x * 2 * Math.PI) * 3
    camera.position.z = Math.cos(cursor.x * 2 * Math.PI) * 3
    camera.position.y = cursor.y * 5 // damit wir auch etwas von oben oder unten schauen können
    camera.lookAt(mesh.position) // look at center

Für eine Kreisbahn um ein Objekt das nicht im Mittelpunkt ist, müßten wir noch die Koordinaten des Objekts auslesen und zu den Kamerakoordinaten addieren. So könnten wir den kompletten Kreis verschieben.

Bouncing Sphere + Shadow

    // Update the sphere
    sphere.position.x = Math.cos(elapsedTime) * 1.5
    sphere.position.z = Math.sin(elapsedTime) * 1.5
    sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))

    // Update the shadow accordingly
    sphereShadow.position.x = sphere.position.x
    sphereShadow.position.z = sphere.position.z
    sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3

Orbit Controls

https://threejs.org/docs/index.html?q=controls#examples/en/controls/OrbitControls

ThreeJS spart mit eigenen Control Klassen eine Menge Arbeit. OrbitControls müssen zusätzlich geladen werden. Also in HTML

<script src="/javascripts/OrbitControls.js"></script>

Oder z.B. in Webpack:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

Dann erstellt man einfach ein OrbitControl Objekt und übergibt die Kamera und ein DOM Objekt (i.d.R. das Canvas).

const controls = new OrbitControls(camera,canvas)

Geometry Snippets

Create Geometry / Geometry Objekt erzeugen

Beispiel: viele zufällige Dreiecke erzeugen

// Object
// const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)
const geometry = new THREE.BufferGeometry()
const count = 50
const positionsArray = new Float32Array(count * 3 * 3)
for (let i = 0; i < positionsArray.length; i++) {
    positionsArray[i] = Math.random() - 0.5 //-0.5 < x < 0.5
}
const positionsAttribute = new THREE.BufferAttribute(positionsArray,3) // use vals 3 by 3
geometry.setAttribute('position',positionsAttribute) // position is the attribute name in shaders

// Example Array
// const positionsArray = new Float32Array([
//     0,0,0,
//     0,1,0,
//     1,0,0
// ])

Debugging

lil-gui

// https://lil-gui.georgealways.com/#
import GUI from 'lil-gui'; 
/**
 * Debug
 */
const gui = new GUI({
    width:400
})
gui.close()
const params = {
    color: 0xff0000,
    spin: () =>
    {
        console.log('spin')
        let tl = new gsap.timeline
        tl.to(mesh.rotation,{y: mesh.rotation.y + 0.5*Math.PI, duration: 1.5, ease: "circ"})
        //...
    }
}
//...
// Debug
//gui.add(mesh.position,'y',-2,2,0.1) // OR
gui.add(mesh.position,'y')
  .min(-2)
  .max(3)
  .step(0.1)
  .name('elevation') // chain version
gui.add(mesh,'visible')
gui.add(material,'wireframe')
// we can not use material.color as it's not an object
// thus we use a separately created object...
gui.addColor(params,'color')
// ... and update material when this param changed:
.onChange( ()=>
{
    material.color.set(params.color)
})
gui.add(params, 'spin')

Textures

https://threejs.org/docs/index.html?q=texture#api/en/constants/Textures
three.js - Textures
Manche Materialien können eine oder mehrere Texturen enthalten.

TextureLoader

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
//...
const material = new THREE.MeshBasicMaterial({ map: texture })

TextureLoader Callbacks Manchmal nützlich für Fehlersuche etc.

const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
    '/textures/door/color.jpg',
    () =>
    {
        console.log('load')
    },
    () =>
    {
        console.log('progress')
    },
    () =>
    {
        console.log('error')
    }
)

Loading Manager

Nützlich für Progress

const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () =>
{
    console.log('onStart')
}
loadingManager.onLoad = () =>
{
    console.log('onLoad')
}
loadingManager.onProgress = () =>
{
    console.log('onProgress')
}
loadingManager.onError = () =>
{
    console.log('onError')
}

const textureLoader = new THREE.TextureLoader(loadingManager)

const colorTexture = textureLoader.load('/textures/door/color.jpg')
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const heightTexture = textureLoader.load('/textures/door/height.jpg')
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')

UV Wrapping

Wrapping Mode

THREE.RepeatWrapping
THREE.ClampToEdgeWrapping // default
THREE.MirroredRepeatWrapping
const colorTexture = textureLoader.load('/textures/door/color.jpg')

// TEXTURE TRANSFORMATIONS
colorTexture.repeat.x = 2
colorTexture.repeat.y = 3

colorTexture.wrapS = THREE.RepeatWrapping
colorTexture.wrapT = THREE.RepeatWrapping
//colorTexture.wrapS = THREE.MirroredRepeatWrapping
//colorTexture.wrapT = THREE.MirroredRepeatWrapping

colorTexture.offset.x = 0.5
colorTexture.offset.y = 0.5

colorTexture.center.x = 0.5
colorTexture.center.y = 0.5
colorTexture.rotation = Math.PI * 0.25

Mipmapping

Minification

const colorTexture = textureLoader.load('/textures/checkerboard-1024x1024.png')
colorTexture.generateMipmaps = false // NearestFilter doesn't need Mipmap
colorTexture.minFilter = THREE.NearestFilter // oft bessere Schärfe aber auch mehr Moiré

Magnification

const colorTexture = textureLoader.load('/textures/minecraft.png')
colorTexture.magFilter = THREE.NearestFilter

Texture Sizing

256x256 / 512x1024 / 1024x1024 //Power of 2, often square

Normal-Maps png verwenden und evtl. NearestFilter

colorTexture.generateMipmaps = false // NearestFilter doesn't need that
colorTexture.minFilter = THREE.NearestFilter
colorTexture.magFilter = THREE.NearestFilter

mehrere Texturen in einem Bild (Bildkanäle) um Ladezeit zu verkürzen wenn notwendig.

Materials

three.js - materials

Besonders wichtig für realistische Ergebnisse ist das MeshStandardMaterial. Siehe unten.

MeshBasicMaterial

const material = new THREE.MeshBasicMaterial()
material.color.set(0xaabb00)
material.map = doorColorTexture
material.side = DoubleSide
material.wireframe = true
material.transparent = true
material.opacity = 0.5
material.alphaMap = doorAlphaTexture

Komplexere Materialien haben natürlich weitere Eigenschaften

MeshNormalMaterial

// normally to debug normals
const material = new THREE.MeshNormalMaterial()
material.flatShading = true

MeshMatcapMaterial

https://github.com/nidorx/matcaps - gute Quelle
// MATCAP - simulate lights / good for modelling
const textureLoader = new THREE.TextureLoader()
const matcapTexture = textureLoader.load('textures/matcaps/1.jpg')
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture

MeshDepthMaterial

Nahe Bereiche werden hell, ferne dunkel gerendert. Kann man z.B. für Camera Tests oder für Nebel nutzen

// DEPTH
const material = new THREE.MeshDepthMaterial()
material.transparent = true
material.opacity = 0.8

MeshLambertMaterial

  • Benötigt / Reagiert mit Licht
  • Performant
  • Ergibt manchmal Linien-Artefakte in der Geometrie
// LAMBERT (reacts to light)
const material = new THREE.MeshLambertMaterial()

MeshPhongMaterial

Ähnlich wie Lambert.

  • Keine Artefakte
  • Weniger Performant
  • Erlaubt auch Lichtreflexion nicht nur Schattierung
// PHONG
const material = new THREE.MeshPhongMaterial
material.color.set(0xddeeee) // Grundfarbe
material.shininess = 180 // Glanz
material.specular = new THREE.Color(0x0066ff) // Glanzlichtfarbe

MeshToonMaterial

Cartoonartigen Effekt. Graustufengradient für Anzahl der Schattierungen. Dann NearestFilter verwenden, damit der Standardfilter die Abstufungen beim Mipmapping nicht kaputt macht.

const gradientTexture = textureLoader.load('textures/gradients/3.jpg')
gradientTexture.generateMipmaps = false 
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter

// TOON
const material = new THREE.MeshToonMaterial()
material.gradientMap = gradientTexture

MeshStandardMaterial

Standard Physical Based Rendering Material

// TEXTURES
const textureLoader = new THREE.TextureLoader()
const doorColorTexture = textureLoader.load('textures/door/color.jpg')
const doorAlphaTexture = textureLoader.load('textures/door/alpha.jpg')
const doorAmbientOcclusionTexture = textureLoader.load('textures/door/ambientOcclusion.jpg')
const doorHeightTexture = textureLoader.load('textures/door/height.jpg')
const doorMetalnessTexture = textureLoader.load('textures/door/metalness.jpg')
const doorNormalTexture = textureLoader.load('textures/door/normal.jpg')
const doorRoughnessTexture = textureLoader.load('textures/door/roughness.jpg')

// STANDARD MATERIAL
const material = new THREE.MeshStandardMaterial()
material.side = DoubleSide
material.map = doorColorTexture

material.roughness = 1 // default
material.roughnessMap = doorRoughnessTexture

material.metalness = 0 // default
material.metalnessMap = doorMetalnessTexture

material.aoMap = doorAmbientOcclusionTexture
material.aoMapIntensity = 1.1

material.displacementMap = doorHeightTexture
material.displacementScale = 0.03

material.normalMap = doorNormalTexture
material.normalScale.set(0.5,0.5)

material.transparent = true // needed for alpha to work
material.alphaMap = doorAlphaTexture

// OBJECTS
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(1,1,100,100), // subdivisions needed for height map
    material
)
// copy uv coordinates to uv2 attribute needed by aomap
plane.geometry.setAttribute(
    'uv2', 
    new THREE.BufferAttribute(plane.geometry.attributes.uv.array,2)
)

MeshPhysicalMaterial

Wie MeshStandardMaterial bietet aber noch zusätzlich ein Coating. Also z.b. ein Material unter einer Lackschicht.

https://threejs.org/examples/#webgl_materials_physical_clearcoat
  • Zusätzlich Coating
  • Nicht so Performant wie Standard

PointsMaterial

You can use PointsMaterial with particles.

ShaderMaterial and RawShaderMaterial

ShaderMaterial and RawShaderMaterial can both be used to create your own materials.

Environment Map

The environment map is like an image of what's surrounding the scene. You can use it to add reflection or refraction to your objects. It can also be used as lighting information.

You can use it with many of the materials we saw.

// ENVIRONMENTAL MAP
const cubeTextureLoader = new THREE.CubeTextureLoader()
// load cube-images in the right order...
const environmentMapTexture = cubeTextureLoader.load([
    '/textures/environmentMaps/4/px.png',
    '/textures/environmentMaps/4/nx.png',
    '/textures/environmentMaps/4/py.png',
    '/textures/environmentMaps/4/ny.png',
    '/textures/environmentMaps/4/pz.png',
    '/textures/environmentMaps/4/nz.png',
])
const material = new THREE.MeshStandardMaterial()
material.envMap = environmentMapTexture
material.metalness = 0.7
material.roughness = 0.2

gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)

Cubemap erzeugen

Environment Maps gibt es in unterschiedlichen Formaten. Z.b. hier als HDRI:

https://polyhaven.com/

Es gibt Tools um diese in Cubemaps umzuwandeln:

https://matheowis.github.io/HDRI-to-CubeMap/

Hinweise

Eigenschaften kann man als Konstruktor Objekt übergeben oder direkt setzen oder über set (manchmal praktischer, wenn als Eigenschaftswert ein Objekt erwartet wird (z.B. bei der Farbe ein Farbobjekt)

3D Text

Three.js - 3D Text
http://gero3.github.io/facetype.js/ - Konvertieren von Fonts nach Facetype

Fontloader

Hinweis: Seit three.js 133 muss der Fontloader und die Fontgeometry importiert werden.

https://threejs.org/docs/index.html?q=fontloa#examples/en/loaders/FontLoader

Basic Example

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js'
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js'//i.e. with webpack
//...
/**
 * Fonts
 */
const fontLoader = new FontLoader()

fontLoader.load(
    //'/fonts/helvetiker_regular.typeface.json',
    '/fonts/BebasNeueBook_Regular.json',
    (font) =>
    {
        console.log('font loaded')
        const textGeometry = new TextGeometry(
            'KHOLJA',
            {
                font: font,
                size: 0.5,
                height: 0.2, // more like extrusion depth
                curveSegments: 8,
                bevelEnabled: true,
                bevelThickness: 0.03,
                bevelSize: 0.01,
                bevelOffset: 0,
                bevelSegments: 4
            }
        )
        textGeometry.center() // easy method to center the text
        const textMaterial = new THREE.MeshBasicMaterial()
        textMaterial.wireframe = true
        const text = new THREE.Mesh(textGeometry,textMaterial)
        scene.add(text)
    }
)

Lights

Three.js - Lights

Light Starters

Moonlight

/**
 * Lights
 */
// Ambient light
const ambientLight = new THREE.AmbientLight('#ffffff', 0.5)
gui.add(ambientLight, 'intensity').min(0).max(1).step(0.001)
scene.add(ambientLight)

// Directional light
const moonLight = new THREE.DirectionalLight('#ffffff', 0.5)
moonLight.position.set(4, 5, - 2)
gui.add(moonLight, 'intensity').min(0).max(1).step(0.001)
gui.add(moonLight.position, 'x').min(- 5).max(5).step(0.001)
gui.add(moonLight.position, 'y').min(- 5).max(5).step(0.001)
gui.add(moonLight.position, 'z').min(- 5).max(5).step(0.001)
scene.add(moonLight)

Shadows / Schatten

Three.js - Shadows - Ausführliche Infos zu Schatten

Komplettes Beispiel

//...
/**
 * Lights
 */
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
gui.add(ambientLight, 'intensity').min(0).max(1).step(0.001)
scene.add(ambientLight)

// Directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5)
directionalLight.position.set(2, 2, - 1)
gui.add(directionalLight, 'intensity').min(0).max(1).step(0.001)
gui.add(directionalLight.position, 'x').min(- 5).max(5).step(0.001)
gui.add(directionalLight.position, 'y').min(- 5).max(5).step(0.001)
gui.add(directionalLight.position, 'z').min(- 5).max(5).step(0.001)

// Add Shadow and mapsize
directionalLight.castShadow = true
directionalLight.shadow.mapSize.x = 1024
directionalLight.shadow.mapSize.y = 1024
// Shadow camera settings...
directionalLight.shadow.camera.near = 1
directionalLight.shadow.camera.far = 6
// ...important for quality
directionalLight.shadow.camera.left = -2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.bottom = -2
// adding a bit of a cheap blur
// directionalLight.shadow.radius = 4

scene.add(directionalLight)

// Shadow camera helper
const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)

// ...

/**
 * Objects
 */
sphere.castShadow = true
// ...
plane.receiveShadow = true
// ...
scene.add(sphere, plane)

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

// Renderer Shadowmap settings
renderer.shadowMap.enabled = true 
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap


Shadow Baking

Wie Texturen kann man auch Schatten baken. Nachteil. Bei Bewegung des Objekts bewegt sich der

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
//...
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    new THREE.MeshBasicMaterial({
        map: bakedShadow
    })
)
// we don't need the rendered shadows in this case
renderer.shadowMap.enabled = false

Dynamic Shadow Baking

Beispiel Kugel mit animiertem Fake-Schatten

const textureLoader = new THREE.TextureLoader()
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')

// Sphere Shadow
const sphereShadow = new THREE.Mesh(
    new THREE.PlaneGeometry(1.5, 1.5),
    new THREE.MeshBasicMaterial({
        color: 0x000000,
        transparent: true,
        alphaMap: simpleShadow
    })
)
sphereShadow.rotation.x = - Math.PI * 0.5
sphereShadow.position.y = plane.position.y + 0.01

scene.add(sphere, sphereShadow, plane)

//...

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update the sphere
    sphere.position.x = Math.cos(elapsedTime) * 1.5
    sphere.position.z = Math.sin(elapsedTime) * 1.5
    sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))

    // Update the shadow accordingly
    sphereShadow.position.x = sphere.position.x
    sphereShadow.position.z = sphere.position.z
    sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

Starters

Starter mit StandardMaterial, Lights, Animation, Resize Handler (webpack)

File:15-lights-good-starter.zip

import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import * as dat from 'lil-gui'

/**
 * Base
 */

// Debug
const gui = new dat.GUI()

// Canvas
const canvas = document.querySelector('canvas.webgl')

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

/**
 * Lights
 */
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)

const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)

/**
 * Objects
 */

// Material
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.4

// Objects
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    material
)
sphere.position.x = - 1.5

const cube = new THREE.Mesh(
    new THREE.BoxGeometry(0.75, 0.75, 0.75),
    material
)

const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 32, 64),
    material
)
torus.position.x = 1.5

const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)
plane.rotation.x = - Math.PI * 0.5
plane.position.y = - 0.65

scene.add(sphere, cube, torus, plane)

/**
 * Sizes
 */
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}

/**
 * Handle Resize
 */
window.addEventListener('resize', () =>
{
    // Update sizes
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight

    // Update camera
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()

    // Update renderer
    renderer.setSize(sizes.width, sizes.height)
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
})

/**
 * Camera
 */
// Base camera
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.x = 1
camera.position.y = 1
camera.position.z = 2
scene.add(camera)

// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true

/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

/**
 * Animate
 */
const clock = new THREE.Clock()

const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()

    // Update objects
    sphere.rotation.y = 0.1 * elapsedTime
    cube.rotation.y = 0.1 * elapsedTime
    torus.rotation.y = 0.1 * elapsedTime

    sphere.rotation.x = 0.15 * elapsedTime
    cube.rotation.x = 0.15 * elapsedTime
    torus.rotation.x = 0.15 * elapsedTime

    // Update controls
    controls.update()

    // Render
    renderer.render(scene, camera)

    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}

tick()

Helpers

Optimizations

Materialien und Geometrien wiederverwenden

Das Erstellen von komplexen Objekten kann zeit- und speicherintensiv sein.

console.time('donuts')
for(let i = 0; i < 100; i++)
{
    const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
    const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
    const donut = new THREE.Mesh(donutGeometry, donutMaterial)
    scene.add(donut)
}
console.timeend('donuts')

Zwei Zeilen umgestellt aber weit über 100mal schneller !

console.time('donuts')
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
for(let i = 0; i < 100; i++)
{
    const donut = new THREE.Mesh(donutGeometry, donutMaterial)
    scene.add(donut)
}
console.timeend('donuts')

Nützliche Schnipsel

Objekte auf Ringbahn positionieren

// Graveyard
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 min = 4 // minimaler Radius
    const max = 8.5 // maximaler Radius
    const radius = min + Math.random() * (max-min)// Radius zwischen min und max
    const angle = Math.random() * 2*Math.PI // 0 < angle < 2PI (voller Kreis im Bogenmaß)
    // (Bogen)Winkel in x,z Koordinaten umrechnen. Ohne * radius wäre Abstand 1
    const x = Math.sin(angle) * radius
    const z = Math.cos(angle) * radius
    const y = 0.35
    
    const grave = new THREE.Mesh(graveGeometry, graveMaterial)
    grave.position.set(x,y,z)
    // Setze Grabsteine leicht schief und verdreht
    grave.rotation.y = (Math.random() - 0.5) * 0.4
    grave.rotation.z = (Math.random() - 0.5) * 0.3 
    
    graves.add(grave)
}

Cool Colors

#ac8e82 - brown walls
#a9c388 - green night grass
#89c854 - green bush
#b35f45 - greek roof red
#b2b6b1 - grey tombstone