ThreeJS - Snippets: Unterschied zwischen den Versionen
| Zeile 450: | Zeile 450: | ||
The material will then pick colors on the texture according to the normal orientation relative to the camera. | The material will then pick colors on the texture according to the normal orientation relative to the camera. | ||
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. | 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. | ||
| − | < | + | <syntaxhighlight lang="javascript"> |
// MATCAP | // MATCAP | ||
const material = new THREE.MeshMatcapMaterial() | const material = new THREE.MeshMatcapMaterial() | ||
material.matcap = matcapTexture | material.matcap = matcapTexture | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Version vom 27. Dezember 2021, 20:46 Uhr
Links
ThreeJS
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
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
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()
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.
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
Zur Erinnerung Objekte sind so aufgeaut:
Mesh Objekt > Geometry > Material > Textur
Prinzip'
Manuell:
- Bild Objekt erstellen
- Texture Objekt erstellen und Bild Objekt zuordnen
- Bildsource dem Bildobjekt zuordnen >
- wenn Bild geladen ist (z.B addEventListener oder onload wie unten)texture.needsUpdate setzen, damit ThreeJs die Texture updaten kann
- Texture dem Material zuordnen (Property map)
Mit TextureLoader (recommended):
- Wenn gewünscht LoadingManager erstellen
- TextureLoader erstellen und LoadingManager übergeben
- Bildquelle mit .load() in TextureLoader laden -> gibt texture Objekt zurück
- texture dem Material zuordnen
Beispiele
Beispiel 1 - manuell laden
/**
* Textures
*/
const image = new Image()
const texture = new THREE.Texture(image)
image.src = '/textures/door/color.jpg'
image.onload = () =>
{
texture.needsUpdate = true
}
//...
const material = new THREE.MeshBasicMaterial({ map: texture })
TextureLoader
Der TextureLoader macht im Prinzip das Gleiche, erspart uns aber die onload Funktion :-). Ein TextureLoader kann für mehrere Texturen verwendet werden.
/**
* 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 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.
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
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.
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.
Minification
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.
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
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.
const colorTexture = textureLoader.load('/textures/minecraft.png')
colorTexture.magFilter = THREE.NearestFilter
Hinweise für bessere Performance
- 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.
- Da der NearestFilter keine Interpolation macht sondern nur Pixel wegwirft bzw. verdoppelt, hat er auch eine etwas bessere Performance als andere Filter.
- 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.
- 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.
- Normal-Maps geben über die Farbe die Richtung der Normalen (=Oberfläche) vor. Das sollte exakt sein, deshalb hier lieber ein png Verwenden (verlustfrei).
colorTexture.generateMipmaps = false // NearestFilter doesn't need that
colorTexture.minFilter = THREE.NearestFilter
colorTexture.magFilter = THREE.NearestFilter
- 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.
Materials
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
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
MeshMatcapMaterial is a fantastic material because of how great it can look while being very performant.
- Performant
- simuliert Licht ohne Lichtquelle
- gut für's Modelling
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. 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.
// MATCAP
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture