Μια εξερεύνηση της αναδιαμόρφωσης εδάφους σε πραγματικό χρόνο μέσω αλληλεπιδράσεων χρηστών, χρησιμοποιώντας το React Three Fiber.
Σε αυτό το σεμινάριο, θα εξερευνήσουμε πώς να παραμορφώνουμε δυναμικά το έδαφος, μια δυνατότητα που χρησιμοποιείται ευρέως στα σύγχρονα παιχνίδια. Πριν από κάποιο καιρό, μάθαμε πώς να δημιουργούμε το PS1 jitter shader, κάνοντας ένα νοσταλγικό ταξίδι στα ρετρό γραφικά. Η μετάβαση από εκείνη τη ρετρό αίσθηση σε τεχνολογίες αιχμής ήταν συναρπαστική για μένα, και είμαι χαρούμενος που βλέπω τόσο μεγάλο ενδιαφέρον για αυτά τα θέματα.
Αυτό το σεμινάριο θα χωριστεί σε δύο μέρη. Στο πρώτο μέρος, θα επικεντρωθούμε στη Δυναμική Παραμόρφωση Εδάφους, εξερευνώντας πώς να δημιουργήσουμε και να χειριστούμε το έδαφος διαδραστικά. Στο δεύτερο μέρος, θα προχωρήσουμε ένα βήμα παραπέρα δημιουργώντας μια απεριόριστη ζώνη βάδισης χρησιμοποιώντας τα παραγόμενα κομμάτια, διατηρώντας παράλληλα την βέλτιστη απόδοση.
Κατασκευή Διαδραστικής Παραμόρφωσης Εδάφους Βήμα προς Βήμα
Αφού στήσουμε τη σκηνή, θα δημιουργήσουμε μια planeGeometry και θα εφαρμόσουμε την υφή χιονιού που αποκτήσαμε από το AmbientCG. Για να ενισχύσουμε τον ρεαλισμό, θα αυξήσουμε την τιμή του displacementScale, δημιουργώντας ένα πιο δυναμικό και ρεαλιστικό χιονισμένο περιβάλλον. Θα εμβαθύνουμε στα CHUNKs αργότερα στο σεμινάριο.
const [colorMap, normalMap, roughnessMap, aoMap, displacementMap] =
useTexture([
"/textures/snow/snow-color.jpg",
"/textures/snow/snow-normal-gl.jpg",
"/textures/snow/snow-roughness.jpg",
"/textures/snow/snow-ambientocclusion.jpg",
"/textures/snow/snow-displacement.jpg",
]);
return <mesh
rotation={[-Math.PI / 2, 0, 0]} // Rotate to make it horizontal
position={[chunk.x * CHUNK_SIZE, 0, chunk.z * CHUNK_SIZE]}
>
<planeGeometry
args={[
CHUNK_SIZE + CHUNK_OVERLAP * 2,
CHUNK_SIZE + CHUNK_OVERLAP * 2,
GRID_RESOLUTION,
GRID_RESOLUTION,
]}
/>
<meshStandardMaterial
map={colorMap}
normalMap={normalMap}
roughnessMap={roughnessMap}
aoMap={aoMap}
displacementMap={displacementMap}
displacementScale={2}
/>
</mesh>
))}
Αφού δημιουργήσουμε το planeGeometry, θα εξερευνήσουμε τη συνάρτηση deformMesh—τον πυρήνα αυτής της επίδειξης:
const deformMesh = useCallback(
(mesh, point) => {
if (!mesh) return;
// Retrieve neighboring chunks around the point of deformation.
const neighboringChunks = getNeighboringChunks(point, chunksRef);
// Temporary vector to hold vertex positions during calculations
const tempVertex = new THREE.Vector3();
// Array to keep track of geometries that require normal recomputation
const geometriesToUpdate = [];
// Iterate through each neighboring chunk to apply deformations
neighboringChunks.forEach((chunk) => {
const geometry = chunk.geometry;
// Validate that the chunk has valid geometry and position attributes
if (!geometry || !geometry.attributes || !geometry.attributes.position)
return;
const positionAttribute = geometry.attributes.position;
const vertices = positionAttribute.array;
// Flag to determine if the current chunk has been deformed
let hasDeformation = false;
// Loop through each vertex in the chunk's geometry
for (let i = 0; i < positionAttribute.count; i++) {
// Extract the current vertex's position from the array
tempVertex.fromArray(vertices, i * 3);
// Convert the vertex position from local to world coordinates
chunk.localToWorld(tempVertex);
// Calculate the distance between the vertex and the point of influence
const distance = tempVertex.distanceTo(point);
// Check if the vertex is within the deformation radius
if (distance < DEFORM_RADIUS) {
// Calculate the influence of the deformation based on distance.
// The closer the vertex is to the point, the greater the influence.
// Using a cubic falloff for a smooth transition.
const influence = Math.pow(
(DEFORM_RADIUS - distance) / DEFORM_RADIUS,
3
);
// Calculate the vertical offset (y-axis) to apply to the vertex.
// This creates a depression effect that simulates impact or footprint.
const yOffset = influence * 10;
tempVertex.y -= yOffset * Math.sin((distance / DEFORM_RADIUS) * Math.PI);
// Add a wave effect to the vertex's y-position.
// This simulates ripples or disturbances caused by the deformation.
tempVertex.y += WAVE_AMPLITUDE * Math.sin(WAVE_FREQUENCY * distance);
// Convert the modified vertex position back to local coordinates
chunk.worldToLocal(tempVertex);
// Update the vertex position in the geometry's position array
tempVertex.toArray(vertices, i * 3);
// Mark that this chunk has undergone deformation
hasDeformation = true;
}
}
// If any vertex in the chunk was deformed, update the geometry accordingly
if (hasDeformation) {
// Indicate that the position attribute needs to be updated
positionAttribute.needsUpdate = true;
// Add the geometry to the list for batch normal recomputation
geometriesToUpdate.push(geometry);
// Save the deformation state for potential future use or persistence
saveChunkDeformation(chunk);
}
});
// After processing all neighboring chunks, recompute the vertex normals
// for each affected geometry. This ensures that lighting and shading
// accurately reflect the new geometry after deformation.
if (geometriesToUpdate.length > 0) {
geometriesToUpdate.forEach((geometry) => geometry.computeVertexNormals());
}
},
[
getNeighboringChunks,
chunksRef,
saveChunkDeformation,
]
);
Πρόσθεσα το μέρος «Προσθήκη ενός υποτονικού εφέ κύματος για οπτική ποικιλία» σε αυτή τη λειτουργία για να αντιμετωπίσω ένα πρόβλημα που περιόριζε την φυσική εμφάνιση του χιονιού καθώς σχηματιζόταν η πίστα. Οι άκρες του χιονιού έπρεπε να προεξέχουν ελαφρώς. Έτσι έμοιαζε πριν το προσθέσω:
Αφού δημιουργήσουμε τη συνάρτηση deformMesh, θα καθορίσουμε πού να τη χρησιμοποιήσουμε για να ολοκληρώσουμε την Δυναμική Παραμόρφωση Εδάφους. Συγκεκριμένα, θα το ενσωματώσουμε στο useFrame, επιλέγοντας τα οστά του δεξιού και αριστερού ποδιού στην κινούμενη εικόνα του χαρακτήρα και εξάγοντας τις θέσεις τους από το matrixWorld.
useFrame((state, delta) => {
// Other codes...
// Get the bones representing the character's left and right feet
const leftFootBone = characterRef.current.getObjectByName("mixamorigLeftFoot");
const rightFootBone = characterRef.current.getObjectByName("mixamorigRightFoot");
if (leftFootBone) {
// Get the world position of the left foot bone
tempVector.setFromMatrixPosition(leftFootBone.matrixWorld);
// Apply terrain deformation at the position of the left foot
deformMesh(activeChunk, tempVector);
}
if (rightFootBone) {
// Get the world position of the right foot bone
tempVector.setFromMatrixPosition(rightFootBone.matrixWorld);
// Apply terrain deformation at the position of the right foot
deformMesh(activeChunk, tempVector);
}
// Other codes...
});
Και να το: μια ομαλή, δυναμική παραμόρφωση σε δράση!
Απεριόριστο Περπάτημα με CHUNKs
Στον κώδικα που έχουμε εξερευνήσει μέχρι τώρα, ίσως έχετε παρατηρήσει τα μέρη CHUNK. Με απλά λόγια, δημιουργούμε χιονισμένα μπλοκ σε διάταξη 3×3. Για να διασφαλίσουμε ότι ο χαρακτήρας παραμένει πάντα στο κέντρο, αφαιρούμε τα προηγούμενα CHUNKs με βάση την κατεύθυνση στην οποία κινείται ο χαρακτήρας και δημιουργούμε νέα CHUNKs μπροστά στην ίδια κατεύθυνση. Μπορείτε να δείτε αυτή τη διαδικασία σε δράση στο GIF παρακάτω. Ωστόσο, αυτή η μέθοδος παρουσίασε αρκετές προκλήσεις.
Προβλήματα:
- Εμφανίζονται κενά στις αρθρώσεις μεταξύ των CHUNKs
- Οι υπολογισμοί κορυφών διαταράσσονται στις αρθρώσεις
- Οι διαδρομές από το προηγούμενο CHUNK εξαφανίζονται αμέσως κατά τη μετάβαση σε ένα νέο CHUNK.
Λύσεις:
- getChunkKey
// Generates a unique key for a chunk based on its current position.
// Uses globally accessible CHUNK_SIZE for calculations.
// Purpose: Ensures each chunk can be uniquely identified and managed in a Map.
const deformedChunksMapRef = useRef(new Map());
const getChunkKey = () =>
`${Math.round(currentChunk.position.x / CHUNK_SIZE)},${Math.round(currentChunk.position.z / CHUNK_SIZE)}`;
2. saveChunkDeformation
// Saves the deformation state of the current chunk by storing its vertex positions.
// Purpose: Preserves the deformation of a chunk for later retrieval.
const saveChunkDeformation = () => {
if (!currentChunk) return;
// Generate the unique key for this chunk
const chunkKey = getChunkKey();
// Save the current vertex positions into the deformation map
const position = currentChunk.geometry.attributes.position;
deformedChunksMapRef.current.set(
chunkKey,
new Float32Array(position.array)
);
};
3. loadChunkDeformation
// Restores the deformation state of the current chunk, if previously saved.
// Purpose: Ensures that deformed chunks retain their state when repositioned.
const loadChunkDeformation = () => {
if (!currentChunk) return false;
// Retrieve the unique key for this chunk
const chunkKey = getChunkKey();
// Get the saved deformation data for this chunk
const savedDeformation = deformedChunksMapRef.current.get(chunkKey);
if (savedDeformation) {
const position = currentChunk.geometry.attributes.position;
// Restore the saved vertex positions
position.array.set(savedDeformation);
position.needsUpdate = true;
currentChunk.geometry.computeVertexNormals();
return true;
}
return false;
};
4. getNeighboringChunks
// Finds chunks that are close to the current position.
// Purpose: Limits deformation operations to only relevant chunks, improving performance.
const getNeighboringChunks = () => {
return chunksRef.current.filter((chunk) => {
// Calculate the distance between the chunk and the current position
const distance = new THREE.Vector2(
chunk.position.x - currentPosition.x,
chunk.position.z - currentPosition.z
).length();
// Include chunks within the deformation radius
return distance < CHUNK_SIZE + DEFORM_RADIUS;
});
};
5. recycleDistantChunks
// Recycles chunks that are too far from the character by resetting their deformation state.
// Purpose: Prepares distant chunks for reuse, maintaining efficient resource usage.
const recycleDistantChunks = () => {
chunksRef.current.forEach((chunk) => {
// Calculate the distance between the chunk and the character
const distance = new THREE.Vector2(
chunk.position.x - characterPosition.x,
chunk.position.z - characterPosition.z
).length();
// If the chunk is beyond the unload distance, reset its deformation
if (distance > CHUNK_UNLOAD_DISTANCE) {
const geometry = chunk.geometry;
const originalPosition = geometry.userData.originalPosition;
if (originalPosition) {
// Reset vertex positions to their original state
geometry.attributes.position.array.set(originalPosition);
geometry.attributes.position.needsUpdate = true;
// Recompute normals for correct lighting
geometry.computeVertexNormals();
}
// Remove the deformation data for this chunk
const chunkKey = getChunkKey(chunk.position.x, chunk.position.z);
deformedChunksMapRef.current.delete(chunkKey);
}
});
};
Με αυτές τις λειτουργίες, επιλύσαμε τα προβλήματα με τα CHUNKs, επιτυγχάνοντας την εμφάνιση που επιδιώκαμε.
Συμπεράσματα
Σε αυτό το σεμινάριο, καλύψαμε τα βασικά της δημιουργίας Δυναμικής Παραμόρφωσης Εδάφους χρησιμοποιώντας το React Three Fiber. Από την υλοποίηση ρεαλιστικής παραμόρφωσης χιονιού μέχρι τη διαχείριση των CHUNKs για απεριόριστες ζώνες περπατήματος, εξερευνήσαμε ορισμένες βασικές τεχνικές και αντιμετωπίσαμε κοινές προκλήσεις κατά τη διάρκεια της πορείας.
Ενώ αυτό το έργο επικεντρώθηκε στα βασικά, παρέχει ένα σταθερό σημείο εκκίνησης για την ανάπτυξη πιο σύνθετων χαρακτηριστικών, όπως προηγμένοι έλεγχοι χαρακτήρων ή δυναμικά περιβάλλοντα. Οι έννοιες της χειρισμού κορυφών και της διαχείρισης τμημάτων είναι ευέλικτες και μπορούν να εφαρμοστούν σε πολλά άλλα δημιουργικά έργα.
Σας ευχαριστώ που παρακολουθήσατε, και ελπίζω αυτό το σεμινάριο να σας εμπνεύσει να δημιουργήσετε τις δικές σας διαδραστικές 3D εμπειρίες! Αν έχετε οποιαδήποτε ερώτηση ή σχόλιο, μη διστάσετε να επικοινωνήσετε μαζί μου. Καλή κωδικοποίηση!