ThreeJS - Snippets: Unterschied zwischen den Versionen

Aus Wikizone
Wechseln zu: Navigation, Suche
 
(122 dazwischenliegende Versionen von 3 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
 
== Links ==
 
== Links ==
 
  [[ThreeJS]]
 
  [[ThreeJS]]
 +
[[Three.js - Particles]]
 +
[[Three.js - Shaders]]
  
 +
== Basics ==
 +
<syntaxhighlight lang="javascript">
 +
/**
 +
* Test cube
 +
*/
 +
const cube = new THREE.Mesh(
 +
    new THREE.BoxGeometry(1, 1, 1),
 +
    new THREE.MeshBasicMaterial()
 +
)
 +
scene.add(cube)
 +
</syntaxhighlight>
 
== Helfer ==
 
== Helfer ==
 
=== Axes Helper ===
 
=== Axes Helper ===
Zeile 30: Zeile 43:
  
 
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.
 
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.
 +
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
renderer.setPixelRatio( Math.min(window.devicePixelRatio, 2) )
 
renderer.setPixelRatio( Math.min(window.devicePixelRatio, 2) )
Zeile 68: Zeile 82:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== Animation Basics ==
+
== Animation ==
 +
[[Three.js - Animation]]
 +
[[Three.js - Scroll Based Animation]]
 
=== Timebased Tick / Loop Function ===
 
=== Timebased Tick / Loop Function ===
Für Animationen können wir in einem Loop die Szene Rendern, Objekte verändern, Szene erneut Rendern usw. In JavaScript kann man dazu die window.requestAnimationFrame Funktion nutzen. Damit die zeitlichen Abläufe nicht von der Rechnerleistung sondern rein von der Zeit abhängen gibt es einige Möglichkeiten diesen Loop umzusetzen.
 
 
Die Beispiele setzen eine Setup mit einem Camera Object 'camera', einer Szene scene, und einem Renderer 'renderer' voraus. Du kannst z.B. das Beispiel auf der Hauptseite nutzen.
 
==== Pure JavaScript calculation ====
 
<syntaxhighlight lang="javascript">
 
let time = Date.now()
 
const tick = () =>
 
{
 
    // JS based time calculation
 
    const currentTime = Date.now()
 
    const deltaTime = currentTime - time
 
    time = currentTime
 
    //console.log(deltaTime)
 
    // Update objects
 
    mesh.rotation.y += 0.001 * deltaTime
 
    renderer.render(scene, camera)
 
    // tell JS to call tick on the next frame
 
    window.requestAnimationFrame(tick)
 
}
 
// go...
 
tick()
 
</syntaxhighlight>
 
 
 
==== ThreeJS Clock Object ====
 
==== ThreeJS Clock Object ====
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
Zeile 128: Zeile 121:
  
 
== Nützliche Snippets für Animationen ==
 
== Nützliche Snippets für Animationen ==
 +
 
=== Kreisbewegung / Circular Movement ===
 
=== Kreisbewegung / Circular Movement ===
 
  myObject.position.y = Math.sin(elapsedTime) //(-1 -> 1 -> -1 -> ...)
 
  myObject.position.y = Math.sin(elapsedTime) //(-1 -> 1 -> -1 -> ...)
 
  myObject.position.x = Math.cos(elapsedTime)
 
  myObject.position.x = Math.cos(elapsedTime)
 +
 
=== Cursor auswerten ===
 
=== Cursor auswerten ===
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
Zeile 166: Zeile 161:
  
 
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.
 
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 ===
 +
<syntaxhighlight lang="javascript">
 +
    // 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
 +
</syntaxhighlight>
  
 
== Orbit Controls ==
 
== Orbit Controls ==
Zeile 178: Zeile 186:
 
== Geometry Snippets ==
 
== Geometry Snippets ==
 
=== Create Geometry / Geometry Objekt erzeugen ===
 
=== Create Geometry / Geometry Objekt erzeugen ===
 +
[[Three.js - eigene Geometrie erzeugen]]
 
Beispiel: viele zufällige Dreiecke erzeugen
 
Beispiel: viele zufällige Dreiecke erzeugen
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
Zeile 211: Zeile 220:
 
})
 
})
 
gui.close()
 
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
 
// Debug
Zeile 233: Zeile 232:
 
// we can not use material.color as it's not an object
 
// we can not use material.color as it's not an object
 
// thus we use a separately created object...
 
// thus we use a separately created object...
 +
// ... and update material when this param changed:
 
gui.addColor(params,'color')
 
gui.addColor(params,'color')
// ... and update material when this param changed:
 
 
.onChange( ()=>
 
.onChange( ()=>
 
{
 
{
Zeile 240: Zeile 239:
 
})
 
})
 
gui.add(params, 'spin')
 
gui.add(params, 'spin')
 +
</syntaxhighlight>
 +
'''Beispiel: Renderer ClearColor'''
 +
<syntaxhighlight lang="javascript">
 +
// Debug
 +
const params = {}
 +
const gui = new dat.GUI({width: 400})
 +
params.clearColor = '#2b4945'
 +
//...
 +
renderer.setClearColor(params.clearColor)
 +
gui.addColor(params, 'clearColor').onChange( () => {renderer.setClearColor(params.clearColor)})
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
== Textures ==
 
== Textures ==
 
  https://threejs.org/docs/index.html?q=texture#api/en/constants/Textures
 
  https://threejs.org/docs/index.html?q=texture#api/en/constants/Textures
 +
[[three.js - Textures]] - ausführliche Infos zu TextureLoader Callbacks, LoadingManager...
 +
[[three.js - baked map texture]]
 +
=== Beispiel ===
 +
<syntaxhighlight lang="javascript">
 +
/**
 +
* 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 doorNormalTexture = textureLoader.load('/textures/door/normal.jpg')
 +
const doorMetalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
 +
const doorRoughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
 +
 +
// HOW TO REPEAT TILES - uv wrapping
 +
const repeat = 10;
 +
 +
const grassColorTexture = textureLoader.load('/textures/grass/color.jpg')
 +
const grassNormalTexture = textureLoader.load('/textures/grass/normal.jpg')
 +
const grassRoughnessTexture = textureLoader.load('/textures/grass/roughness.jpg')
 +
 +
grassColorTexture.repeat.set(repeat,repeat)
 +
grassNormalTexture.repeat.set(repeat,repeat)
 +
grassRoughnessTexture.repeat.set(repeat,repeat)
 +
 +
grassColorTexture.wrapS = THREE.RepeatWrapping
 +
grassNormalTexture.wrapS = THREE.RepeatWrapping
 +
grassRoughnessTexture.wrapS = THREE.RepeatWrapping
 +
 +
grassColorTexture.wrapT = THREE.RepeatWrapping
 +
grassNormalTexture.wrapT = THREE.RepeatWrapping
 +
grassRoughnessTexture.wrapT = THREE.RepeatWrapping
 +
 +
//...
 +
 +
/**
 +
* Objects
 +
*/
 +
 +
// Door
 +
const door = new THREE.Mesh(
 +
    new THREE.PlaneGeometry(2,2,100,100),// HEIGHTMAP NEEDS SOME SUBDIVISIONS
 +
    new THREE.MeshStandardMaterial({
 +
        map: doorColorTexture,
 +
        transparent: true, // NEEDED FOR ALPHA TO WORK
 +
        alphaMap: doorAlphaTexture,
 +
        aoMap: doorAmbientOcclusionTexture,
 +
        displacementMap: doorHeightTexture,
 +
        displacementScale: 0.05,
 +
        normalMap: doorNormalTexture,
 +
        metalnessMap: doorMetalnessTexture,
 +
        roughnessMap: doorRoughnessTexture,
 +
        //wireframe: true
 +
    })
 +
)
 +
// AOMAP NEEDS HIS OWN UV ATTRIBUTE (here we copy from geometry)
 +
door.geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(door.geometry.attributes.uv.array, 2))
 +
 +
house.add(door)
 +
 +
// Floor
 +
const floor = new THREE.Mesh(
 +
    new THREE.PlaneGeometry(20, 20),
 +
    new THREE.MeshStandardMaterial({
 +
        //color: '#a9c388',
 +
        map: grassColorTexture,
 +
        normalMap: grassNormalTexture,
 +
        roughnessMap: grassRoughnessTexture   
 +
    })
 +
)
 +
 +
floor.rotation.x = - Math.PI * 0.5
 +
floor.position.y = 0
 +
 +
scene.add(floor)
 +
</syntaxhighlight>
 +
 +
== Materials ==
 +
[[three.js - materials]] - Mehr Info und weitere Materialien.
 +
 +
=== MeshBasicMaterial ===
 +
<syntaxhighlight lang="javascript">
 +
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
 +
</syntaxhighlight>
 +
 +
=== MeshMatcapMaterial ===
 +
https://github.com/nidorx/matcaps - gute Quelle
 +
<syntaxhighlight lang="javascript">
 +
// 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
 +
</syntaxhighlight>
 +
 +
=== MeshStandardMaterial ===
 +
Standard Physical Based Rendering Material
 +
 +
<syntaxhighlight lang="javascript">
 +
// 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)
 +
)
 +
</syntaxhighlight>
 +
 +
=== Environment Map ===
 +
[[Three.js - Environment Map (Panorama)]]
 +
 +
<syntaxhighlight lang="javascript">
 +
// 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)
 +
 +
</syntaxhighlight>
 +
 +
=== 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====
 +
<syntaxhighlight lang="javascript">
 +
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()
  
Zur Erinnerung Objekte sind so aufgeaut:
+
fontLoader.load(
Mesh Objekt > Geometry > Material > Textur
+
    //'/fonts/helvetiker_regular.typeface.json',
=== Prinzip' ===
+
    '/fonts/BebasNeueBook_Regular.json',
'''Manuell:'''
+
    (font) =>
* Bild Objekt erstellen
+
    {
* Texture Objekt erstellen und Bild Objekt zuordnen
+
        console.log('font loaded')
* Bildsource dem Bildobjekt zuordnen >
+
        const textGeometry = new TextGeometry(
* wenn Bild geladen ist (z.B addEventListener oder onload wie unten)texture.needsUpdate setzen, damit ThreeJs die Texture updaten kann
+
            'KHOLJA',
* Texture dem Material zuordnen (Property map)
+
            {
 +
                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)
 +
    }
 +
)
 +
</syntaxhighlight>
  
'''Mit TextureLoader (recommended):'''
+
== Lights ==
* Wenn gewünscht LoadingManager erstellen
+
[[Three.js - Lights]]
* TextureLoader erstellen und LoadingManager übergeben
+
=== Light Starters ===
* Bildquelle mit .load() in TextureLoader laden -> gibt texture Objekt zurück
+
==== Neutral Starter ====
* texture dem Material zuordnen
+
<syntaxhighlight lang="javascript">
 +
/**
 +
* 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)
  
=== Beispiele ===
+
// Directional light
 +
const directionalLight = new THREE.DirectionalLight('#ffffff', 0.5)
 +
directionalLight.position.set(4, 5, - 2)
 +
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)
 +
scene.add(moonLight)
 +
</syntaxhighlight>
  
'''Beispiel 1 - manuell laden'''
+
==== Moonlight Starter ====
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
/**
 
/**
  * Textures
+
  * Lights
 
  */
 
  */
const image = new Image()
+
// Ambient light
const texture = new THREE.Texture(image)  
+
const ambientLight = new THREE.AmbientLight('#b9d5ff', 0.5)
image.src = '/textures/door/color.jpg'
+
gui.add(ambientLight, 'intensity').min(0).max(1).step(0.001)
image.onload = () =>  
+
scene.add(ambientLight)
{
+
 
    texture.needsUpdate = true
+
// Directional light
}
+
const moonLight = new THREE.DirectionalLight('#b9d5ff', 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)
 +
</syntaxhighlight>
 +
 
 +
== Shadows / Schatten ==
 +
[[Three.js - Shadows]] - Ausführliche Infos zu Schatten
  
 +
=== Komplettes Beispiel ===
 +
<syntaxhighlight lang="javascript">
 
//...
 
//...
 +
/**
 +
* 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))
  
const material = new THREE.MeshBasicMaterial({ map: texture })
+
// Renderer Shadowmap settings
 +
renderer.shadowMap.enabled = true
 +
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== TextureLoader ===
 
Der TextureLoader macht im Prinzip das Gleiche, erspart uns aber die onload Funktion :-). Ein TextureLoader kann für mehrere Texturen verwendet werden.
 
  
 +
==== Shadow Baking ====
 +
Wie Texturen kann man auch Schatten baken. Nachteil. Bei Bewegung des Objekts bewegt sich der
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
/**
 
/**
Zeile 289: Zeile 599:
 
  */
 
  */
 
const textureLoader = new THREE.TextureLoader()
 
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
+
const bakedShadow = textureLoader.load('/textures/bakedShadow.jpg')
 
//...
 
//...
const material = new THREE.MeshBasicMaterial({ map: texture })
+
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
 
</syntaxhighlight>
 
</syntaxhighlight>
  
'''TextureLoader Callbacks'''
+
==== Dynamic Shadow Baking ====
Manchmal nützlich für Fehlersuche etc.
+
'''Beispiel Kugel mit animiertem Fake-Schatten'''
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
 
const textureLoader = new THREE.TextureLoader()
 
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
+
const simpleShadow = textureLoader.load('/textures/simpleShadow.jpg')
    '/textures/door/color.jpg',
+
 
    () =>
+
// Sphere Shadow
     {
+
const sphereShadow = new THREE.Mesh(
        console.log('load')
+
     new THREE.PlaneGeometry(1.5, 1.5),
    },
+
     new THREE.MeshBasicMaterial({
     () =>
+
         color: 0x000000,
    {
+
        transparent: true,
         console.log('progress')
+
         alphaMap: simpleShadow
    },
+
     })
    () =>
 
    {
 
         console.log('error')
 
     }
 
 
)
 
)
</syntaxhighlight>
+
sphereShadow.rotation.x = - Math.PI * 0.5
 +
sphereShadow.position.y = plane.position.y + 0.01
  
=== Loading Manager ===
+
scene.add(sphere, sphereShadow, plane)
Nützlich wenn viele Assets geladen werden müssen. Also z.B. viele Texturen. Wird erstellt und dann als Parameter dem TextureLoader übergeben. Dann kann man über die Callbacks den Ladevorgang checken und darauf reagieren.
 
  
<syntaxhighlight lang="javascript">
+
//...
  
const loadingManager = new THREE.LoadingManager()
+
/**
 +
* Animate
 +
*/
 +
const clock = new THREE.Clock()
  
loadingManager.onStart = () =>
+
const tick = () =>
 
{
 
{
     console.log('onStart')
+
     const elapsedTime = clock.getElapsedTime()
}
+
 
loadingManager.onLoad = () =>
+
    // Update the sphere
{
+
    sphere.position.x = Math.cos(elapsedTime) * 1.5
     console.log('onLoad')
+
    sphere.position.z = Math.sin(elapsedTime) * 1.5
}
+
     sphere.position.y = Math.abs(Math.sin(elapsedTime * 3))
loadingManager.onProgress = () =>
+
 
{
+
    // Update the shadow accordingly
     console.log('onProgress')
+
    sphereShadow.position.x = sphere.position.x
}
+
    sphereShadow.position.z = sphere.position.z
loadingManager.onError = () =>
+
    sphereShadow.material.opacity = (1 - sphere.position.y) * 0.3
{
+
 
     console.log('onError')
+
    // Update controls
 +
     controls.update()
 +
 
 +
    // Render
 +
    renderer.render(scene, camera)
 +
 
 +
     // Call tick again on the next frame
 +
    window.requestAnimationFrame(tick)
 
}
 
}
  
const textureLoader = new THREE.TextureLoader(loadingManager)
+
tick()
 +
</syntaxhighlight>
  
const colorTexture = textureLoader.load('/textures/door/color.jpg')
+
== Fog ==
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
+
[[Three.js - Fog]]
const heightTexture = textureLoader.load('/textures/door/height.jpg')
+
<syntaxhighlight lang="javascript">
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
+
//...
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
+
// Fog
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
+
const fog = new THREE.Fog('#262837', 1, 15)
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
+
scene.fog = fog
 +
//...
 +
renderer.setClearColor('#262837')
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== UV Wrapping ===
+
== Particles ==
====Wrapping Mode====
+
Benötigen eine Geometry, ein Material, ein Points Objekt (statt wie sonst ein Mesh)
  THREE.RepeatWrapping
+
[[Three.js - Particles]]
THREE.ClampToEdgeWrapping // default
+
'''Starter Examples'''
THREE.MirroredRepeatWrapping
+
<syntaxhighlight lang="javascript">
 +
/**
 +
* Particles
 +
  */
 +
// Geometry
 +
const particlesGeometry = new THREE.SphereBufferGeometry(1,32,32)
 +
// Material
 +
const particlesMaterial = new THREE.PointsMaterial({
 +
    size: 0.02,
 +
    sizeAttenuation: true, //perspective smaller if far
 +
})
 +
// Points
 +
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
 +
scene.add(particles)
 +
</syntaxhighlight>
  
 +
'''Random Points Starter'''
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
const colorTexture = textureLoader.load('/textures/door/color.jpg')
+
/**
 +
* Fireflies
 +
*/
 +
const firefliesGeometry = new THREE.BufferGeometry()
 +
const firefliesCount = 30
 +
const positionArray = new Float32Array(firefliesCount * 3)
  
// TEXTURE TRANSFORMATIONS
+
for(let i = 0; i < firefliesCount; i++)
colorTexture.repeat.x = 2
+
{
colorTexture.repeat.y = 3
+
    positionArray[i * 3 + 0] = ( Math.random() -0.5) * 4.5 // x from -0.5 to 4
 +
    positionArray[i * 3 + 1] = ( Math.random() + 0.05) * 3 // y from 0.05 to 2.95
 +
    positionArray[i * 3 + 2] = ( Math.random() ) * 4.5 // z from 0 to 4.5
 +
}
  
colorTexture.wrapS = THREE.RepeatWrapping
+
firefliesGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3))
colorTexture.wrapT = THREE.RepeatWrapping
+
const firefliesMaterial = new THREE.PointsMaterial({ size: 0.1, sizeAttenuation: true })
//colorTexture.wrapS = THREE.MirroredRepeatWrapping
+
const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial)
//colorTexture.wrapT = THREE.MirroredRepeatWrapping
 
  
colorTexture.offset.x = 0.5
+
scene.add(fireflies)
colorTexture.offset.y = 0.5
+
</syntaxhighlight>
  
colorTexture.center.x = 0.5
+
== Models Importieren ==
colorTexture.center.y = 0.5
+
[[Three.js - Import Models]]
colorTexture.rotation = Math.PI * 0.25
 
  
 +
<syntaxhighlight lang="javascript">
 +
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 +
//...
 +
const gltfLoader = new GLTFLoader()
 +
gltfLoader.load(
 +
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
 +
    (gltf) => {
 +
        console.log(gltf) //success - show what we got
 +
        const children = [...gltf.scene.children] // duplicate
 +
        for(const child of children){
 +
            scene.add(child)
 +
        }
 +
        //scene.add(gltf.scene)
 +
        //scene.add(gltf.scene.children[1])
 +
    },
 +
    () => {console.log('progress')},
 +
    () => {console.log('error')}
 +
)
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
=== DRACO Loader nutzen ===
  
==== Mipmapping ====
+
<syntaxhighlight lang="javascript">
Je nach Entfernung eines Objekts wird eine Textur kleiner oder größer angezeigt. Daher erstellt ThreeJS kleinere Versionen einer Textur und speichert diese zusammen mit dem Original in einer Mipmap. Dazu wird die original Texture immer wieder um die Hälfte verkleinert bis nur noch ein Pixel vorhanden ist. Im Prinzip ist die Map ein Bild aus allen Bildern. Diesen Prozess nennt man Mipmapping.  
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
 +
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
 +
//...
 +
const dracoLoader = new DRACOLoader()
 +
dracoLoader.setDecoderPath("/draco/") // get webassembly version (faster but you have to provide the appropriate draco files)
  
Für das Erstellen dieser Bildversionen kann man verschiedene '''Filter''' einsetzen. Diese resultieren z.B. in besserer Schärfe oder weniger Moiree Effekten, resultieren aber auch in unterschiedlicher Performance.
+
const gltfLoader = new GLTFLoader()
 +
gltfLoader.setDRACOLoader(dracoLoader)
 +
gltfLoader.load(
 +
    '/models/Duck/glTF-Draco/Duck.gltf',
 +
    (gltf) => {
 +
        scene.add(gltf.scene)
 +
    }
 +
)
 +
</syntaxhighlight>
  
'''Minification'''
+
== Starters ==
 +
[[Three.js - Starters]]
  
Wenn das Objekt klein dargestellt wird, hat der Screen für das Bild weniger Pixel zur verfügung als die Textur hergibt. In dem Fall spricht man von '''Minification'''.
+
== Helpers ==
 +
=== Nützliche Renderer Settings ===
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
const colorTexture = textureLoader.load('/textures/checkerboard-1024x1024.png')
+
/**
colorTexture.generateMipmaps = false // NearestFilter doesn't need Mipmap
+
* Renderer
colorTexture.minFilter = THREE.NearestFilter // oft bessere Schärfe aber auch mehr Moiré
+
*/
 +
const renderer = new THREE.WebGLRenderer({
 +
    canvas: canvas,
 +
})
 +
renderer.setSize(sizes.width, sizes.height)
 +
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
 +
renderer.setClearColor('#262837')
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
=== Nützliche Animation Settings ===
 +
<syntaxhighlight lang="javascript">
 +
/**
 +
* Animate
 +
*/
 +
const clock = new THREE.Clock()
  
'''Magnification'''
+
const tick = () =>
 +
{
 +
    const elapsedTime = clock.getElapsedTime()
  
Im gegenteiligen Fall - das Objekt ist sehr nahe reichen die Bildpixel der Textur nicht mehr aus und sie muss skaliert werden. Auch hier kann man die selben Filter einsetzen. Der Standardfilter interpoliert die Pixel und erzeugt eine '''Unschärfe'''. Das passt in den meisten Fällen am besten. Bei Geometrischen Formen (z.b. wie in Minecraft-Kacheln) funktioniert der Nearest Filter gut, der einfach die Pixel wiederholt.  
+
    // Update material (used for own Shader only)
 +
    material.uniforms.uTime.value = elapsedTime
  
<syntaxhighlight lang="javascript">
+
    // Update controls (used for orbit controls only)
const colorTexture = textureLoader.load('/textures/minecraft.png')
+
    controls.update()
colorTexture.magFilter = THREE.NearestFilter
 
</syntaxhighlight>
 
  
'''Hinweise für bessere Performance'''
+
    // Render
* Mipmapping halbiert das Bild bis nur noch ein Pixel übrig ist. Daher sollte die '''Pixelanzahl''' des Bildes eine '''zweier Potenz''' sein. Am besten nimmt man Formate wie ... 256x256 / 512x1024 / 1024x1024 ... Oft nimmt man quadratische Formate.
+
    renderer.render(scene, camera)
  
* Da der '''NearestFilter''' keine Interpolation macht sondern nur Pixel wegwirft bzw. verdoppelt, hat er auch eine etwas bessere Performance als andere Filter.  
+
    // Call tick again on the next frame
 +
    window.requestAnimationFrame(tick)
 +
}
  
* Wenn der '''NearestFilter für die Minification''' genutzt wird kann man das '''Mipmapping ausschalten'''. der Filter ignoriert einfach die nicht benötigten Pixel der Originaltextur.
+
tick()
 
+
</syntaxhighlight>
* '''Transparenz''' kann über ein '''transparentes png''' erreicht werden oder über über eine '''Alpha-Kanal''' Bildversion (weiß = opak, schwarz = transparent). '''Alpha ist GPU-Freundlicher''', ein einzelnes png kann vielleicht etwas Ladezeit sparen.
+
=== Orbit Controls ===
 
+
Don't forget to update in tick function, when animating
* '''Normal-Maps''' geben über die Farbe die Richtung der Normalen (=Oberfläche) vor. Das sollte exakt sein, deshalb hier lieber ein '''png Verwenden''' (verlustfrei).
 
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
colorTexture.generateMipmaps = false // NearestFilter doesn't need that
+
// Controls
colorTexture.minFilter = THREE.NearestFilter
+
const controls = new OrbitControls(camera, canvas)
colorTexture.magFilter = THREE.NearestFilter
+
controls.enableDamping = true
 
</syntaxhighlight>
 
</syntaxhighlight>
  
* Es ist möglich '''mehrere Texturen in einem Bild''' zu vereinen. '''Z.b.''' kann man den '''Rotkanal für eine Heightmap''' und den '''Blaukanal als Alphakanal''' verwenden. Dies verkürzt die ladezeit.
+
=== Optimizations ===
 
+
==== Materialien und Geometrien wiederverwenden ====
== Materials ==
+
Das Erstellen von komplexen Objekten kann zeit- und speicherintensiv sein.
Eigentschaften 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)
 
=== MeshBasicMaterial ===
 
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
const material = new THREE.MeshBasicMaterial()
+
console.time('donuts')
material.color.set(0xaabb00)
+
for(let i = 0; i < 100; i++)
material.map = doorColorTexture
+
{
material.side = DoubleSide
+
    const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
material.wireframe = true
+
    const donutMaterial = new THREE.MeshMatcapMaterial({ matcap: matcapTexture })
material.transparent = true
+
    const donut = new THREE.Mesh(donutGeometry, donutMaterial)
material.opacity = 0.5
+
    scene.add(donut)
material.alphaMap = doorAlphaTexture
+
}
 +
console.timeend('donuts')
 
</syntaxhighlight>
 
</syntaxhighlight>
 
+
Zwei Zeilen umgestellt aber weit '''über 100mal schneller !'''
Komplexere Materialien haben natürlich weitere Eigenschaften
 
=== MeshNormalMaterial ===
 
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
// normally to debug normals
+
console.time('donuts')
const material = new THREE.MeshNormalMaterial()
+
const donutGeometry = new THREE.TorusGeometry(0.3, 0.2, 20, 45)
material.flatShading = true
+
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')
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== MeshMatcapMaterial ===
+
=== Nützliche Schnipsel ===
https://github.com/nidorx/matcaps - gute Quelle
+
==== Objekte auf Ringbahn positionieren ====
MeshMatcapMaterial is a fantastic material because of how great it can look while being very performant.
+
<syntaxhighlight lang="javascript">
* Performant
+
// Graveyard
* simuliert Licht ohne Lichtquelle
+
const graves = new THREE.Group()
* gut für's Modelling
+
scene.add(graves)
 +
const graveGeometry = new THREE.BoxGeometry(0.6, 0.8, 0.2)
 +
const graveMaterial = new THREE.MeshStandardMaterial({color: '#b2b6b1'})
  
For it to work, the MeshMatcapMaterial '''needs a reference texture that looks like a sphere'''.
+
for (let i = 0; i  < 50; i++) {
The material will then pick colors on the texture according to the normal orientation relative to the camera.
+
    const min = 4 // minimaler Radius
To set that reference matcap texture, use the matcap property. For it to work, the MeshMatcapMaterial needs a reference '''texture that looks like a sphere'''. The material will then pick colors on the texture according to the normal orientation relative to the camera.
+
    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)
 +
}
 +
</syntaxhighlight>
  
 +
==== Zufallsarrays für Particles sinnvoll aufbauen ====
 
<syntaxhighlight lang="javascript">
 
<syntaxhighlight lang="javascript">
// MATCAP
+
for(let i = 0; i < firefliesCount; i++)
const material = new THREE.MeshMatcapMaterial()
+
{
material.matcap = matcapTexture
+
    positionArray[i * 3 + 0] = Math.random() * 4
 +
    positionArray[i * 3 + 1] = Math.random() * 4
 +
    positionArray[i * 3 + 2] = Math.random() * 4
 +
}
 +
console.log(positionArray)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== MeshDepthMaterial ===
+
=== Cool Colors ===
Nahe Bereiche werden hell, ferne dunkel gerendert. Kann man z.B. für Camera Tests oder für Nebel nutzen
+
<pre>
 
+
#ac8e82 - brown walls
<syntaxhighlight lang="javascript">
+
#a9c388 - green night grass
// DEPTH
+
#89c854 - green bush
const material = new THREE.MeshDepthMaterial()
+
#b35f45 - greek roof red
material.transparent = true
+
#b2b6b1 - grey tombstone
material.opacity = 0.8
+
#b9d5ff - blue moonlight
</syntaxhighlight>
+
#ff7d49 - orange warm light
 +
#262837 - blueish fog
 +
</pre>

Aktuelle Version vom 17. Februar 2022, 01:39 Uhr

Links[Bearbeiten]

ThreeJS
Three.js - Particles
Three.js - Shaders

Basics[Bearbeiten]

/**
 * Test cube
 */
 const cube = new THREE.Mesh(
    new THREE.BoxGeometry(1, 1, 1),
    new THREE.MeshBasicMaterial()
)
scene.add(cube)

Helfer[Bearbeiten]

Axes Helper[Bearbeiten]

Koordinatenachsen anzeigen

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

Viewport Settings[Bearbeiten]

Handle Viewport Resizing[Bearbeiten]

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)[Bearbeiten]

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[Bearbeiten]

// 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[Bearbeiten]

Three.js - Animation
Three.js - Scroll Based Animation

Timebased Tick / Loop Function[Bearbeiten]

ThreeJS Clock Object[Bearbeiten]

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[Bearbeiten]

// 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[Bearbeiten]

Kreisbewegung / Circular Movement[Bearbeiten]

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

Cursor auswerten[Bearbeiten]

// 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[Bearbeiten]

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[Bearbeiten]

    // 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[Bearbeiten]

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[Bearbeiten]

Create Geometry / Geometry Objekt erzeugen[Bearbeiten]

Three.js - eigene Geometrie 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[Bearbeiten]

lil-gui[Bearbeiten]

// https://lil-gui.georgealways.com/#
import GUI from 'lil-gui'; 
/**
 * Debug
 */
const gui = new GUI({
    width:400
})
gui.close()
//...
// 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...
// ... and update material when this param changed:
gui.addColor(params,'color')
.onChange( ()=>
{
    material.color.set(params.color)
})
gui.add(params, 'spin')

Beispiel: Renderer ClearColor

// Debug
const params = {}
const gui = new dat.GUI({width: 400})
params.clearColor = '#2b4945'
//...
renderer.setClearColor(params.clearColor)
gui.addColor(params, 'clearColor').onChange( () => {renderer.setClearColor(params.clearColor)})

Textures[Bearbeiten]

https://threejs.org/docs/index.html?q=texture#api/en/constants/Textures
three.js - Textures - ausführliche Infos zu TextureLoader Callbacks, LoadingManager...
three.js - baked map texture

Beispiel[Bearbeiten]

/**
 * 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 doorNormalTexture = textureLoader.load('/textures/door/normal.jpg')
const doorMetalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const doorRoughnessTexture = textureLoader.load('/textures/door/roughness.jpg')

// HOW TO REPEAT TILES - uv wrapping
const repeat = 10;

const grassColorTexture = textureLoader.load('/textures/grass/color.jpg')
const grassNormalTexture = textureLoader.load('/textures/grass/normal.jpg')
const grassRoughnessTexture = textureLoader.load('/textures/grass/roughness.jpg')

grassColorTexture.repeat.set(repeat,repeat)
grassNormalTexture.repeat.set(repeat,repeat)
grassRoughnessTexture.repeat.set(repeat,repeat)

grassColorTexture.wrapS = THREE.RepeatWrapping
grassNormalTexture.wrapS = THREE.RepeatWrapping
grassRoughnessTexture.wrapS = THREE.RepeatWrapping

grassColorTexture.wrapT = THREE.RepeatWrapping
grassNormalTexture.wrapT = THREE.RepeatWrapping
grassRoughnessTexture.wrapT = THREE.RepeatWrapping

//...

/** 
 * Objects
 */

// Door
const door = new THREE.Mesh(
    new THREE.PlaneGeometry(2,2,100,100),// HEIGHTMAP NEEDS SOME SUBDIVISIONS
    new THREE.MeshStandardMaterial({
        map: doorColorTexture,
        transparent: true, // NEEDED FOR ALPHA TO WORK
        alphaMap: doorAlphaTexture,
        aoMap: doorAmbientOcclusionTexture,
        displacementMap: doorHeightTexture,
        displacementScale: 0.05,
        normalMap: doorNormalTexture,
        metalnessMap: doorMetalnessTexture,
        roughnessMap: doorRoughnessTexture,
        //wireframe: true
    })
)
// AOMAP NEEDS HIS OWN UV ATTRIBUTE (here we copy from geometry)
door.geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(door.geometry.attributes.uv.array, 2))

house.add(door)

// Floor
const floor = new THREE.Mesh(
    new THREE.PlaneGeometry(20, 20),
    new THREE.MeshStandardMaterial({ 
        //color: '#a9c388',
        map: grassColorTexture,
        normalMap: grassNormalTexture,
        roughnessMap: grassRoughnessTexture    
    })
)

floor.rotation.x = - Math.PI * 0.5
floor.position.y = 0

scene.add(floor)

Materials[Bearbeiten]

three.js - materials - Mehr Info und weitere Materialien.

MeshBasicMaterial[Bearbeiten]

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

MeshMatcapMaterial[Bearbeiten]

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

MeshStandardMaterial[Bearbeiten]

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)
)

Environment Map[Bearbeiten]

Three.js - Environment Map (Panorama)
// 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)

Hinweise[Bearbeiten]

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[Bearbeiten]

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

Fontloader[Bearbeiten]

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[Bearbeiten]

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[Bearbeiten]

Three.js - Lights

Light Starters[Bearbeiten]

Neutral Starter[Bearbeiten]

/**
 * 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 directionalLight = new THREE.DirectionalLight('#ffffff', 0.5)
directionalLight.position.set(4, 5, - 2)
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)
scene.add(moonLight)

Moonlight Starter[Bearbeiten]

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

// Directional light
const moonLight = new THREE.DirectionalLight('#b9d5ff', 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[Bearbeiten]

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

Komplettes Beispiel[Bearbeiten]

//...
/**
 * 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[Bearbeiten]

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[Bearbeiten]

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()

Fog[Bearbeiten]

Three.js - Fog
//...
// Fog
const fog = new THREE.Fog('#262837', 1, 15)
scene.fog = fog 
//...
renderer.setClearColor('#262837')

Particles[Bearbeiten]

Benötigen eine Geometry, ein Material, ein Points Objekt (statt wie sonst ein Mesh)

Three.js - Particles

Starter Examples

/**
 * Particles
 */
// Geometry
const particlesGeometry = new THREE.SphereBufferGeometry(1,32,32)
// Material
const particlesMaterial = new THREE.PointsMaterial({
    size: 0.02,
    sizeAttenuation: true, //perspective smaller if far
})
// Points
const particles = new THREE.Points(particlesGeometry, particlesMaterial)
scene.add(particles)

Random Points Starter

/**
 * Fireflies
 */
const firefliesGeometry = new THREE.BufferGeometry()
const firefliesCount = 30
const positionArray = new Float32Array(firefliesCount * 3)

for(let i = 0; i < firefliesCount; i++)
{
    positionArray[i * 3 + 0] = ( Math.random() -0.5) * 4.5 // x from -0.5 to 4
    positionArray[i * 3 + 1] = ( Math.random() + 0.05) * 3 // y from 0.05 to 2.95
    positionArray[i * 3 + 2] = ( Math.random() ) * 4.5 // z from 0 to 4.5
}

firefliesGeometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3))
const firefliesMaterial = new THREE.PointsMaterial({ size: 0.1, sizeAttenuation: true })
const fireflies = new THREE.Points(firefliesGeometry, firefliesMaterial)

scene.add(fireflies)

Models Importieren[Bearbeiten]

Three.js - Import Models

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
//...
const gltfLoader = new GLTFLoader()
gltfLoader.load(
    '/models/FlightHelmet/glTF/FlightHelmet.gltf',
    (gltf) => { 
        console.log(gltf) //success - show what we got
        const children = [...gltf.scene.children] // duplicate
        for(const child of children){
            scene.add(child)
        }
        //scene.add(gltf.scene)
        //scene.add(gltf.scene.children[1])
    },
    () => {console.log('progress')},
    () => {console.log('error')}
)

DRACO Loader nutzen[Bearbeiten]

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
//...
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath("/draco/") // get webassembly version (faster but you have to provide the appropriate draco files)

const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load(
    '/models/Duck/glTF-Draco/Duck.gltf',
    (gltf) => { 
        scene.add(gltf.scene)
    }
)

Starters[Bearbeiten]

Three.js - Starters

Helpers[Bearbeiten]

Nützliche Renderer Settings[Bearbeiten]

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

Nützliche Animation Settings[Bearbeiten]

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

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

    // Update material (used for own Shader only)
    material.uniforms.uTime.value = elapsedTime

    // Update controls (used for orbit controls only)
    controls.update()

    // Render
    renderer.render(scene, camera)

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

tick()

Orbit Controls[Bearbeiten]

Don't forget to update in tick function, when animating

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

Optimizations[Bearbeiten]

Materialien und Geometrien wiederverwenden[Bearbeiten]

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[Bearbeiten]

Objekte auf Ringbahn positionieren[Bearbeiten]

// 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)
}

Zufallsarrays für Particles sinnvoll aufbauen[Bearbeiten]

for(let i = 0; i < firefliesCount; i++)
{
    positionArray[i * 3 + 0] = Math.random() * 4
    positionArray[i * 3 + 1] = Math.random() * 4
    positionArray[i * 3 + 2] = Math.random() * 4
}
console.log(positionArray)

Cool Colors[Bearbeiten]

#ac8e82 - brown walls
#a9c388 - green night grass
#89c854 - green bush
#b35f45 - greek roof red
#b2b6b1 - grey tombstone
#b9d5ff - blue moonlight
#ff7d49 - orange warm light
#262837 - blueish fog