From a31be29e9bde1feb08ff22407c6420036f37e174 Mon Sep 17 00:00:00 2001 From: Bo Kou Date: Thu, 4 Jul 2024 18:10:43 +0800 Subject: [PATCH] docs: add docs and examples (#2127) --- docs/_meta.json | 86 ++ docs/animation/animator.md | 119 +++ docs/animation/animatorController.md | 201 +++++ docs/animation/clip-for-artist.md | 102 +++ docs/animation/clip.md | 227 +++++ docs/animation/overview.md | 40 + docs/animation/sprite-sheet.md | 32 + docs/animation/state-machine.md | 166 ++++ docs/animation/system.md | 46 + docs/art/bake-blender.md | 86 ++ docs/art/bake-c4d.md | 56 ++ docs/art/lottie.md | 38 + docs/assets/build.md | 337 ++++++++ docs/assets/gc.md | 37 + docs/assets/interface.md | 91 ++ docs/assets/load.md | 143 ++++ docs/assets/overall.md | 25 + docs/assets/type.md | 44 + docs/core/canvas.md | 119 +++ docs/core/clone.md | 105 +++ docs/core/component.md | 98 +++ docs/core/engine.md | 94 ++ docs/core/entity.md | 122 +++ docs/core/math.md | 432 ++++++++++ docs/core/scene.md | 159 ++++ docs/core/space.md | 64 ++ docs/core/time.md | 20 + docs/core/transform.md | 131 +++ docs/device/restore.md | 85 ++ docs/graphics/2D/2d.md | 28 + docs/graphics/2D/lottie.md | 157 ++++ docs/graphics/2D/spine.md | 161 ++++ docs/graphics/2D/sprite.md | 65 ++ docs/graphics/2D/spriteAtlas.md | 163 ++++ docs/graphics/2D/spriteMask.md | 78 ++ docs/graphics/2D/spriteRenderer.md | 96 +++ docs/graphics/2D/text.md | 185 ++++ docs/graphics/_meta.json | 35 + docs/graphics/background/background.md | 21 + docs/graphics/background/sky.md | 96 +++ docs/graphics/background/solidColor.md | 36 + docs/graphics/background/texture.md | 52 ++ docs/graphics/camera/camera.md | 56 ++ docs/graphics/camera/component.md | 138 +++ docs/graphics/camera/control.md | 60 ++ docs/graphics/camera/depthTexture.md | 14 + docs/graphics/camera/multiCamera.md | 17 + docs/graphics/light/ambient.md | 55 ++ docs/graphics/light/bake.md | 43 + docs/graphics/light/directional.md | 37 + docs/graphics/light/light.md | 33 + docs/graphics/light/point.md | 34 + docs/graphics/light/shadow.md | 80 ++ docs/graphics/light/spot.md | 40 + docs/graphics/material/composition.md | 52 ++ docs/graphics/material/editor.md | 27 + docs/graphics/material/material.md | 17 + docs/graphics/material/script.md | 83 ++ docs/graphics/mesh/bufferMesh.md | 192 +++++ docs/graphics/mesh/mesh.md | 48 ++ docs/graphics/mesh/modelMesh.md | 211 +++++ docs/graphics/mesh/primitiveMesh.md | 120 +++ docs/graphics/model/assets.md | 61 ++ docs/graphics/model/glTF.md | 138 +++ docs/graphics/model/importGlTF.md | 38 + docs/graphics/model/model.md | 23 + docs/graphics/model/opt.md | 24 + docs/graphics/model/restoration.md | 137 +++ docs/graphics/model/use.md | 133 +++ .../particle/renderer-animation-module.md | 21 + .../particle/renderer-color-module.md | 23 + .../particle/renderer-emission-module.md | 40 + .../graphics/particle/renderer-main-module.md | 39 + .../particle/renderer-rotation-module.md | 20 + .../graphics/particle/renderer-size-module.md | 41 + .../particle/renderer-velocity-module.md | 22 + docs/graphics/particle/renderer.md | 95 +++ docs/graphics/renderer/meshRenderer.md | 60 ++ docs/graphics/renderer/order.md | 63 ++ docs/graphics/renderer/renderer.md | 61 ++ docs/graphics/renderer/skinnedMeshRenderer.md | 30 + docs/graphics/shader/blinnPhong.md | 32 + docs/graphics/shader/custom.md | 311 +++++++ docs/graphics/shader/lab.md | 484 +++++++++++ docs/graphics/shader/pbr.md | 72 ++ docs/graphics/shader/shader.md | 65 ++ docs/graphics/shader/unlit.md | 69 ++ docs/graphics/texture/2d.md | 114 +++ docs/graphics/texture/compression.md | 44 + docs/graphics/texture/cube.md | 53 ++ docs/graphics/texture/rtt.md | 54 ++ docs/graphics/texture/texture.md | 155 ++++ docs/input/framebuffer-picker.md | 43 + docs/input/keyboard.md | 66 ++ docs/input/pointer.md | 137 +++ docs/input/wheel.md | 16 + docs/interface/hierarchy.md | 113 +++ docs/interface/inspector.md | 72 ++ docs/interface/intro.md | 35 + docs/interface/menu.md | 36 + docs/interface/shortcut.md | 11 + docs/interface/viewport.md | 85 ++ docs/performance/scene-standard.md | 39 + docs/performance/stats.md | 20 + docs/physics/collider.md | 92 ++ docs/physics/controller.md | 49 ++ docs/physics/debug.md | 18 + docs/physics/joint-basic.md | 43 + docs/physics/manager.md | 85 ++ docs/physics/overall.md | 49 ++ docs/quick-start/core-concept.md | 205 +++++ docs/quick-start/flappy-bird.md | 781 +++++++++++++++++ docs/quick-start/overview.md | 112 +++ docs/script/attributes.md | 71 ++ docs/script/class.md | 219 +++++ docs/script/communication.md | 65 ++ docs/script/create.md | 15 + docs/script/edit.md | 78 ++ docs/xr/camera.md | 35 + docs/xr/compatibility.md | 53 ++ docs/xr/features.md | 120 +++ docs/xr/input.md | 57 ++ docs/xr/manager.md | 46 + docs/xr/overall.md | 22 + docs/xr/session.md | 37 + docs/xr/start.md | 80 ++ examples/AStar.ts | 563 ++++++++++++ examples/CSS-DOM.ts | 92 ++ examples/ambient-light.ts | 110 +++ examples/animation-customAnimationClip.ts | 235 +++++ examples/animation-event.ts | 107 +++ examples/animation-sprite.ts | 114 +++ examples/animation-stateMachineScript.ts | 113 +++ examples/assets-gc.ts | 51 ++ examples/background/gui.ts | 82 ++ examples/background/index.ts | 59 ++ examples/background/material-list.ts | 30 + examples/benchmark-animation.ts | 73 ++ examples/benchmark-particle.ts | 457 ++++++++++ examples/benchmark-video.ts | 137 +++ examples/blend-mode.ts | 68 ++ examples/blinn-phong.ts | 136 +++ examples/bounding-box.ts | 99 +++ examples/box-selection.ts | 50 ++ examples/buffer-mesh-independent.ts | 187 ++++ examples/buffer-mesh-instance.ts | 191 +++++ examples/buffer-mesh-interleaved.ts | 123 +++ .../buffer-mesh-particle-shader-effect.ts | 280 ++++++ examples/camera-depth-texture.ts | 139 +++ examples/cascaded-shadow.ts | 284 ++++++ examples/compressed-texture.ts | 96 +++ examples/controls-free.ts | 73 ++ examples/culling-mask.ts | 79 ++ examples/custom-mesh.ts | 351 ++++++++ examples/device-restore.ts | 107 +++ examples/draw-lines.ts | 326 +++++++ examples/filter-mode.ts | 74 ++ examples/flappy-bird.ts | 806 ++++++++++++++++++ examples/framebuffer-picker.ts | 164 ++++ examples/gizmo.ts | 238 ++++++ examples/gltf-basic.ts | 35 + examples/gltf-loader.ts | 467 ++++++++++ examples/hdr-loader.ts | 50 ++ examples/ibl-baker.ts | 229 +++++ examples/infinity-grid.ts | 39 + examples/input-glTF.ts | 89 ++ examples/input-glTFMerge.ts | 81 ++ examples/input-log.ts | 57 ++ examples/input-pointer.ts | 165 ++++ examples/input-pointerButton.ts | 71 ++ examples/input-pointerRaycast.ts | 193 +++++ examples/input-wheel.ts | 91 ++ examples/light-type.ts | 131 +++ examples/lines.ts | 106 +++ examples/lite-collision-detection.ts | 138 +++ examples/lite-raycast.ts | 160 ++++ examples/lottie-3d-rotation.ts | 41 + examples/lottie-benchmark.ts | 45 + examples/lottie-clips.ts | 41 + examples/lottie.ts | 36 + examples/material-head-distort.ts | 570 +++++++++++++ examples/model-mesh.ts | 176 ++++ examples/mrt.ts | 168 ++++ examples/multi-camera.ts | 120 +++ examples/multi-scene.ts | 166 ++++ examples/multi-viewport.ts | 79 ++ examples/obj-loader.ts | 78 ++ examples/ortho-control.ts | 53 ++ examples/ortho-switch.ts | 60 ++ examples/outline-multi-pass.ts | 323 +++++++ examples/outline-postprocess.ts | 120 +++ examples/particle-dream.ts | 365 ++++++++ examples/particle-fire.ts | 455 ++++++++++ examples/pbr-anisotropy.ts | 88 ++ examples/pbr-base.ts | 80 ++ examples/pbr-clearcoat.ts | 69 ++ examples/pbr-helmet.ts | 71 ++ examples/physics-debug-draw.ts | 158 ++++ examples/physx-attractor.ts | 229 +++++ examples/physx-collision-detection.ts | 151 ++++ examples/physx-compound.ts | 200 +++++ examples/physx-controller.ts | 562 ++++++++++++ examples/physx-joint-basic.ts | 263 ++++++ examples/physx-raycast.ts | 347 ++++++++ examples/physx-select.ts | 349 ++++++++ examples/planar-shadow.ts | 80 ++ examples/primitive-mesh.ts | 174 ++++ examples/render-target.ts | 125 +++ examples/renderer-cull.ts | 91 ++ examples/scene-basic.ts | 53 ++ examples/scene-fog.ts | 151 ++++ examples/screenshot.ts | 175 ++++ examples/script-basic.ts | 55 ++ examples/shader-lab-multi-pass.ts | 265 ++++++ examples/shader-lab-simple.ts | 170 ++++ examples/shader-lab.ts | 246 ++++++ examples/shader-replacement.ts | 218 +++++ examples/shader-water.ts | 261 ++++++ examples/shadow-basic.ts | 58 ++ examples/shadow-fade.ts | 72 ++ examples/skeleton-animation-additive.ts | 110 +++ examples/skeleton-animation-blendShape.ts | 54 ++ examples/skeleton-animation-crossfade.ts | 89 ++ .../skeleton-animation-customBlendShape.ts | 91 ++ examples/skeleton-animation-play.ts | 70 ++ examples/skeleton-animation-reuse.ts | 89 ++ examples/skeleton-viewer.ts | 46 + examples/sky-procedural.ts | 110 +++ examples/spine-animation.ts | 38 + examples/spine-change-attachment.ts | 79 ++ examples/spine-hack-slot-texture.ts | 141 +++ examples/spine-performance.ts | 47 + examples/spine-skin-change.ts | 66 ++ examples/sprite-atlas.ts | 117 +++ examples/sprite-color.ts | 62 ++ examples/sprite-drawMode.ts | 316 +++++++ examples/sprite-flip.ts | 68 ++ examples/sprite-mask.ts | 160 ++++ examples/sprite-material-blur.ts | 166 ++++ examples/sprite-material-dissolve.ts | 220 +++++ examples/sprite-material-glitch-rgbSplit.ts | 149 ++++ examples/sprite-pivot.ts | 115 +++ examples/sprite-region.ts | 79 ++ examples/sprite-renderer.ts | 99 +++ examples/sprite-sheetAnimation.ts | 150 ++++ examples/sprite-size.ts | 77 ++ examples/text-barrage.ts | 129 +++ examples/text-ktv-subtitle.ts | 202 +++++ examples/text-renderer-font.ts | 68 ++ examples/text-renderer.ts | 96 +++ examples/text-wrap-alignment.ts | 92 ++ examples/texture-aniso.ts | 59 ++ examples/texture-mipmap.ts | 76 ++ examples/tiling-offset.ts | 143 ++++ examples/transform-basic.ts | 104 +++ examples/transparent-shadow.ts | 199 +++++ examples/unlit-material.ts | 57 ++ examples/video-background.ts | 124 +++ examples/video-transparent.ts | 150 ++++ examples/wrap-mode.ts | 78 ++ examples/xr-ar-imageTracking.ts | 272 ++++++ examples/xr-ar-planeTracking.ts | 297 +++++++ examples/xr-ar-simple.ts | 89 ++ examples/xr-vr-shotball.ts | 368 ++++++++ 264 files changed, 32626 insertions(+) create mode 100644 docs/_meta.json create mode 100644 docs/animation/animator.md create mode 100644 docs/animation/animatorController.md create mode 100644 docs/animation/clip-for-artist.md create mode 100644 docs/animation/clip.md create mode 100644 docs/animation/overview.md create mode 100644 docs/animation/sprite-sheet.md create mode 100644 docs/animation/state-machine.md create mode 100644 docs/animation/system.md create mode 100644 docs/art/bake-blender.md create mode 100644 docs/art/bake-c4d.md create mode 100644 docs/art/lottie.md create mode 100644 docs/assets/build.md create mode 100644 docs/assets/gc.md create mode 100644 docs/assets/interface.md create mode 100644 docs/assets/load.md create mode 100644 docs/assets/overall.md create mode 100644 docs/assets/type.md create mode 100644 docs/core/canvas.md create mode 100644 docs/core/clone.md create mode 100644 docs/core/component.md create mode 100644 docs/core/engine.md create mode 100644 docs/core/entity.md create mode 100644 docs/core/math.md create mode 100644 docs/core/scene.md create mode 100644 docs/core/space.md create mode 100644 docs/core/time.md create mode 100644 docs/core/transform.md create mode 100644 docs/device/restore.md create mode 100644 docs/graphics/2D/2d.md create mode 100644 docs/graphics/2D/lottie.md create mode 100644 docs/graphics/2D/spine.md create mode 100644 docs/graphics/2D/sprite.md create mode 100644 docs/graphics/2D/spriteAtlas.md create mode 100644 docs/graphics/2D/spriteMask.md create mode 100644 docs/graphics/2D/spriteRenderer.md create mode 100644 docs/graphics/2D/text.md create mode 100644 docs/graphics/_meta.json create mode 100644 docs/graphics/background/background.md create mode 100644 docs/graphics/background/sky.md create mode 100644 docs/graphics/background/solidColor.md create mode 100644 docs/graphics/background/texture.md create mode 100644 docs/graphics/camera/camera.md create mode 100644 docs/graphics/camera/component.md create mode 100644 docs/graphics/camera/control.md create mode 100644 docs/graphics/camera/depthTexture.md create mode 100644 docs/graphics/camera/multiCamera.md create mode 100644 docs/graphics/light/ambient.md create mode 100644 docs/graphics/light/bake.md create mode 100644 docs/graphics/light/directional.md create mode 100644 docs/graphics/light/light.md create mode 100644 docs/graphics/light/point.md create mode 100644 docs/graphics/light/shadow.md create mode 100644 docs/graphics/light/spot.md create mode 100644 docs/graphics/material/composition.md create mode 100644 docs/graphics/material/editor.md create mode 100644 docs/graphics/material/material.md create mode 100644 docs/graphics/material/script.md create mode 100644 docs/graphics/mesh/bufferMesh.md create mode 100644 docs/graphics/mesh/mesh.md create mode 100644 docs/graphics/mesh/modelMesh.md create mode 100644 docs/graphics/mesh/primitiveMesh.md create mode 100644 docs/graphics/model/assets.md create mode 100644 docs/graphics/model/glTF.md create mode 100644 docs/graphics/model/importGlTF.md create mode 100644 docs/graphics/model/model.md create mode 100644 docs/graphics/model/opt.md create mode 100644 docs/graphics/model/restoration.md create mode 100644 docs/graphics/model/use.md create mode 100644 docs/graphics/particle/renderer-animation-module.md create mode 100644 docs/graphics/particle/renderer-color-module.md create mode 100644 docs/graphics/particle/renderer-emission-module.md create mode 100644 docs/graphics/particle/renderer-main-module.md create mode 100644 docs/graphics/particle/renderer-rotation-module.md create mode 100644 docs/graphics/particle/renderer-size-module.md create mode 100644 docs/graphics/particle/renderer-velocity-module.md create mode 100644 docs/graphics/particle/renderer.md create mode 100644 docs/graphics/renderer/meshRenderer.md create mode 100644 docs/graphics/renderer/order.md create mode 100644 docs/graphics/renderer/renderer.md create mode 100644 docs/graphics/renderer/skinnedMeshRenderer.md create mode 100644 docs/graphics/shader/blinnPhong.md create mode 100644 docs/graphics/shader/custom.md create mode 100644 docs/graphics/shader/lab.md create mode 100644 docs/graphics/shader/pbr.md create mode 100644 docs/graphics/shader/shader.md create mode 100644 docs/graphics/shader/unlit.md create mode 100644 docs/graphics/texture/2d.md create mode 100644 docs/graphics/texture/compression.md create mode 100644 docs/graphics/texture/cube.md create mode 100644 docs/graphics/texture/rtt.md create mode 100644 docs/graphics/texture/texture.md create mode 100644 docs/input/framebuffer-picker.md create mode 100644 docs/input/keyboard.md create mode 100644 docs/input/pointer.md create mode 100644 docs/input/wheel.md create mode 100644 docs/interface/hierarchy.md create mode 100644 docs/interface/inspector.md create mode 100644 docs/interface/intro.md create mode 100644 docs/interface/menu.md create mode 100644 docs/interface/shortcut.md create mode 100644 docs/interface/viewport.md create mode 100644 docs/performance/scene-standard.md create mode 100644 docs/performance/stats.md create mode 100644 docs/physics/collider.md create mode 100644 docs/physics/controller.md create mode 100644 docs/physics/debug.md create mode 100644 docs/physics/joint-basic.md create mode 100644 docs/physics/manager.md create mode 100644 docs/physics/overall.md create mode 100644 docs/quick-start/core-concept.md create mode 100644 docs/quick-start/flappy-bird.md create mode 100644 docs/quick-start/overview.md create mode 100644 docs/script/attributes.md create mode 100644 docs/script/class.md create mode 100644 docs/script/communication.md create mode 100644 docs/script/create.md create mode 100644 docs/script/edit.md create mode 100644 docs/xr/camera.md create mode 100644 docs/xr/compatibility.md create mode 100644 docs/xr/features.md create mode 100644 docs/xr/input.md create mode 100644 docs/xr/manager.md create mode 100644 docs/xr/overall.md create mode 100644 docs/xr/session.md create mode 100644 docs/xr/start.md create mode 100644 examples/AStar.ts create mode 100644 examples/CSS-DOM.ts create mode 100644 examples/ambient-light.ts create mode 100644 examples/animation-customAnimationClip.ts create mode 100644 examples/animation-event.ts create mode 100644 examples/animation-sprite.ts create mode 100644 examples/animation-stateMachineScript.ts create mode 100644 examples/assets-gc.ts create mode 100644 examples/background/gui.ts create mode 100644 examples/background/index.ts create mode 100644 examples/background/material-list.ts create mode 100644 examples/benchmark-animation.ts create mode 100644 examples/benchmark-particle.ts create mode 100644 examples/benchmark-video.ts create mode 100644 examples/blend-mode.ts create mode 100644 examples/blinn-phong.ts create mode 100644 examples/bounding-box.ts create mode 100644 examples/box-selection.ts create mode 100644 examples/buffer-mesh-independent.ts create mode 100644 examples/buffer-mesh-instance.ts create mode 100644 examples/buffer-mesh-interleaved.ts create mode 100644 examples/buffer-mesh-particle-shader-effect.ts create mode 100644 examples/camera-depth-texture.ts create mode 100644 examples/cascaded-shadow.ts create mode 100644 examples/compressed-texture.ts create mode 100644 examples/controls-free.ts create mode 100644 examples/culling-mask.ts create mode 100644 examples/custom-mesh.ts create mode 100644 examples/device-restore.ts create mode 100644 examples/draw-lines.ts create mode 100644 examples/filter-mode.ts create mode 100644 examples/flappy-bird.ts create mode 100644 examples/framebuffer-picker.ts create mode 100644 examples/gizmo.ts create mode 100644 examples/gltf-basic.ts create mode 100644 examples/gltf-loader.ts create mode 100644 examples/hdr-loader.ts create mode 100644 examples/ibl-baker.ts create mode 100644 examples/infinity-grid.ts create mode 100644 examples/input-glTF.ts create mode 100644 examples/input-glTFMerge.ts create mode 100644 examples/input-log.ts create mode 100644 examples/input-pointer.ts create mode 100644 examples/input-pointerButton.ts create mode 100644 examples/input-pointerRaycast.ts create mode 100644 examples/input-wheel.ts create mode 100644 examples/light-type.ts create mode 100644 examples/lines.ts create mode 100644 examples/lite-collision-detection.ts create mode 100644 examples/lite-raycast.ts create mode 100644 examples/lottie-3d-rotation.ts create mode 100644 examples/lottie-benchmark.ts create mode 100644 examples/lottie-clips.ts create mode 100644 examples/lottie.ts create mode 100644 examples/material-head-distort.ts create mode 100644 examples/model-mesh.ts create mode 100644 examples/mrt.ts create mode 100644 examples/multi-camera.ts create mode 100644 examples/multi-scene.ts create mode 100644 examples/multi-viewport.ts create mode 100644 examples/obj-loader.ts create mode 100644 examples/ortho-control.ts create mode 100644 examples/ortho-switch.ts create mode 100644 examples/outline-multi-pass.ts create mode 100644 examples/outline-postprocess.ts create mode 100644 examples/particle-dream.ts create mode 100644 examples/particle-fire.ts create mode 100644 examples/pbr-anisotropy.ts create mode 100644 examples/pbr-base.ts create mode 100644 examples/pbr-clearcoat.ts create mode 100644 examples/pbr-helmet.ts create mode 100644 examples/physics-debug-draw.ts create mode 100644 examples/physx-attractor.ts create mode 100644 examples/physx-collision-detection.ts create mode 100644 examples/physx-compound.ts create mode 100644 examples/physx-controller.ts create mode 100644 examples/physx-joint-basic.ts create mode 100644 examples/physx-raycast.ts create mode 100644 examples/physx-select.ts create mode 100644 examples/planar-shadow.ts create mode 100644 examples/primitive-mesh.ts create mode 100644 examples/render-target.ts create mode 100644 examples/renderer-cull.ts create mode 100644 examples/scene-basic.ts create mode 100644 examples/scene-fog.ts create mode 100644 examples/screenshot.ts create mode 100644 examples/script-basic.ts create mode 100644 examples/shader-lab-multi-pass.ts create mode 100644 examples/shader-lab-simple.ts create mode 100644 examples/shader-lab.ts create mode 100644 examples/shader-replacement.ts create mode 100644 examples/shader-water.ts create mode 100644 examples/shadow-basic.ts create mode 100644 examples/shadow-fade.ts create mode 100644 examples/skeleton-animation-additive.ts create mode 100644 examples/skeleton-animation-blendShape.ts create mode 100644 examples/skeleton-animation-crossfade.ts create mode 100644 examples/skeleton-animation-customBlendShape.ts create mode 100644 examples/skeleton-animation-play.ts create mode 100644 examples/skeleton-animation-reuse.ts create mode 100644 examples/skeleton-viewer.ts create mode 100644 examples/sky-procedural.ts create mode 100644 examples/spine-animation.ts create mode 100644 examples/spine-change-attachment.ts create mode 100644 examples/spine-hack-slot-texture.ts create mode 100644 examples/spine-performance.ts create mode 100644 examples/spine-skin-change.ts create mode 100644 examples/sprite-atlas.ts create mode 100644 examples/sprite-color.ts create mode 100644 examples/sprite-drawMode.ts create mode 100644 examples/sprite-flip.ts create mode 100644 examples/sprite-mask.ts create mode 100644 examples/sprite-material-blur.ts create mode 100644 examples/sprite-material-dissolve.ts create mode 100644 examples/sprite-material-glitch-rgbSplit.ts create mode 100644 examples/sprite-pivot.ts create mode 100644 examples/sprite-region.ts create mode 100644 examples/sprite-renderer.ts create mode 100644 examples/sprite-sheetAnimation.ts create mode 100644 examples/sprite-size.ts create mode 100644 examples/text-barrage.ts create mode 100644 examples/text-ktv-subtitle.ts create mode 100644 examples/text-renderer-font.ts create mode 100644 examples/text-renderer.ts create mode 100644 examples/text-wrap-alignment.ts create mode 100644 examples/texture-aniso.ts create mode 100644 examples/texture-mipmap.ts create mode 100644 examples/tiling-offset.ts create mode 100644 examples/transform-basic.ts create mode 100644 examples/transparent-shadow.ts create mode 100644 examples/unlit-material.ts create mode 100644 examples/video-background.ts create mode 100644 examples/video-transparent.ts create mode 100644 examples/wrap-mode.ts create mode 100644 examples/xr-ar-imageTracking.ts create mode 100644 examples/xr-ar-planeTracking.ts create mode 100644 examples/xr-ar-simple.ts create mode 100644 examples/xr-vr-shotball.ts diff --git a/docs/_meta.json b/docs/_meta.json new file mode 100644 index 000000000..aee7078f4 --- /dev/null +++ b/docs/_meta.json @@ -0,0 +1,86 @@ +{ + "quick-start": { + "title": "快速开始", + "theme": { + "collapse": true + } + }, + "interface": { + "title": "界面", + "theme": { + "collapse": true + } + }, + "core": { + "title": "核心", + "theme": { + "collapse": true + } + }, + "assets": { + "title": "资产", + "theme": { + "collapse": true + } + }, + "graphics": { + "title": "图形", + "theme": { + "collapse": true + } + }, + "animation": { + "title": "动画", + "theme": { + "collapse": true + } + }, + "physics": { + "title": "物理", + "theme": { + "collapse": true + } + }, + "script": { + "title": "脚本", + "theme": { + "collapse": true + } + }, + "xr": { + "title": "XR", + "theme": { + "collapse": true + } + }, + "input": { + "title": "输入", + "theme": { + "collapse": true + } + }, + "device": { + "title": "设备", + "theme": { + "collapse": true + } + }, + "performance": { + "title": "性能", + "theme": { + "collapse": true + } + }, + "art": { + "title": "美术", + "theme": { + "collapse": true + } + }, + "how-to-write-docs": { + "title": "文档编写指南" + }, + "en": { + "display": "hidden" + } +} diff --git a/docs/animation/animator.md b/docs/animation/animator.md new file mode 100644 index 000000000..56576a0da --- /dev/null +++ b/docs/animation/animator.md @@ -0,0 +1,119 @@ +--- +order: 3 +title: 动画控制组件 +type: 动画 +label: Animation +--- + +动画控制组件([Animator](/apis/core/#Animator))的作用是读取[动画控制器](/docs/animation-animatorController)([AnimatorController](/apis/core/#AnimatorController))的数据,并播放其内容。 + +### 参数说明 + +| 属性 | 功能说明 | +| :----------------- | :----------------------------- | +| animatorController | 绑定 `AnimatorController` 资产 | + +## 编辑器使用 + +1. 当我们把模型拖入到场景中,模型以初始姿态展示出来,但是并不会播放任何动画,我们需要在模型实体上添加动画控制组件([Animator](/apis/core/#Animator)) + +![2](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*kuSLTaxomrUAAAAAAAAAAAAADsJ_AQ/original) + +2. 动画控制组件([Animator](/apis/core/#Animator))需要绑定一个 [动画控制器](/docs/animation-animatorController) 资产,我们创建并绑定 + +![3](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*irT7SZvw4N8AAAAAAAAAAAAADsJ_AQ/original) + +![4](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*VtX3RJR8kdMAAAAAAAAAAAAADsJ_AQ/original) + +3. 至此你在导出的项目中就可以通过 `animator.play` 播放[动画控制器](/docs/animation-animatorController)中的动画了。 + +如果你没有为实体添加 动画控制组件([Animator](/apis/core/#Animator))的话 Galacean Engine 会为你默认创建一个并且 [动画控制器](/docs/animation-animatorController) 中默认添加了模型的所有动画片段,当然除此以外你可以通过 [动画控制器](/docs/animation-animatorController) 实现更多的功能。 + +## 脚本使用 + +> 在使用脚本之前,最好阅读[动画系统构成](/docs/animation-system)文档,以帮助你更好的了解动画系统的运行逻辑 + +### 播放动画 + +在加载 GLTF 模型后引擎会自动为模型添加一个 Animator 组件,并将模型中的动画片段加入其中。可以直接在模型的根实体上获取 Animator 组件,并播放指定动画。 + +```typescript +engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play("run"); + }); +``` + +#### 控制播放速度 + +你可以通过 [speed](/apis/core/#Animator-speed)  属性来控制动画的播放速度。 `speed`  默认值为 `1.0` ,值越大播放的越快,越小播放的越慢。当值为负数时,进行倒播。 + +```typescript +animator.speed = 2.0; +``` + +#### 暂停/恢复播放 + +你可以通过设置 Animator 的 [enabled](/apis/core/#Animator-enabled) 来控制动画的暂停和播放. + +```typescript +// 暂停 +animator.enabled = false; +// 恢复 +animator.enabled = true; +``` + +如果你只想针对某一个动画状态进行暂停,可以通过将它的速度设置为 0 来实现。 + +```typescript +const state = animator.findAnimatorState("xxx"); +state.speed = 0; +``` + +#### 播放指定动画状态 + + + +你可以使用 [play](/apis/core/#Animator-play)  方法来播放指定的 AnimatorState。参数为 AnimatorState 的`name`,其他参数说明详见[API 文档](/apis/core/#Animator-play)。 + +```typescript +animator.play("run"); +``` + +如果需要在动画中的某一时刻开始播放可以通过以下方式 + +```typescript +const layerIndex = 0; +const normalizedTimeOffset = 0.5; // 归一化的时间 +animator.play("run", layerIndex, normalizedTimeOffset); +``` + +### 获取当前在播放的动画状态 + +你可以使用 [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState)  方法来获取当前正在播放的 AnimatorState。参数为动画状态所在层的序号`layerIndex`, 详见[API 文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。 + +```typescript +const currentState = animator.getCurrentAnimatorState(0); +// 播放一次 +currentState.wrapMode = WrapMode.Once; +// 循环播放 +currentState.wrapMode = WrapMode.Loop; +``` + +### 获取动画状态 + +你可以使用 [findAnimatorState](/apis/core/#Animator-findAnimatorState)  方法来获取指定名称的 AnimatorState。详见[API 文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。 + +```typescript +const state = animator.findAnimatorState("xxx"); +// 播放一次 +state.wrapMode = WrapMode.Once; +// 循环播放 +state.wrapMode = WrapMode.Loop; +``` diff --git a/docs/animation/animatorController.md b/docs/animation/animatorController.md new file mode 100644 index 000000000..8cfc9e08a --- /dev/null +++ b/docs/animation/animatorController.md @@ -0,0 +1,201 @@ +--- +order: 2 +title: 动画控制器 +type: 动画 +label: Animation +--- + +动画控制器([AnimatorController](/apis/core/#AnimatorController))用于组织[动画片段](/docs/animation-clip)([AnimationClip](/apis/core/#AnimationClip))实现更加灵活丰富的动画效果。 + +## 编辑器使用 + +### 基础使用 + +通过动画控制器的编辑器,用户可以在其中组织[动画片段](/docs/animation-clip)的播放逻辑 + +1. 准备好动画片段([制作动画片段](/docs/animation-clip)) + +![1](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*Qc8sQ6iJd8IAAAAAAAAAAAAADsJ_AQ/original) + +2. 要组织播放这些动画片段我们需要创建一个动画控制器([AnimatorController](/apis/core/#AnimatorController))资产 + +![3](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*irT7SZvw4N8AAAAAAAAAAAAADsJ_AQ/original) + +3. 刚创建的动画控制器中没有任何数据,我们需要对他进行编辑,双击资产, 并为它添加一个 AnimatorState + +![5](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*BcYXSI6OTyoAAAAAAAAAAAAADsJ_AQ/original) + +4. 点击 AnimatorState 为它绑定一个动画片段 + +![6](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*KwFzRZCmbxoAAAAAAAAAAAAADsJ_AQ/original) + +5. 在[动画控制组件](/docs/animation-animator)上绑定该动画控制器([AnimatorController](/apis/core/#AnimatorController))资产 + +![4](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*VtX3RJR8kdMAAAAAAAAAAAAADsJ_AQ/original) + +6. 至此你在导出的项目中就可以通过 `animator.play("New State")` 播放 `run` 动画了 + +你还可以通过动画控制器的编辑器实现更多的功能: + +### 默认播放 + +将 AnimatorState 连接到`entry`上你导出的项目运行时就会自动播放其上的动画,而不需再调用 `animator.play`。同时你也会看到编辑器的模型也开始播放动画了。 +![2](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*t2JlQ7PGqikAAAAAAAAAAAAADsJ_AQ/original) + +### 动画过渡 + +将两个想要过渡的 `AnimatorState` 连接即可实现动画过渡的效果, 点击两个动画间的连线,可以修改动画过渡的参数调整效果 + +![animationcrossfade](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*7_OFQqABtc0AAAAAAAAAAAAADsJ_AQ/original) + +#### 参数说明 + +| 属性 | 功能说明 | +| :------- | :----------------------------------------------------------------- | +| duration | 过渡时长,时间为相对目标状态的归一化时间, 默认值为 1.0 | +| offset | 目标状态向前的偏移时间,时间为相对目标状态的归一化时间, 默认值为 0 | +| exitTime | 起始状态过渡开始时间,时间为相对起始状态的归一化时间, 默认值为 0.3 | + +### 动画叠加 + +Galacean 引擎支持多层的动画叠加。动画叠加是通过 `AnimatorControllerLayer` 间的混合达到的效果。第一层是基础动画层,修改它的权重及混合模式将不会生效。 + +双击 `AnimatorController` 资源文件编辑动画,添加 Layer,将混合的动作也连接`entry` + +![animationadditive](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*vF7fS6mRnmYAAAAAAAAAAAAADsJ_AQ/original) + +有的时候你想要得到一个固定的姿势,需要裁减设计师给到的动画切片,可以修改 `AnimatorState` 的`StartTime` 及 `EndTime`,点击 `AnimatorState` 即可对其进行编辑: + +![1](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*JNFGTboEM5QAAAAAAAAAAAAADsJ_AQ/original) + +| 属性 | 功能说明 | +| :------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Name | 修改 `AnimatorState` 的名字,名字在所在的层要是**唯一**的。 | +| AnimationClip | 用于绑定 `AnimationClip` 资产,`AnimationClip` 存储了模型的动画数据。 | +| WrapMode | `AnimatorState` 是循环播放还是播放一次,默认为 `Once` 即播放一次。 | +| Speed | `AnimatorState` 的播放速度,默认值为 1.0 ,值越大动画速度越快 | +| StartTime | `AnimatorState` 从 `AnimationClip` 的哪个时间开始播放,时间为相对 `AnimationClip` 时长的归一化时间。默认值为 0 ,即从头开始播放。 例如:值为 1.0 ,则是 `AnimationClip` 的最后一帧状态。 | +| EndTime | `AnimatorState` 播放到 `AnimationClip` 的哪个时间结束播放,时间为相对 `AnimationClip` 时长的归一化时间。默认值为 1.0 ,即播放到最后。 | + +你也可以通过修改 `Layer` 的 `Weight` 参数来调整 `Layer` 在混合中的权重,通过修改 `Blending` 来修改混合模式。 + +![animationadditive2](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*_3aNSKP44FgAAAAAAAAAAAAADsJ_AQ/original) + +| 属性 | 功能说明 | +| :------- | :--------------------------------------------------------------------------------- | +| Name | 该层的名字。 | +| Weight | 该层的混合权重,默认值为 1.0 。 | +| Blending | 该层的混合模式,`Additive` 为叠加模式, `Override` 为覆盖模式,默认值为 `Override` | + +## 脚本使用 + +> 在使用脚本之前,最好阅读[动画系统构成](/docs/animation-system)文档,以帮助你更好的了解动画系统的运行逻辑 + +### 默认播放 + +你可以通过设置 AnimatorStateMachine 的[defaultState](/apis/core/#AnimatorStateMachine-defaultState) 来设置所在层的默认播放动画,这样当 Animator `enabled=true` 时你不需要调用 `play` 方法即可默认播放。 + +```typescript +const layers = animator.animatorController.layers; +layers[0].stateMachine.defaultState = animator.findAnimatorState("walk"); +layers[1].stateMachine.defaultState = animator.findAnimatorState("sad_pose"); +layers[1].blendingMode = AnimatorLayerBlendingMode.Additive; +``` + +### 动画过渡 + +你可以通过为 `AnimatorState` 添加 `AnimatorTransition` 实现动画状态间的过渡。 + +```typescript +const walkThenRunState = animatorStateMachine.addState("walkThenRun"); +walkThenRunState.clip = walkClip; +const runState = animatorStateMachine.addState("run"); +runState.clip = runClip; +const transition = new AnimatorStateTransition(); +transition.duration = 1; +transition.offset = 0; +transition.exitTime = 0.5; +transition.destinationState = runState; +walkThenRunState.addTransition(transition); +animator.play("walkThenRun"); +``` + +通过这样的方式你之后每次在该动画状态机所在的层播放 `walkThenRun` 动画时都会在 `walk` 动画播放一半时开始过渡到 `run` 动画。 + +### 动画叠加 + +将想要叠加的动画状态添加到其他层并将它的混合模式设置为 `AnimatorLayerBlendingMode.Additive` 即可实现动画叠加效果, + + + +### 动画数据 + +#### 设置动画数据 + +你可以通过 [animatorController](/apis/core/#Animator-animatorController)  属性来设置动画控制器的动画数据,加载完成的 GLTF 模型会自动添加一个默认的 AnimatorController。 + +```typescript +animator.animatorController = new AnimatorController(); +``` + +#### 复用动画数据 + +有的时候模型的动画数据存储在其他模型中,可以用如下的方式引入使用: + + + +除此以外还有一种方式,Animator 的 [AnimatorController](/apis/core/#AnimatorController) 就是一个数据存储的类,它不会包含运行时的数据,基于这种设计只要绑定 Animator 组件的模型的**骨骼节点的层级结构和命名相同**,我们就可以对动画数据进行复用。 + +```typescript +const animator = model1.getComponent(Animator); +animator.animatorController = model2.getComponent(Animator).animatorController; +``` + +### 状态机脚本 + + + +状态机脚本为用户提供了动画状态的生命周期钩子函数来编写自己的游戏逻辑代码。用户可以通过继承 [StateMachineScript](/apis/core/#StateMachineScript) 类来使用状态机脚本。 + +状态机脚本提供了三个动画状态周期: + +- `onStateEnter`:动画状态开始播放时回调。 +- `onStateUpdate`:动画状态更新时回调。 +- `onStateExit`:动画状态结束时回调。 + +```typescript +class theScript extends StateMachineScript { + // onStateEnter is called when a transition starts and the state machine starts to evaluate this state + onStateEnter(animator: Animator, stateInfo: any, layerIndex: number) { + console.log("onStateEnter", animator, stateInfo, layerIndex); + } + + // onStateUpdate is called on each Update frame between onStateEnter and onStateExit callbacks + onStateUpdate(animator: Animator, stateInfo: any, layerIndex: number) { + console.log("onStateUpdate", animator, stateInfo, layerIndex); + } + + // onStateExit is called when a transition ends and the state machine finishes evaluating this state + onStateExit(animator: Animator, stateInfo: any, layerIndex: number) { + console.log("onStateExit", animator, stateInfo, layerIndex); + } +} + +animatorState.addStateMachineScript(theScript); +``` + +如果你的脚本不用复用的话你也可以这么写: + +```typescript +state.addStateMachineScript( + class extends StateMachineScript { + onStateEnter( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + console.log("onStateEnter: ", animatorState); + } + } +); +``` diff --git a/docs/animation/clip-for-artist.md b/docs/animation/clip-for-artist.md new file mode 100644 index 000000000..25d0bd929 --- /dev/null +++ b/docs/animation/clip-for-artist.md @@ -0,0 +1,102 @@ +--- +order: 7 +title: 美术动画切片 +type: 动画 +label: Animation +--- + +动画切片(**AnimationClip**) 为**一段时间轴上的动画组合**,可以是多个物体的旋转、位移、缩放、权重动画,如**走路、跑步、跳跃**可以分别导出 3 个动画切片;Galacean 引擎可以选择播放哪一个动画切片,前提是建模软件导出的 FBX 或者 glTF 里面包含多个动画切片。 + +为减少沟通成本,本文列举了几种常见的动画切片方法,导出 glTF 方便 Galacean 引擎直接使用,也可以通过 [glTF 预览](https://galacean.antgroup.com/#/gltf-viewer) 页面进行功能校验。 + +Blender 的动画编辑页面非常友好,能够清晰地可视化显示受动画影响的节点,并且在时间轴上显示关键帧,因此推荐使用 Blender 进行动画切片。 + +### Blender + +1. 打开 Blender,导入 Blender 支持的模型格式,然后切换到 **动画编辑** 窗口: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/6922d329-6cfa-473d-9fd1-312592e7c307/1622617152228-2c30967c-9203-4ad2-b239-6033cb004bc3.png) + +2. 通过上图的 “新建动画切片”按钮,可以快速的复制当前动画切片,然后进行独有的操作,如果没有显示该按钮,请确保打开了 “**动作编辑器**”: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/53cc73a1-17b2-4a4f-ad42-9a8b059fb69c/1622617514416-e0b83cd6-439f-4003-aa23-f85ca0df04dc.png) + +举例,新建了一个名为 **animation_copy** 的动画切片,然后只保留最后 5 帧动画: + +image.png + +image.png + +image.png + +导出的切片时间轴必须一致,可以通过右下角或者输出属性两个地方进行配置: + +image.png + +image.png + +3. 因为时间轴必须一致,因此需要将刚才切的动画切片,都移到起始帧,拖拽即可: + +image.png + +image.png + +4. 至此,动画切片已经准备完成,导出 glTF 或者 FBX ,接入 Galacean 引擎即可: + +image.png + +image.png + +Unity 也可以导出动画切片,但是效率比较低。 + +### Unity + +插件:[AntG-Unity-Plugin.unitypackage.zip](https://www.yuque.com/attachments/yuque/0/2021/zip/381718/1622541632701-4f33e890-5295-4430-8798-d979b8df504f.zip?_lake_card=%7B%22src%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2021%2Fzip%2F381718%2F1622541632701-4f33e890-5295-4430-8798-d979b8df504f.zip%22%2C%22name%22%3A%22AntG-Unity-Plugin.unitypackage.zip%22%2C%22size%22%3A490677%2C%22type%22%3A%22application%2Fzip%22%2C%22ext%22%3A%22zip%22%2C%22status%22%3A%22done%22%2C%22taskId%22%3A%22u4c98eaae-9ce5-43c7-ae94-c26f4ce0c0f%22%2C%22taskType%22%3A%22upload%22%2C%22id%22%3A%22uef3d6075%22%2C%22card%22%3A%22file%22%7D) + +1. 下载插件。 + +2. 打开 Unity 。 + +3. 双击插件, **Import** 默认框选选项: + +image.png + +若安装成功,可以看到菜单栏多出 **AntG** 选项: + +image.png + +4. 把需要切片的 FBX 文件拖拽进资源栏: + +image.png + +5. 单击该资源,出现动画调试预览框。增加动画切片,并根据需求调整每个切片的时间轴 **start**、**end**,如果预览动画效果异常,确认没有勾选 **Resample Curves** 这个默认开启选项,切片完成后,记得点击右下角的 **Apply** 确认按钮。 + +image.png + +image.png + +6. 至此,动画切片资源已经制作完毕,接下来介绍如何导出,先将该资源拖拽到节点树中: + +image.png + +7. 然后给该节点增加 **Animator** Component: + +image.png + +8. 可以看到,Animator 组件需要绑定一个 Animator Controller 资源,因此我们需要在资源栏新建一个 Animator Controller 资源: + +image.png + +9. 双击该 controller 资源,将我们之前的动画切片拖拽进去: + +image.png + +10. Animator Controller 资源制作完毕,绑定到刚才的 Component 上: + +image.png + +11. 右键该节点,选择导出 AntG: + +image.png + +12. 至此,制作的动画切片 glTF 文件导出完毕,可以访问 Galacean 的 [glTF 预览](https://galacean.antgroup.com/#/gltf-viewer) 进行功能校验。 diff --git a/docs/animation/clip.md b/docs/animation/clip.md new file mode 100644 index 000000000..502adc45d --- /dev/null +++ b/docs/animation/clip.md @@ -0,0 +1,227 @@ +--- +order: 1 +title: 动画片段 +type: 动画 +label: Animation +--- + +**动画片段**是 Galacean 动画系统的核心元素之一,Galacean 支持导入外部设计软件设计的模型动画,设计师输出的带动画的模型中的每个动画在 Galacean 中会被解析成一个个的**动画片段**资产,我们也可以通过动画片段编辑器创作额外的动画。 + +有两种常用的方式得到动画片段 + +1. 导入用第三方工具(例如 Autodesk® 3ds Max®, Autodesk® Maya®, Blender)创建的带动画的模型,详见[美术制作动画片段](/docs/animation-clip-for-artist) + +![1](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*Qc8sQ6iJd8IAAAAAAAAAAAAADsJ_AQ/original) + +2. 在 Galacean 中创建动画片段(下文会介绍编辑器和脚本这两种创建方式) + +## 动画面板介绍 + +动画片段编辑器目前支持除物理相关组件的所有可插值属性的编辑,如果你需要编辑其他属性,要在实体上添加对应的组件。 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*3SAjRb60cfoAAAAAAAAAAAAADsJ_AQ/original) + +> 注:Galacean 动画编辑器默认以 60FPS 为基准,上图所示时间`0:30`为 30 帧处, 若时间轴刻度为`1:30`则为 90 帧处 + +## 基础操作 + +### Transform 组件示例 + +1. 在 **[资产面板](/docs/assets-interface)** 中 右键/点击+ 创建 `动画片段` 资产 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*j4FMRKx91nEAAAAAAAAAAAAADsJ_AQ/original) + +2. 双击 `动画片段` 资产,并选择一个实体作为 `动画片段` 的编辑对象 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*_E1kRqt8LroAAAAAAAAAAAAADsJ_AQ/original) + +点击 `创建` 按钮编辑器会自动为你的Entity添加 [动画控制组件](/docs/animation-animator) 并将该动画片段添加到 [动画控制器](/docs/animation-animatorController) 中 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*q46SRrV6WfsAAAAAAAAAAAAADsJ_AQ/original) +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*C2a4SZDGG_4AAAAAAAAAAAAADsJ_AQ/original) + +3. 添加要做动画的属性(这里我添加了两个) + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*69xIS7ABJJkAAAAAAAAAAAAADsJ_AQ/original) +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*-4bnQI-LcLsAAAAAAAAAAAAADsJ_AQ/original) + +4. 给属性添加关键帧 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*QnQwR6tLRYMAAAAAAAAAAAAADsJ_AQ/original) +当我们点击添加关键帧按钮时,关键帧会存入当前指定属性的值,所以当我们什么都未改变时,关键帧存入的是此刻这个实体的 `position` 值。我们希望他 60 帧后移动到 (3, 0, 0)的位置,所以先将这个正方体通过属性面板修改到(3, 0, 0) 再添加关键帧 +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*ClTgSriu4-8AAAAAAAAAAAAADsJ_AQ/original) +同理我们也为 `rotation` 属性添加关键帧 +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*hOkoRKlNfeYAAAAAAAAAAAAADsJ_AQ/original) + +##### 录制模式 + +我们提供了录制模式以方便开发者快速的添加关键帧。开启录制模式,当对应的属性修改时就会自动添加关键帧 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*sFwtSLOlyhoAAAAAAAAAAAAADsJ_AQ/original) + +### 文字动画示例 + +首先要有一个具有 TextRender 组件的实体 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*VGEGS6kOBqkAAAAAAAAAAAAADsJ_AQ/original) + +此时我们再添加属性时可以看到,可添加关键帧的属性增加了 `TextRenderer` 组件相关的可插值的属性 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*X5YxQb_HieUAAAAAAAAAAAAADsJ_AQ/original) + +我们按照上文的方式用录制模式添加关键帧 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*tsTqSqZVsdUAAAAAAAAAAAAADsJ_AQ/original) + + +### 帧动画示例 + +除了数值类型,Galacean 还支持引用类型的动画曲线,可以阅读[帧动画](/docs/animation-sprite-sheet)来了解,如何制作帧动画。 + +### 材质动画示例 +Galacean 还支持组件中的资产属性的动画编辑。如果组件中有材质资产,在 `Inspector` 中会有额外的资产属性的编辑 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*ctbcSKqHmIAAAAAAAAAAAAAADsJ_AQ/original) + +需要注意的是,编辑器默认的[材质](/docs/graphics-material)是不可编辑的 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*SNRHQbOcqhAAAAAAAAAAAAAADsJ_AQ/original) + +所以我们想给这个立方体做材质的动画需要新建一个材质并替换它之后就同上文一样开启录制模式直接修改属性即可 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*uEQOS52hXpUAAAAAAAAAAAAADsJ_AQ/original) +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*TGDFTrXZbAEAAAAAAAAAAAAADsJ_AQ/original) + + +## 更多操作 + +### 操作关键帧 + +#### 修改关键帧时间 + +选中关键帧并拖动即可 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*_vZSR4YEDqMAAAAAAAAAAAAADsJ_AQ/original) + +可以滑动 `鼠标滚轮` 缩放时间轴 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*BTgPS45hkNYAAAAAAAAAAAAADsJ_AQ/original) + +#### 修改关键帧的值 + +开启录制模式,把时间线移动到指定关键帧上,然后重新输入值即可, 在不开启录制模式的情况,需要重新点击添加关键帧按钮。 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*MiSXQZ4q7DMAAAAAAAAAAAAADsJ_AQ/original) + +#### 删除关键帧 + +选中关键帧右键选择删除,也可以按键盘上的删除键/退格键删除 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*MpPMRK2WaEMAAAAAAAAAAAAADsJ_AQ/original) + +### 编辑子实体 + +`动画片段` 不仅可以作用于添加`Animator`组件的实体上,还可以作用于它的子实体上。 + +1. 我们为刚才的立方体添加一个子实体 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*6RYIQpG7DPwAAAAAAAAAAAAADsJ_AQ/original) + +2. 我们再点击添加属性就可以看到子实体的属性可以添加了 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*j7rfRq0REKIAAAAAAAAAAAAADsJ_AQ/original) + +3. 开启录制模式,编辑子实体添加关键帧即可 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*j7rfRq0REKIAAAAAAAAAAAAADsJ_AQ/original) + + +### 编辑动画曲线 + +`动画片段编辑器` 支持动画曲线编辑,点击右上角的曲线Icon即可切换 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*luV0QaDUAQAAAAAAAAAAAAAADsJ_AQ/original) + +曲线模式的纵轴为属性的数值 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*luV0QaDUAQAAAAAAAAAAAAAADsJ_AQ/original) + + +你可以按 `shift+鼠标滚轮` 调整纵轴的比例 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*SjO2TaubiuIAAAAAAAAAAAAADsJ_AQ/original) + +属性对应曲线的颜色与按钮的颜色相同 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*OS3uSbdB36AAAAAAAAAAAAAADsJ_AQ/original) + +选择关键帧会出现两个控制点,调整控制点即可调整曲线 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*kikYQoiYMLoAAAAAAAAAAAAADsJ_AQ/original) + +也可以通过右键关键帧直接设置内置的预设值 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*KRzoT5Pocc4AAAAAAAAAAAAADsJ_AQ/original) + +选择属性面板的属性可以仅编辑指定属性的曲线 + + + +## 脚本使用 + +> 在使用脚本之前,最好阅读[动画系统构成](/docs/animation-system)文档,以帮助你更好的了解动画系统的运行逻辑 + +你可以自己创建一个 [AnimationClip](/apis/core/#AnimationClip)  并通过 [addCurveBinding](/apis/core/#AnimationClip-addCurveBinding) 为它绑定 [AnimationCurve](/apis/core/#AnimationCurve)。 + +```typescript +//custom rotate clip +const rotateClip = new AnimationClip("rotate"); +const rotateState = + animator.animatorController.layers[0].stateMachine.addState("rotate"); +rotateState.clip = rotateClip; + +const rotateCurve = new AnimationVector3Curve(); +const key1 = new Keyframe(); +key1.time = 0; +key1.value = new Vector3(0, 0, 0); +const key2 = new Keyframe(); +key2.time = 10; +key2.value = new Vector3(0, 360, 0); +rotateCurve.addKey(key1); +rotateCurve.addKey(key2); +rotateClip.addCurveBinding("", Transform, "rotation", rotateCurve); + +//custom color clip +const colorClip = new AnimationClip("color"); +const colorState = + animator.animatorController.layers[0].stateMachine.addState("color"); +colorState.clip = colorClip; + +const colorCurve = new AnimationFloatCurve(); +const key1 = new Keyframe(); +key1.time = 0; +key1.value = 0; +const key2 = new Keyframe(); +key2.time = 10; +key2.value = 1; +colorCurve.addKey(key1); +colorCurve.addKey(key2); +colorClip.addCurveBinding("/light", DirectLight, "color.r", colorCurve); +``` + +你同样可以为你的子实体 `/light` 绑定 AnimationCurve,就像上面的代码示例,同时 `addCurveBinding` 的第三个参数并不局限于组件的属性,它是一个能索引到值的路径。 + + + +### 动画事件 + +你可以使用 [AnimationEvent](/apis/core/#AnimationEvent)  来为 AnimationClip 添加事件,动画事件将在指定时间调用你在同一实体上绑定组件的指定回调函数。 + +```typescript +const event = new AnimationEvent(); +event.functionName = "test"; +event.time = 0.5; +clip.addEvent(event); +``` + + diff --git a/docs/animation/overview.md b/docs/animation/overview.md new file mode 100644 index 000000000..7d1ffbe06 --- /dev/null +++ b/docs/animation/overview.md @@ -0,0 +1,40 @@ +--- +order: 0 +title: 动画系统概述 +type: 动画 +label: Animation +--- + +Galacean 的动画系统具有以下功能: + +- 解析 GLTF/FBX 模型中的动画并转为 Galacean 中的 AnimationClip 对象 +- 为 Galacean 的所有组件及其属性添加动画 +- 设置动画的开始/结束时间以裁剪动画 +- 设置动画间的过渡,将多个动画叠加 +- 将一个模型的动画应用到另一个模型上 +- 添加动画事件及动画生命周期的脚本 + +## 动画工作流程 + +使用编辑器创建互动项目的整体流程: + +```mermaid +flowchart LR + 添加动画片段 --> 创建动画控制器并导入动画片段 --> 添加动画控制组件播放动画 +``` + +### 1. 添加动画片段 + +Galacean 的动画系统基于动画片段的概念,动画片段包含某些对象应如何随时间改变其位置、旋转或其他属性的相关信息。每个动画片段可视为单个线性录制。 + +你可以在编辑器中创建动画片段,详见[制作动画片段](/docs/animation-clip),也可以导入用第三方工具(例如 Autodesk® 3ds Max®, Autodesk® Maya®, Blender)创建的带动画的模型,详见[美术制作动画片段](/docs/animation-clip-for-artist),或者来自动作捕捉工作室或其他来源。 + +### 2. 创建动画控制器并导入动画片段 + +动画控制器是一个类似于流程图的结构化系统,它在动画系统中充当状态机,负责跟踪当前应该播放哪个片段以及动画应该何时改变或混合在一起。 + +你可以在这篇[动画控制器](/docs/animation-animatorController)中了解如何使用。 + +### 3. 添加动画控制组件 + +编辑好动画控制器后,我们需要在实体上添加[动画控制组件](/docs/animation-animator)并绑定动画控制器资产来播放动画。 diff --git a/docs/animation/sprite-sheet.md b/docs/animation/sprite-sheet.md new file mode 100644 index 000000000..479d38473 --- /dev/null +++ b/docs/animation/sprite-sheet.md @@ -0,0 +1,32 @@ +--- +order: 4 +title: 帧动画 +type: 动画 +label: Animation +--- + +Galacean 支持引用类型的动画曲线,你可以添加类型为资产的关键帧比如(精灵)下图为制作精灵动画的流程: + +1. 给节点添加 `SpriteRenderer` 组件 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*XiUaQ76M4Q0AAAAAAAAAAAAADsJ_AQ/original) + +2. 添加`精灵`,可以参考[精灵](/docs/graphics-2d-sprite) + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*ababSZAMpJMAAAAAAAAAAAAADsJ_AQ/original) + +3. 在 **[资产面板](/docs/assets-interface)** 中创建 [动画片段](/docs/animation-clip) + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*CZQjSqZAHGsAAAAAAAAAAAAADsJ_AQ/original) + + +4. 开启录制模式,编辑器中点到对应的帧数,在 `SpriteRenderer` 中添加 `Sprite` 即可自动添加关键帧 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*Eff6TbgYps8AAAAAAAAAAAAADsJ_AQ/original) + + +### 脚本实现 + +引擎在 1.1 版本支持引用类型的动画曲线([AnimationRefCurve](/apis/core/#AnimationRefCurve)),关键帧的值可以是资产如(精灵,材质),你可以通过创建引用类型的动画曲线实现比如帧动画的能力: + + diff --git a/docs/animation/state-machine.md b/docs/animation/state-machine.md new file mode 100644 index 000000000..28acc01cd --- /dev/null +++ b/docs/animation/state-machine.md @@ -0,0 +1,166 @@ +--- +order: 5 +title: 动画状态机 +type: 动画 +label: Animation +--- + +### 动画过渡 + +将两个想要过渡的 `AnimatorState` 连接即可实现动画过渡的效果, 点击两个动画间的连线,可以修改动画过渡的参数调整效果 + +![animationcrossfade](https://gw.alipayobjects.com/zos/OasisHub/cd8fa035-0c1c-493e-a0c7-54d301f96156/1667458692286-29d9f543-9b98-4911-8fa7-ac38b61b1668.gif) + +#### 参数说明 + +| 属性 | 功能说明 | +| :------- | :---------------------------------------------------------| +| duration | 过渡时长,时间为相对目标状态的归一化时间, 默认值为 1.0 | +| offset | 目标状态向前的偏移时间,时间为相对目标状态的归一化时间, 默认值为 0 | +| exitTime | 起始状态过渡开始时间,时间为相对起始状态的归一化时间, 默认值为 0.3 | + +#### 脚本使用 + +你可以通过为 `AnimatorState` 添加 `AnimatorTransition` 实现动画状态间的过渡。 + +```typescript +const walkState = animatorStateMachine.addState('walk'); +walkState.clip = walkClip; +const runState = animatorStateMachine.addState('run'); +runState.clip = runClip; +const transition = new AnimatorStateTransition(); +transition.duration = 1; +transition.offset = 0; +transition.exitTime = 0.5; +transition.destinationState = runState; +walkState.addTransition(transition); +animator.play("walk"); +``` +通过这样的方式你之后每次在该动画状态机所在的层播放 `walk` 动画时都会在播放一半时开始过渡到 `run` 动画。 + +### 动画叠加 + +Galacean引擎支持多层的动画叠加。动画叠加是通过 `AnimatorControllerLayer` 间的混合达到的效果。第一层是基础动画层,修改它的权重及混合模式将不会生效。 + +双击 `AnimatorController` 资源文件编辑动画,添加 Layer,将混合的动作也连接`entry` + +![animationadditive](https://gw.alipayobjects.com/zos/OasisHub/7548a66b-a72f-4cad-9b27-c9f1a2824aff/1667459461151-4568a32a-07db-427b-922e-3bc6f844097b.gif) + +有的时候你想要得到一个固定的姿势,需要裁减设计师给到的动画切片,可以向上图一样修改 `AnimatorState` 的`StartTime` 及 `EndTime`,点击 `AnimatorState` 即可对其进行编辑: + +![1](https://gw.alipayobjects.com/zos/OasisHub/cc0db4c9-95f9-48d7-a3ac-48d69e94a31d/1.jpg) + +| 属性 | 功能说明 | +| :------- | :------------------------------------------------------------------- | +| Name | 修改 `AnimatorState` 的名字,名字在所在的层要是**唯一**的。 | +| AnimationClip | 用于绑定 `AnimationClip` 资产,`AnimationClip` 存储了模型的动画数据。 | +| WrapMode | `AnimatorState` 是循环播放还是播放一次,默认为 `Once` 即播放一次。 | +| Speed | `AnimatorState` 的播放速度,默认值为 1.0 ,值越大动画速度越快 | +| StartTime | `AnimatorState` 从 `AnimationClip` 的哪个时间开始播放,时间为相对 `AnimationClip` 时长的归一化时间。默认值为 0 ,即从头开始播放。 例如:值为 1.0 ,则是 `AnimationClip` 的最后一帧状态。 | +| EndTime | `AnimatorState` 播放到 `AnimationClip` 的哪个时间结束播放,时间为相对 `AnimationClip` 时长的归一化时间。默认值为 1.0 ,即播放到最后。 | + +你也可以通过修改 `Layer` 的 `Weight` 参数来调整 `Layer` 在混合中的权重,通过修改 `Blending` 来修改混合模式。 + +![animationadditive2](https://gw.alipayobjects.com/zos/OasisHub/acd80bdf-7c8d-41ac-8a2f-fe75cc6d2da4/1667459778293-be31b02b-7f6c-4c27-becc-2c0c8e80b538.gif) + +| 属性 | 功能说明 | +| :------- | :------------------------------------------------------------------------- | +| Name | 该层的名字。 | +| Weight | 该层的混合权重,默认值为 1.0 。 | +| Blending | 该层的混合模式,`Additive` 为叠加模式, `Override` 为覆盖模式,默认值为 `Override` | + + +#### 脚本使用 + +将想要叠加的动画状态添加到其他层并将它的混合模式设置为 `AnimatorLayerBlendingMode.Additive` 即可实现动画叠加效果, + + + +### 默认播放 + +将 AnimatorState 连接到`entry`上你导出的项目运行时就会自动播放其上的动画,而不需再调用 `animator.play`。同时你也会看到编辑器的模型也开始播放动画了。 +![2](https://gw.alipayobjects.com/zos/OasisHub/de60a906-0d3c-4578-8d50-aa2ce050e560/2.jpg) + +#### 脚本使用 + +你可以通过设置AnimatorStateMachine的[defaultState](/apis/core/#AnimatorStateMachine-defaultState) 来设置所在层的默认播放动画,这样当Animator `enabled=true` 时你不需要调用 `play` 方法即可默认播放。 + +```typescript +const layers = animator.animatorController.layers; +layers[0].stateMachine.defaultState = animator.findAnimatorState('walk'); +layers[1].stateMachine.defaultState = animator.findAnimatorState('sad_pose'); +layers[1].blendingMode = AnimatorLayerBlendingMode.Additive; +``` + +### 获取当前在播放的动画状态 + +你可以使用 [getCurrentAnimatorState](/apis/core/#Animator-getCurrentAnimatorState) 方法来获取当前正在播放的AnimatorState。参数为动画状态所在层的序号`layerIndex`, 详见[API文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。 + +```typescript +const currentState = animator.getCurrentAnimatorState(0); +// 播放一次 +currentState.wrapMode = WrapMode.Once; +// 循环播放 +currentState.wrapMode = WrapMode.Loop; +``` + +### 获取动画状态 + +你可以使用 [findAnimatorState](/apis/core/#Animator-findAnimatorState) 方法来获取指定名称的AnimatorState。详见[API文档](/apis/core/#Animator-getCurrentAnimatorState)。获取之后可以设置动画状态的属性,比如将默认的循环播放改为一次。 + +```typescript +const state = animator.findAnimatorState('xxx'); +// 播放一次 +state.wrapMode = WrapMode.Once; +// 循环播放 +state.wrapMode = WrapMode.Loop; +``` + +### 状态机脚本 + + + +状态机脚本为用户提供了动画状态的生命周期钩子函数来编写自己的游戏逻辑代码。用户可以通过继承 [StateMachineScript](/apis/core/#StateMachineScript) 类来使用状态机脚本。 + +状态机脚本提供了三个动画状态周期: + +- `onStateEnter`:动画状态开始播放时回调。 +- `onStateUpdate`:动画状态更新时回调。 +- `onStateExit`:动画状态结束时回调。 + +```typescript +class theScript extends StateMachineScript { + // onStateEnter is called when a transition starts and the state machine starts to evaluate this state + onStateEnter(animator: Animator, stateInfo: any, layerIndex: number) { + console.log('onStateEnter', animator, stateInfo, layerIndex); + } + + // onStateUpdate is called on each Update frame between onStateEnter and onStateExit callbacks + onStateUpdate(animator: Animator, stateInfo: any, layerIndex: number) { + console.log('onStateUpdate', animator, stateInfo, layerIndex); + } + + // onStateExit is called when a transition ends and the state machine finishes evaluating this state + onStateExit(animator: Animator, stateInfo: any, layerIndex: number) { + console.log('onStateExit', animator, stateInfo, layerIndex); + } +} + +animatorState.addStateMachineScript(theScript) +``` + +如果你的脚本不用复用的话你也可以这么写: + +```typescript +state.addStateMachineScript( + class extends StateMachineScript { + onStateEnter( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + console.log("onStateEnter: ", animatorState); + } + } +); +``` \ No newline at end of file diff --git a/docs/animation/system.md b/docs/animation/system.md new file mode 100644 index 000000000..a63e2ce6d --- /dev/null +++ b/docs/animation/system.md @@ -0,0 +1,46 @@ +--- +order: 6 +title: 动画系统构成 +type: 动画 +label: Animation +--- + +### 动画系统的构成 + +```mermaid +flowchart TD + %% Colors %% + linkStyle default stroke-width:1px,stroke-dasharray:3 + classDef white fill:white,stroke:#000,stroke-width:1px,color:#000 + classDef yellow fill:#fffd75,stroke:#000,stroke-width:1px,color:#000 + classDef green fill:#93ff75,stroke:#000,stroke-width:1px,color:#000 + + Animator:::green --> AnimatorController:::yellow + Animator --> AnimatorControllerParameter:::white + AnimatorController --> AnimatorControllerLayer + AnimatorControllerLayer --> AnimatorStateMachine + AnimatorControllerLayer --> BlendingMode:::white + AnimatorStateMachine --> AnimatorState + AnimatorStateMachine --> AnimatorTransition:::white + AnimatorState --> AnimationClip + AnimationClip:::yellow --> AnimationCurve + AnimationClip --> AnimationEvent:::white + AnimationCurve --> Keyframe + AnimationCurve --> Interpolation:::white +``` + +| 概念 | 解释 | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Animator](/apis/core/#Animator) | 动画控制器组件,用于控制动画的播放。Animator 组件读取 AnimatorController 作为动画数据。通过 AnimatorControllerParameter 设置该 Animator 中的变量。 | +| [AnimatorController](/apis/core/#AnimatorController) | 用于存储 Animator 组件的动画数据。一个 AnimatorController 包含多个 AnimatorControllerLayer,用于分层播放或动画叠加。 | +| AnimatorControllerParameter(开发中) | 动画控制器中使用的变量,使用户可以通过在脚本中改变变量以控制动画状态的切换。 | +| [AnimatorControllerLayer](/apis/core/#AnimatorControllerLayer) | 存储该层的动画状态机数据,混合模式以及混合的权重。多个 AnimatorControllerLayer 同时播放时可以通过设置 `blendingMode = AnimatorLayerBlendingMode.Additive` 实现动画叠加的效果。 | +| [AnimatorStateMachine](/apis/core/#AnimatorStateMachine) | 每个 AnimatorControllerLayer 中有一个 AnimatorStateMachine,用于控制动画状态的播放及状态间的切换及过渡。 | +| [BlendingMode](/apis/core/#AnimatorControllerLayer-blendingMode) | 动画层的混合模式,默认为 `AnimatorLayerBlendingMode.Override` 既覆盖模式,可以通过将下面的层设置为 `AnimatorLayerBlendingMode.Additive` 实现动画叠加的效果。 | +| [AnimatorState](/apis/core/#AnimatorState) | AnimatorState 是 AnimatorStateMachine 的基本构成。可以控制 AnimationClip 的速度,是否循环,开始结束时间。每个 AnimatorState 需绑定一个 AnimationClip,当处于该状态时,则会播放该 AnimationClip。 | +| [AnimatorTransition](/apis/core/#AnimatorTransition) | AnimatorTransition 定义了状态机何时以及如何从一个状态过渡到另一个状态。通过它可以设置两个动画状态的过渡开始时间 `exitTime`,目标状态的开始时间 `offset` 及过渡时长 `duration`。 | +| [AnimationClip](/apis/core/#AnimationClip) | 动画片段,存储设计师制作的基于关键帧的动画数据。一个 AnimationClip 一般对应一个模型的特定动作,每个 AnimationClip 包含多个 AnimationCurve。 | +| [AnimationCurve](/apis/core/#AnimationCurve) | 一个模型拥有多个骨骼,模型动画中每个骨骼实体的指定属性的动画关键帧数据存储于 AnimationCurve 中。一个 AnimationCurve 中包含多个 Keyframe 既关键帧数据。 | +| [AnimationEvent](/apis/core/#AnimationEvent) | AnimationEvent 可以让你在指定时间调用其同一实体绑定的脚本的回调函数. | +| [Keyframe](/apis/core/#KeyFrame) | 存储动画关键帧数据,既指定时间实体的属性的值应是多少。 | +| [Interpolation](/apis/core/#AnimationCurve-interpolation) | 动画曲线中关键帧的插值方式。既当时间在两个关键帧间时,属性的值该如何计算。 | diff --git a/docs/art/bake-blender.md b/docs/art/bake-blender.md new file mode 100644 index 000000000..de5474d12 --- /dev/null +++ b/docs/art/bake-blender.md @@ -0,0 +1,86 @@ +--- +order: 0 +title: Blender 烘焙 +type: 美术 +label: Art +--- + +> **_特别感谢 扫地盲僧及 UU 跑腿效能团队 提供本篇教程_** + +美术在模型实际制作过程中,可能会使用较多的素材,材质等以达到更好的视觉效果。但在导出时会出现较大的渲染落差,有时还会丢失一些效果。 + +我们在最大程度还原 3D 模型渲染流程方面做了一些优化,希望能给大家带来帮助。 + +### Blender 烘焙 + +1. 选择模型,目前这个建筑由很多不同的模型组成,我们需要把它们合并成为一个模型 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/062ab80a-f13e-4bde-b916-61ed65150540/1635163063741-2a68da6a-bb53-47ef-8404-7f52c127c802.png) + +2. 选中 2 个模型合并的快捷键是【ctrl+j】,如果遇到哪些模型有修改器,需要先把修改器应用了再合并,否则修改器做出来的效果会失效。 + +image.png + +image.png + +3. 开始烘焙,选择着色器视图,选中材质属性,在节点视图【shift+a】新建:纹理 -> 图像纹理。 + +image.png + +4. 点击新建>取一个合适的名字即可,左侧展开就能看到你刚新建的图像纹理。 + +image.png + +image.png + +5. 把你新建的这个图像纹理复制到建筑下的所有材质节点中,并且是选中状态。 + +image.png + +6. 配置烘焙参数,先不要点击烘焙,需要下一步的 UV + +image.png + +### 拆 UV + +1. 进入 UV 编辑视图,选中建筑模型,【tab 键】打开编辑模式,可以看到现在 UV 是混乱的 + +image.png + +2. 按【U】选择智能 UV 投射,点击确定,就能获得到一个还不错的 UV 展开图,当然如果美术有时间也可以手工 + +image.png + +image.png + +image.png + +3. 返回到着色器视图,选中模型图层和图层下所有材质,设置烘焙下参数不用改,点击烘焙。下面有进度条,耐心等待即可 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/e8c439b1-4dc4-419e-abe4-ad7fb7a3b8e5/1635165107820-c9733262-2672-4d1a-ac01-0452ed71c440.png) + +4. 烘焙完成,左侧我们新建的图像纹理已经烘焙好了,【alt+s】可快速保存这个烘焙贴图 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/96becab7-74e3-4b2b-94e8-0c74bc5929d5/1635165192308-88c6f55f-faa6-4aa2-91c5-2b7114dbc3e8.png) + +### 导出 glTF 文件 + +1. 将上述模型的所有材质都删掉,新建一个背景材质赋予上 + +image.png + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/a2d19c0a-79b9-4d51-b306-83a86d4388e4/1635165697839-5f88f82f-a66b-453d-970d-3398818ca8d8.png) + +2. 把房子的烘焙图拖进节点视图中,连接颜色 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/ad28062c-09f0-4586-8ca0-a927f94e57d0/1635165825176-cb680c0b-9126-47f8-8ee2-920df3831a89.png) + +3. 导出 glTF 格式,确保这 2 个图层的眼睛和相机都开启状态 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/1590d92c-54ba-4efa-b6ca-6c2c3e8b95de/1635165880957-933cc281-8848-436f-a2af-186d818202d1.png) + +4. 放入[glTF 查看器](https://galacean.antgroup.com/#/gltf-viewer)预览效果 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/81cfd9e4-474a-45dc-8133-de27a9c08dd6/1635166016557-59978f7f-6c91-4f13-99b3-9907e5c8cd44.png) + +希望能给大家带来帮助~ diff --git a/docs/art/bake-c4d.md b/docs/art/bake-c4d.md new file mode 100644 index 000000000..ead1efbb3 --- /dev/null +++ b/docs/art/bake-c4d.md @@ -0,0 +1,56 @@ +--- +order: 1 +title: C4D 烘焙 +type: 美术 +label: Art +--- + +以 C4D-OC 渲染器的烘培(windows)为例讲解。 + +### 什么是烘焙 + +关于烘培就是将所有渲染好的材质颜色信息用一张贴图的形式表达出来。 + +烘培首先需要两组模型:一个高模,一个低模。高模用于烘培精细程度较高的贴图,低模用于在进入引擎后使用贴图两者在制作时要保证 uv 的统一,所以先产生低模排布好 uv 然后进行细节加工产生高模,高模可以较多的制作细节来烘培出细节更丰富的贴图。 + +![1.gif](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*pbduQosyOJwAAAAAAAAAAAAAARQnAQ) + +左边为低模 右边为高模 从布线信息可以看得出来,并且高模细的细节会多一些。 + +![2.gif](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*SgbzSKngA2IAAAAAAAAAAAAAARQnAQ) + +随便布线细节有差异但是两者的可以被看到的部分要保持一致 被遮盖的部分不做考虑所以高模必须由低模产生来保证 uv 的一致。 + +### 具体的烘培过程 + +1.将准备好的高模用 C4D 进行调节材质渲染出来需要的效果,像面部使用的贴图也需要按照整体的 uv 排布来绘制贴图。调节好材质之后就可以准备进行烘培了。 + +image.png + +2. 烘培重要的一点是需要对摄像机的模式进行选择,对需要进行输出的摄像机进行标签指定,加入 OC 渲染器特有的摄像机标签。 + + + +3. 点击添加好的摄像机标签进入标签属性摄像机类型会有很多选项其中一项为烘培,选择烘培。 + +image.png + +4. 在烘培菜单中将烘培群组 ID 设置为除 1 以外的数字这里设置为 2。 + +image.png + +5. 然后需要将需要烘培的模型并为一组,如下图所示将所有需要的模型打一个组并且加入 OC 对象标签。 + +image.png + +6. 点击标签出现标签属性选择对象图层,然后将里面的烘培 ID 设置为和烘培群组 ID 一样的数值这里是 2,然后点击渲染即可,这样就可以烘培出需要的图片。 + +image.png + +image.png + +如果对烘焙的结果不是非常满意,C4D、Substance Painter 都能够涂刷修改贴图。真实感渲染不是唯一的选择,涂刷贴图也可以用来还原一些特殊的风格。 + +image.png + +image.png diff --git a/docs/art/lottie.md b/docs/art/lottie.md new file mode 100644 index 000000000..b52b1f628 --- /dev/null +++ b/docs/art/lottie.md @@ -0,0 +1,38 @@ +--- +order: 2 +title: 导出 Lottie 动画 +type: 美术 +label: Art +--- + +## 什么是 Bodymovin + +- Bodymovin 是一个 AE 的插件,它可以把动画直接输出成代码,直接给程序员使用放在各个终端上使用。 +- 你可以在 github 上找到最新版本的 bodymovin 使用。 +- Bodymovin 的版本等于输出的 json 文件版本。 + +## 怎样使用 Bodymovin + +- 到 Bodymovin 的 GitHub 首页(链接:airbnb/lottie-web)克隆项目到本地,或者下载 .zip 包。    + ![image.png](https://gw.alipayobjects.com/zos/OasisHub/429a17b1-19b3-41b8-902c-4992d722832f/1597673434824-27e06992-4a7d-486a-8514-62a470c53789.png) +- 在项目目录的“/build/extension”目录下找到“bodymovin.zxp”文件,这个就是插件包了。 +- 下载安装 ZXP Installer。 + ZXP 插件安装器地址: [https://aescripts.com/learn/zxp-installer](https://aescripts.com/learn/zxp-installer)
+ +![image.png](https://gw.alipayobjects.com/zos/OasisHub/1e996008-498c-4845-953b-8d39f05503e0/1597674042809-af5a084f-f21b-4bf4-b0d4-7404466b2a1e.png) + +- 打开 AE,点击“编辑”>“首选项”>“常规”菜单项,选中“允许脚本写入文件和访问网络”,点击确定。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/22b31fcd-2b6e-4691-abd1-b173ccab87e7/1597674058269-f2242296-32c5-4ae9-973b-2943e04e94bc.png) + +- 点击“窗口”>“扩展”>“Bodymovin”菜单项,就可以打开 Bodymovin 的界面使用插件了。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/cb002ffc-4b59-4dbd-a85d-56e0c1809475/1597674100420-41e2440c-fe9a-4280-8000-4f384ccdf9c3.png) + +- 打开 Bodymovin 插件窗口,可以发现该项目的名称出现在了下面的列表中。选中该名称,设置好 json 文件输出位置,点击 “Render”。 + +image.png + +- 点击上图中的 Settings,可以对导出的 json 进行配置: + +image.png diff --git a/docs/assets/build.md b/docs/assets/build.md new file mode 100644 index 000000000..ba89823f0 --- /dev/null +++ b/docs/assets/build.md @@ -0,0 +1,337 @@ +--- +order: 2 +title: 项目导出 +type: 资产工作流 +label: Resource +--- + +## HTML5 项目 + +Galacean Editor 项目导出功能可以将当前编辑器项目作为一个前端项目下载到本地。你可以在编辑器中配置项目导出的参数,如资产导出配置、渲染导出配置、物理导出配置等。基于这些配置,编辑器会生成出项目所需的代码、资产,生成对应的 `package.json`,并最终打包成一个 zip 包供你下载。 + +### 导出配置 + +#### 资产导出配置 + +image-20231007201437362 + +资产导出配置可以用来控制导出的资源类型和质量等参数。在资产导出配置中,你可以选择导出的资源类型,例如模型、纹理、HDR 等等,以及选择每种类型的导出质量和格式等参数。在导出模型时,你可以选择是否导出模型的网格信息、骨骼信息、动画信息等。 + +| 配置 | 描述 | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| glTF Quantize | glTF 压缩算法,详见[这里](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md) | +| glTF Meshopt | glTF 压缩算法,详见[这里](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) | +| 纹理类型 | 勾选 [KTX2](https://www.khronos.org/ktx/) 开启[纹理压缩](/docs/graphics-texture-compression)优化选项 | +| 纹理压缩格式 | 勾选 [KTX2](https://www.khronos.org/ktx/) 后可见,不同压缩格式会影响纹理的尺寸和渲染质量 | +| 纹理压缩质量 | 勾选 [KTX2](https://www.khronos.org/ktx/) 后可见,可以一定限度上调整纹理的尺寸和渲染质量 | +| 主场景 | 选择 **[资产面板](/docs/assets-interface)** 中的某个场景作为项目加载后的主场景 | + +#### 渲染导出配置 + + + +渲染导出配置可以用来控制项目的渲染效果和性能等参数。 + +| 配置 | 描述 | +| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| WebGL Mode | WebGL 的版本,`Auto` 值表示根据设备能力自动选择 WebGL 版本 | +| WebGL [Context](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext) 的配置 | Anti-Alias、Alpha、Preserve Drawing Buffer 等 | +| Device Pixel Ratio | [设备的像素比](/docs/core-canvas),用来控制画布的尺寸 | + +### 项目启动 + +在点击导出面板中的下载按钮后,你将得到一个项目的压缩包。解压缩后进入文件夹,目录结构(以 React 项目为例)如下: + +```shell +├── example # 📁 示例目录 +│ ├── main.tsx # 示例组件 +├── public # 📁 公共资源目录 +│ ├── scene.json # 场景文件 +│ └── ... # 其他 +├── src # 📁 源代码目录 +│ └── ... # 其他 +├── index.tsx # ⚙️ 组件代码入口 +├── index.html # ⚙️ 示例项目入口文件 +├── project.json # ⚙️ 编辑器导出工程配置 +|── tsconfig.json # ⚙️ TypeScript 配置文件 +├── vite.config.ts # ⚙️ vite 配置文件 +├── package.json # ⚙️ 项目配置文件 +└── ... # 其他 +``` + +### 项目调试 + +接下来就可以在本地进行项目的调试与预览了,依次在文件夹目录里的 Terminal 中运行以下命令,看看本地效果是否与编辑器中的效果一致吧: + +```bash +npm install +npm run dev +``` + +image-20231008163057689 + +### 项目构建与部署 + +一切准备完毕后就将项目构建并部署上去吧,在文件夹目录里的 Terminal 中运行以下命令: + +```bash +npm run build +``` + +image-20231008163057689 + +可以发现,当 `build` 完毕后,文件目录(左上角)多出了一个 `dist` 文件夹,里面即包含了运行所需的所有代码与资源,接下来只需要将这个文件内的所有内容上传 CDN 即可。 + +image-20231008163057689 + +随后访问对应地址: + +image-20231008163057689 + +> 导出项目为 vite 工程,更多部署方案参考 [vite 官网](https://vitejs.dev/guide/) + +## 小程序项目 + +目前 Galacean 已经适配到支付宝和淘宝小程序。本教程默认开发者已经具备一定的小程序开发能力,如果没有,请阅读下面教程,下载小程序开发工具及申请 AppId: + +- [支付宝小程序](https://opendocs.alipay.com/mini/developer) +- [淘宝小程序](https://miniapp.open.taobao.com/docV3.htm?docId=119114&docType=1&tag=dev) + +小程序项目发布: + +- [支付宝小程序发布](https://opendocs.alipay.com/mini/introduce/release) +- [淘宝小程序发布](https://developer.alibaba.com/docs/doc.htm?spm=a219a.7629140.0.0.258775fexQgSFj&treeId=635&articleId=117321&docType=1) + +### 项目导出 + +Galacean 编辑器导出支付宝小程序的功能仍在开发中,交互方式和模板工程后续可能会有改动。 + +image-20231008163057689 + +### 项目启动 + +点击下载后会下载一个 zip 文件,解压文件目录结构如下: + +```shell +. +├── mini # 📁 小程序执行目录 +│ ├── dist # 📁 代码构建结果 +│ ├── pages # 📁 小程序页面 +│ ├── app.json # ⚙️ 项目配置文件 +│ ├── app.js # 代码入口 +├── public # 📁 公共资源目录 +│ ├── scene.json # 场景文件 +│ └── ... # 其他 +├── src # 📁 源代码目录 +├── mini.project.json # ⚙️ 工程配置文件 +├── project.json # ⚙️ 编辑器导出工程配置 +└── ... # 其他 +``` + +接下来就可以安装依赖和启动项目: + +```shell +npm install +npm run dev +``` + +用小程序 IDE 打开可以看到: + +![image-20230420111035524](https://mdn.alipayobjects.com/rms/afts/img/A*kEUkTbfSMIwAAAAAAAAAAAAAARQnAQ/original/image-20230420111035524.png) + +### 本地资源处理 + +#### 蚂蚁集团内部用户 + +直接使用『上传到 CDN 』即可(在导出面板选项中,参考上图),使用集团默认 CDN 即可。若想使用自定义 CDN,参考非蚂蚁集团内部用户。 + +#### 非蚂蚁集团内部用户 + +1. public 文件请自行上传 CDN +2. 修改 scene.json 文件或配置 baseUrl + +### 包内文件加载(WIP) + +目前还没有支持小程序的本地文件加载。 + +### 已知问题 + +- 小程序不支持 WebAssembly,目前无法使用 PhysX 作为物理后端 +- 目前不支持本地文件加载,需要手动上传到 CDN + +## 注意事项 + +在使用编辑器项目导出功能时,你需要注意以下事项: + +1. 导出的项目需要在支持 WebGL 的环境中运行。 +2. 导出的项目中可能包含大量的资源文件,你需要对项目进行优化和压缩,以提高项目的性能和加载速度。 +3. 导出的项目中可能包含敏感信息和数据,你需要对项目进行安全性评估和保护,以防止信息泄漏和数据丢失等情况。 + +--- + +## 小程序的补充说明 + +### 小程序项目使用 OrbitControl + +1. 引入二方库 + +```bash +npm install @galacean/engine-toolkit-controls -S +``` + +```typescript +import { OrbitControl } from "@galacean/engine-toolkit-controls/dist/miniprogram"; +``` + +2. 添加组件 + +`OrbitControl` 组件需要添加到相机节点上。 + +```typescript +cameraEntity.addComponent(OrbitControl); +``` + +3. 事件模拟派发 + +因为小程序不支持 `addEventListener` 添加监听事件,得手动添加事件的模拟,并且小程序的 canvas 的多指触控存在 bug,所以添加一个和 canvas 大小和位置一样的 view 层去派发触摸事件: + +```html + + + + + +``` + +```typescript +import { dispatchPointerUp, dispatchPointerDown, dispatchPointerMove, dispatchPointerLeave, dispatchPointerCancel } from "@galacean/engine-miniprogram-adapter"; + +Page({ + ... + onTouchEnd(e) { + dispatchPointerUp(e); + dispatchPointerLeave(e); + }, + onTouchStart(e) { + dispatchPointerDown(e); + }, + onTouchMove(e) { + dispatchPointerMove(e); + }, + onTouchCancel(e) { + dispatchPointerCancel(e); + } +}) +``` + +### Pro code 创建 Galacean 小程序项目 + +> 需要 Node.js 版本 >=12.0.0. + +使用 yarn 创建 + +```bash +yarn create @galacean/galacean-app --template miniprogram +``` + +使用 npm **6.x** 版本创建 + +``` +npm init @galacean/galacean-app --template miniprogram +``` + +使用 npm **7.x** 版本创建 + +```she +npm init @galacean/galacean-app -- --template miniprogram +``` + +**根据提示**完成后续步骤后,可以使用小程序开发工具打开项目: + +![image-20210609164550721](https://gw.alipayobjects.com/zos/OasisHub/3e2df40f-6ccd-4442-85f8-69233d04b3b5/image-20210609164550721.png) + +选择对应目录即可,顺利的话可以看到: + +![image-20210609164816776](https://gw.alipayobjects.com/zos/OasisHub/04386e9c-b882-41f7-8aa6-a1bf990d578b/image-20210609164816776.png) + +### 已有项目 Pro code 使用 Galacean + +本教程假设你已经有一定开发能力,若不熟悉小程序开发,请详细阅读[小程序开发文档](https://opendocs.alipay.com/mini/developer)。 + +1. 在项目目录中打开 `Terminal`,安装依赖: + +```bash +# 使用 npm +npm install @galacean/engine --save +npm install @galacean/engine-miniprogram-adapter --save +# 使用 yarn +yarn add @galacean/engine +yarn add @galacean/engine-miniprogram-adapter +``` + +2. 在小程序项目配置文件 `app.json` 里添加下面配置项: + +```json +{ + ... + "window": { + ... + "v8WorkerPlugins": "gcanvas_runtime", + "v8Worker": 1, + "enableSkia": "true" + } +} +``` + +3. 在需要添加互动的 axml 页面里加入 canvas 标签 + +```html + +``` + +使用 `onReady` 配置 `canvas` 初始化回调。需要设置 `canvas` 的 id,后面会用到。 + +4. 在页面的 `.js` 代码文件里添加回调函数,使用 `my._createCanvas` 创建所需的 canvas 上下文,之后在 `success` 回调里使用 galacean 即可. + +注意: + +1. 使用 `import * as GALACEAN from "@galacean/engine/dist/miniprogram"` 引入小程序依赖。 +2. 需要使用『@galacean/engine-miniprogram-adapter』里的 `registerCanvas` 注册 `canvas`。 + +详情可以参考下面代码: + +```js +import * as GALACEAN from "@galacean/engine/dist/miniprogram"; +import { registerCanvas } from "@galacean/engine-miniprogram-adapter"; + +Page({ + onCanvasReady() { + my._createCanvas({ + id: "canvas", + success: (canvas) => { + // 注册 canvas + registerCanvas(canvas); + // 适配 canvas 大小 + const info = my.getSystemInfoSync(); + const { windowWidth, windowHeight, pixelRatio, titleBarHeight } = info; + canvas.width = windowWidth * pixelRatio; + canvas.height = (windowHeight - titleBarHeight) * pixelRatio; + + // 创建引擎 + const engine = new GALACEAN.WebGLEngine(canvas); + // 剩余代码和 Galacean Web 版本一致 + ... + }, + }); + } +}) +``` diff --git a/docs/assets/gc.md b/docs/assets/gc.md new file mode 100644 index 000000000..fc6dd8efb --- /dev/null +++ b/docs/assets/gc.md @@ -0,0 +1,37 @@ +--- +order: 4 +title: 资产的释放 +type: 资产工作流 +label: Resource +--- + +为了避免重复加载资源,当资源被加载完成之后,会被缓存在 _ResourceManager_ 内。缓存本身会占用内存和显存,当开发者不再需要缓存的内容时,需要手动去释放缓存的内容。 + +> 注意:资源之间是相互依赖的。 + +例如下图展示的实体包含 [MeshRenderer](/apis/core/#MeshRenderer) 组件,依赖于 [Material](/apis/core/#Material), _Material_ 可能被多个 _MeshRenderer_ 引用,如果释放 _Material_ ,那么引用此的其他 _MeshRenderer_ 则会找不到该 _Material_ 而报错。 + +![image.png](https://gw.alipayobjects.com/mdn/mybank_yulibao/afts/img/A*wXmqRIwqI18AAAAAAAAAAAAAARQnAQ) + +> 注意:JavaScript 无法追踪对象的引用。 一般在 JavaScript 等弱类型语言中,是没有提供给开发者内存管理的功能的,所有对象的内存都是通过垃圾回收机制来管理,你没有办法去判断对象什么时候会被释放,所以没有[析构函数(destructor)](https://zh.wikipedia.org/wiki/%E8%A7%A3%E6%A7%8B%E5%AD%90)去调用引用资源的释放。 + +`ResourceManager` 提供了一套基于引用计数的资源释放,需要开发者手动调用 [gc](/apis/core/#ResourceManager-gc): + +```typescript +engine.resourceManager.gc(); +``` + +## 验证资产释放 + +如果您需要验证资产是否释放成功,可按照以下步骤,在空白页打开以下示例: + + + +该示例在初始化时会通过创建 `Texture2D` 和 `Sprite` 渲染 2D 精灵,当点击右上角 GC 按钮后,`root` 节点被销毁,纹理和精灵资产的引用计数都被清空,此时这些资产会被真正销毁,分别在 `gc` 前后拍摄内存快照可以更直观地感受这个过程 + +1. gc 前: **开发者工具** -> **内存** -> **拍摄堆快照** +2. gc 后: **开发者工具** -> **内存** -> **拍摄堆快照** -> **比较** -> **选择 gc 前快照** + +image-1 + +image-1 diff --git a/docs/assets/interface.md b/docs/assets/interface.md new file mode 100644 index 000000000..9b515d424 --- /dev/null +++ b/docs/assets/interface.md @@ -0,0 +1,91 @@ +--- +order: 1 +title: 资产面板 +type: 资产工作流 +label: Resource +--- + +image-20240319102237183 + +资产面板是编辑器中一个重要的面板,它可以帮助你管理场景中使用到的所有资产。在资产面板中,你可以查看和管理场景中使用到的所有资产,例如材质、贴图、模型等等。通过资产面板,你可以添加或删除资产,以及对资产进行分类管理,从而更好的组织资产。 + +目前,编辑器支持上传或创建的资产有(**+** 表示组合文件): + +| 支持的资产 | 说明 | 交换格式 | 创建方式 | +| ------------------------------------------------ | -------------------------------------------------------------- | --------------------------------------------------- | --------- | +| 文件夹 | 类似操作系统的文件夹,可以把文件拖拽到文件夹中 | | 创建 | +| 场景 | 用于实体树管理 | | 创建 | +| 模型 | 3D 模型文件 | `.gltf`+`.bin`+`.jpg`, `.glb`+`.jpg`, .`fbx`+`.jpg` | 上传 | +| 网格 | 不可添加,只能使用内部网格和模型中的网格 | | - | +| 材质 | 用于调整渲染效果 | | 创建 | +| 纹理 | 上传图片文件创建 2D 纹理 | `.png`,`.jpg`,` .webp` | 上传 | +| 立方体纹理(TextureCube) | 用于场景天空,环境光 | `.hdr` | 上传 | +| 精灵 | 可以直接上传图片文件创建精灵(省去先创建精灵后绑定纹理的步骤) | `.png`,`.jpg`,` .webp` | 创建/上传 | +| 精灵图集(SpriteAtlas) | 把多个精灵打包成图集,用于优化 2D 资产 | | 创建 | +| 字体 | 用于制作 2D 文字 | `.ttf`, `.otf`, `.woff` | 上传 | +| 脚本 | 用于编写业务逻辑 | `.ts` | 创建 | +| 动画控制器(Animation Controller) | 用于组织动画片段和控制动画状态 | | 创建 | +| 动画片段(Animation Clip) | 预先制作好的、连续的动画数据,包含一段时间内关键帧的变化信息 | `.ts` | 创建 | +| 动画状态机脚本(Animation State Machine Script) | 用来控制和管理动画状态机行为的程序脚本 | | 创建 | +| Lottie | 支持 lottie 文件上传 | `.json`(+`.jpg`),图片支持 base64 内置和独立图片 | 上传 | +| Spine | 支持 spine 文件文件上传 | `.json` + `.atlas` + `.jpg` | 上传 | + +### 添加资产 + +为了在场景中添加资产,你可以点击资产面板上的添加按钮,或者资产面板的右键菜单中的添加选项来添加新资产。添加资产后,你可以在 **[检查器面板](/docs/interface-inspector)** 中对资产的属性进行编辑。资产面板中的资产类型非常丰富,例如材质、贴图、模型、字体等等。具体可以参照上方的表格。 + +image-20240319103341208 + + +你还可以将文件拖动到资产面板中来添加资产,组合文件可以直接选中多个文件拖进资产面板即可。 + +drag6 + + +### 组织资产 + +资产面板中的资产可以通过分类来管理,以便更好的组织资产。你可以在资产面板中创建文件夹并将资产移动到对应的文件夹中(也可以移动到左侧目录的文件夹中),以实现分类管理。资产面板中的文件夹可以嵌套,你可以创建多层级的文件夹来更好的组织资产。 + +drag7 + +资产面板提供了对资产浏览友好的工具栏,帮助你快速地查找某个或某类资产。你也可以根据你的使用习惯,对资产的浏览模式、排序方式和缩略图大小进行修改。 + +drag8 + +组织完资产后,每个资产都有一个**相对路径**,我们可以右击某个资产拷贝路径。 + +image-20240319132804611 + +这对项目开发来说很重要,因为项目中经常遇到需要异步加载资产的情况,即初始化不需要加载某个资产(甚至是场景),可以通过脚本来控制某个资产的加载。具体的语法可以看[资产](/docs/assets-load)和[场景](/docs/core-scene)的加载使用,以加载场景为例: + +```typescript +this.engine.resourceManager.load({ url: "...", type: AssetType.Scene }); +``` + +### 删除资产 + +你可以在选中一个资产后点击资产面板上的删除按钮,或者通过右键菜单中的删除选项来删除资产。删除资产时,你需要注意所删除的资产是否会影响场景中其他节点的关联性。 + +### 预览资产 + +在选中一个资产后, 右侧的 **[检查器面板](/docs/interface-inspector)** 会显示出此资产可配置的属性。不同的资产所对应的可配置项是不同的, 比如 glTF 资产会显示模型预览窗, 材质资产会显示出详细的材质配置选项 。 + +image-20240319120017637 + + +### 使用资产 + +部分资产(如 glTF 资产)支持拖拽到场景中或节点树中。 + +drag9 + + + + +### 快捷键 + +| 快捷键 | 功能 | +| -------------- | -------- | +| `⌫` / `Delete` | 删除资源 | +| `⌘` + `D` | 复制资源 | +| `⌘`+ `F` | 搜索资源 | diff --git a/docs/assets/load.md b/docs/assets/load.md new file mode 100644 index 000000000..eed8da372 --- /dev/null +++ b/docs/assets/load.md @@ -0,0 +1,143 @@ +--- +order: 3 +title: 资产的加载 +type: 资产工作流 +label: Resource +--- + +在 Galacean 中,资产加载一般由使用它的情形分为三种情况: + +- 资产被导入到编辑器中,且在某个场景中使用 +- 资产被导入到编辑器中,但没有在场景中使用 +- 资产没有被导入到编辑器中 + +> 若使用项目加载器加载项目,项目只会加载**主场景**中使用到的资源,编辑器里的其他资源不会被加载。 + +```typescript +await engine.resourceManager.load({ + type: AssetType.Project, + url: "xxx.json", +}); +``` + +> 对应地,若使用场景加载器加载某个场景,场景加载器只会加载**该场景**中使用到的资源,其他资源默认不会被加载。 + +```typescript +const scene = await engine.resourceManager.load({ + type: AssetType.Scene, + url: "xxx.json", +}); +engine.sceneManager.activeScene = scene; +``` + +> 至于那些没有在场景中使用的资产,可以使用挂载在 Engine 实例中的 [resourceManager.load](/apis/core/#Engine-resourceManager#load) 加载资源。 + +```typescript +// 若只传入 URL ,引擎会依据后缀推断加载的资产类型,如 `.png` 对应纹理, `.gltf` 则对应模型 +const gltf1 = await this.engine.resourceManager.load( + "test1.gltf" +); +// 也可以通过 `LoadItem` 定义加载的资产类型,重试次数,重试间隔等信息 +const gltf2 = await this.engine.resourceManager.load({ + type: AssetType.GLTF, + url: "test2.gltf", + retryCount: 5, + timeout: 500, + retryInterval: 500, +}); +// 也支持传入数组批量加载,返回按顺序排列的加载好的资源队列。 +const [texture2D, glTFResource] = await this.engine.resourceManager.load([ + "a.png", + "b.gltf", +]); +``` + +下面将具体介绍在运行时加载资源的: + +- 资源路径 +- 加载进度 +- 取消加载 +- 获取加载过的资产 + +## 资源路径 + +资源的 url 路径支持**相对路径**,**绝对路径**与**虚拟路径**: + +- 相对路径是针对运行时根路径而言的,若路径有误,可在开发者工具中根据加载报错信息进行调整 +- 绝对路径是完整指定文件位置的路径,如 `https://xxxx.png`,也包含 `blob` 与 `base64` +- 虚拟路径是在编辑器的资产文件中的路径,一般为 `Assets/sprite.png` + +```typescript +// 加载相对路径下的资源 +this.engine.resourceManager.load("a.png"); + +// 加载绝对路径下的资源 +this.engine.resourceManager.load("https://a.png"); + +// 加载 base64 +this.engine.resourceManager.load({ + url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==", + type: AssetType.Texture2D, +}); + +// 加载编辑器虚拟路径下的资源 +this.engine.resourceManager.load("Assets/texture.png"); +``` + +> 在编辑器中可以通过 **[资产面板](/docs/assets-interface)** -> **右键资产** -> **Copy relative path** 快速复制资产的相对路径 + +### baseUrl + +`ResourceManger` 目前也支持了 `baseUrl` 的设置: + +```typescript +engine.resourceManager.baseUrl = "https://cdn.galacean.com"; +``` + +如果设置了 `baseUrl`,加载的相对路径会和 `baseUrl` 组合: + +```typescript +engine.resourceManager.load("img/2d.png"); +``` + +上面两段代码可以得出实际的加载路径是`https://cdn.galacean.com/img/2d.png`。 + +## 加载进度 + +调用加载队列可以得到一个 [AssetPromise](/apis/core/#AssetPromise) 对象,可以使用 [onProgress](/apis/core/#AssetPromise-onProgress) 获取加载进度。 + +```typescript +this.engine.resourceManager + .load(["a.png", "b.gltf"]) + .onProgress((progress: number) => { + console.log(`当前加载进度为 ${progress}`); + }); +``` + +## 取消加载 + +_ResourceManager_ 对象中有 [cancelNotLoaded](/apis/core/#ResourceManager-cancelNotLoaded) 方法,可以通过调用此方法取消未加载完成的资源。传入 url 会取消特定的 url 的资源加载。 + +```typescript +// 取消所有未加载完的资源。 +this.engine.resourceManager.cancelNotLoaded(); +// 取消特定的 url 资源加载。 +this.engine.resourceManager.cancelNotLoaded("test.gltf"); +``` + +> 注意:目前取消加载未完成资源会抛出异常。 + +## 获取加载过的资产 + +目前加载过的资产会缓存在 _ResourceManager_ 中,如需获取加载过的资产,可以通过较为保险的 `load` 这个**异步方法**,**即使资产没有在缓存中**,该接口也会重新加载对应资源。 + +```typescript +const asset = await this.engine.resourceManager.load(assetItem); +``` + +若明确地知道这个资源现在在缓存中,也可以调用 `getFromCache` 这个**同步方法**: + +```typescript +// 获取传入的 URL 对应的资产 +const asset = this.engine.resourceManager.getFromCache(url); +``` diff --git a/docs/assets/overall.md b/docs/assets/overall.md new file mode 100644 index 000000000..97be0c6f6 --- /dev/null +++ b/docs/assets/overall.md @@ -0,0 +1,25 @@ +--- +order: 0 +title: 资产总览 +type: 资产工作流 +label: Resource +--- + +在 Galacean 中,网格,材质,纹理,精灵,图集,动画片段,动画控制器等等都属于资产。 + +## 资产工作流 + +在 Galacean 中,资产的工作流通常如下: + +```mermaid +flowchart LR + 导入资产 --> 编辑资产 --> 构建导出 --> 分发 --> 加载 +``` + +本章节将主要讲述: + +- [资产类型](/docs/assets-type):介绍**内置资产类型**和如何**自定义资产加载器** +- 编辑状态下[资产的增删改查](/docs/assets-interface) +- 构建项目后[资产如何导出并部署](/docs/assets-build) +- 运行时如何[加载资产](/docs/assets-load) +- 运行时如何[垃圾回收](/docs/assets-gc) diff --git a/docs/assets/type.md b/docs/assets/type.md new file mode 100644 index 000000000..3dc98bbb5 --- /dev/null +++ b/docs/assets/type.md @@ -0,0 +1,44 @@ +--- +order: 1 +title: 资产的类型 +type: 资产工作流 +label: Resource +--- + +Galacean 定义了一系列开箱即用的内置资产,同时也提供了灵活的定制加载能力。 + +## 内置资源类型 + +| 资源 | 加载类型 | 参考 | +| ----------- | --------------------- | -------------------------------------------------------------------------- | +| Texture2D | AssetType.Texture2D | [示例](https://galacean.antgroup.com/#/examples/latest/wrap-mode) | +| TextureCube | AssetType.HDR | [示例](https://galacean.antgroup.com/#/examples/latest/hdr-loader) | +| glTF | AssetType.GLTF | [示例](https://galacean.antgroup.com/#/examples/latest/gltf-basic) | +| 压缩纹理 | AssetType.KTX2 | [示例](https://galacean.antgroup.com/#/examples/latest/compressed-texture) | +| 环境光 | AssetType.Env | [示例](https://galacean.antgroup.com/#/examples/latest/ambient-light) | +| 图集 | AssetType.SpriteAtlas | [示例](https://galacean.antgroup.com/#/examples/latest/sprite-atlas) | +| 字体 | AssetType.Font | [示例](https://galacean.antgroup.com/#/examples/latest/text-renderer-font) | + +> 注意:环境光烘焙产物来自编辑器,或者使用 glTF Viewer,参考下图: + +![gltf viewer](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*9mGbSpQ4HngAAAAAAAAAAAAAARQnAQ) + +## 自定义资产加载器 + +用户也可以自定义加载器来加载自定义的资源: + +```typescript +@resourceLoader(FBX, ["fbx"]) +export class FBXLoader extends Loader { + load(item: LoadItem, resourceManager: ResourceManager): AssetPromise { + return new AssetPromise((resolve, reject)=> { + ... + }) + } +} +``` + +1. 通过 [@resourceLoader](/apis/core/#resourceLoader) 装饰器标注为 _ResourceLoader_,传入类型枚举和被解析的资源后缀名。上面的例子 `FBX` 是类型枚举, `["fbx"]`  是被解析资源的后缀名。 +2. 重写 [load](/apis/core/#ResourceManager-load) 方法, `load`  方法会传入 `loadItem` 和 `resourceManager` , `loadItem`  包含了加载的基信息, `resourceManager`  可以帮助加载其他引用资源。 +3. 返回 [AssetPromise](/apis/core/#AssetPromise)  对象, `resolve`  解析后的资源结果,例如 FBX 返回特定的 `FBXResource` 。 +4. 若报错则 `reject`  错误。 diff --git a/docs/core/canvas.md b/docs/core/canvas.md new file mode 100644 index 000000000..05ad6ed8f --- /dev/null +++ b/docs/core/canvas.md @@ -0,0 +1,119 @@ +--- +order: 1 +title: 画布 +group: 基础 +label: Core +--- + +Galacean Engine 封装了不同平台的画布,如 [WebCanvas](${api}rhi-webgl/WebCanvas)  支持用 [Engine](/apis/core/#Engine) 控制 [HTMLCanvasElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) 或者 [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) 。 + +image.png + +> 若无特殊说明,文档中画布一般都为 `WebCanvas` 。 + +## 基础使用 + +### 创建画布 + +在 HTML 中插入一个 `` 标签,指定一个 id: + +```html + +``` + +> 开发者要注意检查 canvas 的高度和宽度,避免出现高度或宽度的值为 **0** 导致渲染不出来。 + +创建 WebGLEngine 实例的时候会自动创建一个 WebCanvas 实例: + +```typescript +const engine = await WebGLEngine.create({ canvas: "canvas" }); + +console.log(engine.canvas); // => WebCanvas 实例 +``` + +### 基础适配 + +画布尺寸一般通过**设备像素比**控制,以 [WebCanvas](${api}rhi-webgl/WebCanvas) 为例: + +```mermaid +flowchart TD + A[HtmlCanvas.clientWidth] -->|pixelRatio| B[WebCanvas.width] + C[HtmlCanvas.clientHeight] -->|pixelRatio| D[WebCanvas.height] +``` + +若通过编辑器导出 **NPM package** 进行开发,只需在[项目导出](/docs/assets-build)渲染导出配置处控制**设备像素比**即可。 + +image.png + +或在代码中主动调用 `resizeByClientSize` 适配画布。 + +```typescript +// 使用设备像素比( window.devicePixelRatio )调整画布尺寸, +engine.canvas.resizeByClientSize(); +// 自定义像素比调整画布尺寸 +engine.canvas.resizeByClientSize(1.5); +``` + +> 当画布的显示尺寸发生变化时(比如浏览器窗口发生变化时),画面可能出现拉伸或压缩,可以通过调用 `resizeByClientSize` 来恢复正常。一般情况下这行代码已经可以满足适配的需求,如果你有更复杂的适配需求,请阅读“高级使用”部分。 + +## 高级使用 + +关于适配,核心要注意的点是**设备像素比**,以 iPhoneX 为例,设备像素比 `window.devicePixelRatio` 为 _3_,  窗口宽度 `window.innerWidth` 为 _375_,屏幕物理像素宽度则为:375 * 3 = *1125\*。 + +渲染压力和屏幕物理像素高宽成正比,物理像素越大,渲染压力越大,也就越耗电。画布的高宽建议通过 [WebCanvas](${api}rhi-webgl/WebCanvas) 暴露的 API 设置,不建议使用原生 canvas 的 API ,如 `canvas.width` 或 `canvas.style.width` 这些方法修改。 + +> ️ **注意**:有些前端脚手架会插入以下标签修改页面的缩放比: +> +> `` +> +> 这行代码会把 `window.innerWidth` 的值从 375 修改为 1125。 + +除了 `resizeByClientSize` 自动适配,推荐使用以下两种模式: + +### 节能模式 + +考虑到移动端设备虽然是高清屏幕(设别像素比高)但实际显卡性能并不能很好地满足高清实时渲染的性能要求的情况(**3 倍屏和 2 倍屏渲染面积比是 9:4,3 倍屏较容易造成手机发烫**),此模式下引擎通过对画布缩放拉伸来达到适配的目的。代码如下: + +```typescript +const canvas = document.getElementById("canvas"); +const webcanvas = new WebCanvas(canvas); +const pixelRatio = window.devicePixelRatio; // 如果已经设置 meta scale,请设置为 1 +const scale = 3 / 2; // 3 倍高清屏按 2 倍屏来计算画布尺寸 + +/** + * 设置节能模式,默认全屏,也可以自己设置任意高宽 + */ +webcanvas.width = (window.innerWidth * pixelRatio) / scale; +webcanvas.height = (window.innerHeight * pixelRatio) / scale; +webcanvas.setScale(scale, scale); // 拉伸画布 +``` + +如果已经通过 CSS 设置了画布高宽(比如 `width: 100vw; height: 100vh;`),那么可以通过 `resizeByClientSize` 传参实现画布的缩放: + +```typescript +const canvas = document.getElementById("canvas"); +const webcanvas = new WebCanvas(canvas); +const scale = 2 / 3; // 3 倍高清屏按 2 倍屏来计算画布尺寸 + +webcanvas.resizeByClientSize(scale); // 拉伸画布 +``` + +### 固定宽度模式 + +某些情况下,比如设计稿固定 750 宽度的情况,开发者有可能会写死画布宽度来降低适配成本。代码如下: + +```typescript +import { WebCanvas } from "@galacean/engine"; + +const canvas = document.getElementById("canvas"); +const webcanvas = new WebCanvas(canvas); +const fixedWidth = 750; // 固定 750 宽度 + +/** + * 设置固定宽度模式 + */ +const scale = window.innerWidth / fixedWidth; +webcanvas.width = fixedWidth; +webcanvas.height = window.innerHeight / scale; +webcanvas.setScale(scale, scale); // 拉伸画布 +``` diff --git a/docs/core/clone.md b/docs/core/clone.md new file mode 100644 index 000000000..4cf66c382 --- /dev/null +++ b/docs/core/clone.md @@ -0,0 +1,105 @@ +--- +order: 5 +title: 克隆 +type: 核心 +label: Core +--- + + +节点克隆是运行时的常用功能,同时节点克隆也会附带克隆其绑定的组件。例如在初始化阶段根据配置动态创建一定数量相同的实体,然后根据逻辑规则摆放到场景不同的位置。这里会对脚本的克隆细节进行详细讲解。 + +## 实体的克隆 +非常简单,直接调用实体的 [clone()](/apis/design/#IClone-clone) 方法即可完成实体以及附属组件的克隆。 +```typescript +const cloneEntity = entity.clone(); +``` + +## 脚本的克隆 +脚本的本质也是组件,所以当我们调用实体的 [clone()](/apis/design/#IClone-clone) 函数时,引擎不仅会对引擎内置组件进行克隆,还会对自定义脚本进行克隆。引擎内置组件的克隆规则官方已经完成定制,同样我们也将脚本的克隆能力和规则定制开放给了开发者。脚本字段默认的克隆方式为浅拷贝,例如我们对脚本的字段值进行修改后再克隆,克隆后的脚本将保持修改后的值,无需增加任何额外的编码。以下为自定义脚本的克隆案例: +```typescript +// define a custom script +class CustomScript extends Script{ + /** boolean type.*/ + a:boolean = false; + + /** number type.*/ + b:number = 1; + + /** class type.*/ + c:Vector3 = new Vector3(0,0,0); +} + +// Init entity and script +const entity = engine.createEntity(); +const script = entity.addComponent(CustomScript); +script.a = true; +script.b = 2; +script.c.set(1,1,1); + +// Clone logic +const cloneEntity = entity.clone(); +const cloneScript = cloneEntity.getComponent(CustomScript); +console.log(cloneScript.a); // output is true. +console.log(cloneScript.b); // output is 2. +console.log(cloneScript.c); // output is (1,1,1). +``` +### 克隆装饰器 +除了默认的克隆方式外,引擎还提供了“克隆装饰器“对脚本字段的克隆方式进行定制。引擎内置四种克隆装饰: + +| 装饰器名称 | 装饰器释义 | +| :--- | :--- | +| [ignoreClone](/apis/core/#ignoreClone) | 克隆时对字段进行忽略。 | +| [assignmentClone](/apis/core/#assignmentClone) | ( 默认值,和不添加任何克隆装饰器等效) 克隆时对字段进行赋值。如果是基本类型则会拷贝值,如果是引用类型则会拷贝其引用地址。 | +| [shallowClone](/apis/core/#shallowClone) | 克隆时对字段进行浅克隆。克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。| +| [deepClone](/apis/core/#deepClone) | 克隆时对字段进行深克隆。克隆后会保持自身引用独立,并且其内部所有深层字段均保持完全独立。| + +我们将上面的案例稍加修改,分别对 `CustomScript` 中的四个字段添加了不同的“克隆装饰器“。由于 `shallowClone` 和 `deepCone`  较复杂,我们对字段 `c` 和 `d` 增加了额外的打印输出进行进一步讲解。 +```typescript +// define a custom script +class CustomScript extends Script{ + /** boolean type.*/ + @ignoreClone + a:boolean = false; + + /** number type.*/ + @assignmentClone + b:number = 1; + + /** class type.*/ + @shallowClone + c:Vector3[] = [new Vector3(0,0,0)]; + + /** class type.*/ + @deepClone + d:Vector3[] = [new Vector3(0,0,0)]; +} + +// Init entity and script +const entity = engine.createEntity(); +const script = entity.addComponent(CustomScript); +script.a = true; +script.b = 2; +script.c[0].set(1,1,1); +script.d[0].set(1,1,1); + +// Clone logic +const cloneEntity = entity.clone(); +const cloneScript = cloneEntity.getComponent(CustomScript); +console.log(cloneScript.a); // output is false,ignoreClone will ignore the value. +console.log(cloneScript.b); // output is 2,assignmentClone is just assignment the origin value. +console.log(cloneScript.c[0]); // output is Vector3(1,1,1),shallowClone clone the array shell,but use the same element. +console.log(cloneScript.d[0]); // output is Vector3(1,1,1),deepClone clone the array shell and also clone the element. + +cloneScript.c[0].set(2,2,2); // change the field c[0] value to (2,2,2). +cloneScript.d[0].set(2,2,2); // change the field d[0] value to (2,2,2). + +console.log(script.c[0]); // output is (2,2,2). bacause shallowClone let c[0] use the same reference with cloneScript's c[0]. +console.log(script.d[0]); // output is (1,1,1). bacause deepClone let d[0] use the different reference with cloneScript's d[0]. +``` +- 注意: + + - `shallowClone` 和 `deepClone` 通常用于 *Object*、*Array* 和 *Class* 类型。 + - `shallowClone` 克隆后会保持自身引用独立,并使用赋值的方式克隆其内部所有字段(如果内部字段是基本类型则会拷贝值,如果内部字段是引用类型则会拷贝其引用地址)。 + - `deepClone` 为深克隆,会对属性进行深度递归,至于属性的子属性如何克隆,取决于子属性的装饰器。 + - 如果克隆装饰器不能满足诉求,可以通过实现 [_cloneTo()](/apis/design/#IClone-cloneTo) 方法追加自定义克隆。 + diff --git a/docs/core/component.md b/docs/core/component.md new file mode 100644 index 000000000..c909cf945 --- /dev/null +++ b/docs/core/component.md @@ -0,0 +1,98 @@ +--- +order: 4 +title: 组件 +type: 核心 +label: Core +--- + +在 Galacean 引擎中,[Entity](/apis/core/#Entity) 不具备渲染模型等实际的功能,这些功能是通过加载 [Component](/apis/core/#Component) 组件类来实现的。例如,如果想让一个 _Entity_ 变成一个相机,只需要在该 _Entity_ 上添加相机组件 [Camera](/apis/core/#Camera)。这种基于组件的功能扩展方式注重将程序按照功能独立封装,在使用的时候按照需要组合添加,非常有利于降低程序耦合度并提升代码复用率。 + +常用组件: + +| 名称 | 描述 | +| :---------------------------------------------------- | :------------- | +| [Camera](/apis/core/#Camera) | 相机 | +| [MeshRenderer](/apis/core/#MeshRenderer) | 静态模型渲染器 | +| [SkinnedMeshRenderer](/apis/core/#SkinnedMeshRenderer) | 骨骼模型渲染器 | +| [Animator](/apis/core/#Animator) | 动画控制组件 | +| [DirectLight](/apis/core/#DirectLight) | 方向光 | +| [PointLight](/apis/core/#PointLight) | 点光源 | +| [SpotLight](/apis/core/#SpotLight) | 聚光灯 | +| [ParticleRenderer](/apis/core/#ParticleRenderer) | 粒子系统 | +| [BoxCollider](/apis/core/#BoxCollider) | 盒碰撞体 | +| [SphereCollider](/apis/core/#SphereCollider) | 球碰撞体 | +| [PlaneCollider](/apis/core/#PlaneCollider) | 平面碰撞体 | +| [Script](/apis/core/#Script) | 脚本 | + +## 编辑器使用 + +从 **[层级面板](/docs/interface-hierarchy)** 或场景中选择一个实体后,检查器将显示出当前选中节点挂载的所有组件,组件名显示在左上角 + +Name + +你可以在检查器中控制它是否 enabled + +Enable + +如果不需要它也可以将它删除 + +Delete + +或者编辑它的各种属性 + +Edit + +如果是个空节点,你可以点击 `Add Component` 按钮来为当前实体添加新的组件。 + +image-20230926112713126 + +## 脚本使用 + +### 添加组件 + +我们使用 [addComponent(Component)](/apis/core/#Entity-addComponent) 添加组件,例如给 `Entity`  添加“平行光”组件([DirectLight](/apis/core/#DirectLight)): + +```typescript +const lightEntity = rootEntity.createChild("light"); +const directLight = lightEntity.addComponent(DirectLight); +directLight.color = new Color(0.3, 0.3, 1); +directLight.intensity = 1; +``` + +### 查找实体上的组件 + +当我们需要获取某一实体上的组件, [getComponent](/apis/core/#Entity-getComponent) 这个 API 会帮你查找目标组件。 + +```typescript +const component = newEntity.getComponent(Animator); +``` + +有些时候可能会有多个同一类型的组件,而上面的方法只会返回第一个找到的组件。如果需要找到所有组件可以用 [getComponents](/apis/core/#Entity-getComponents): + +```typescript +const components = []; +newEntity.getComponents(Animator, components); +``` + +在 glTF 这种资产得到的实体里,我们可能不知道目标组件位于哪个实体,这时可以使用[getComponentsIncludeChildren](/apis/core/#Entity-getComponentsIncludeChildren)进行查找。 + +```typescript +const components = []; +newEntity.getComponentsIncludeChildren(Animator, components); +``` + +### 获得组件所在的实体 + +继续开头添加组件的例子。可以直接获得组件所在的实体: + +```typescript +const entity = directLight.entity; +``` + +### 状态 + +暂时不使用某组件时,可以主动调用组件的 [enabled](/apis/core/#Component-enabled) + +```typescript +directLight.enabled = false; +``` diff --git a/docs/core/engine.md b/docs/core/engine.md new file mode 100644 index 000000000..ef7209320 --- /dev/null +++ b/docs/core/engine.md @@ -0,0 +1,94 @@ +--- +order: 0 +title: 引擎 +type: 核心 +label: Core +--- + +`Engine` 在 Galacean Engine 中扮演着总控制器的角色,主要包含了**画布**、**渲染控制**和**引擎子系统管理**等三大功能: + +- **[画布](/docs/core-canvas)**:主画布相关的操作,如修改画布宽高等。 +- **渲染控制**: 控制渲染的执行/暂停/继续、垂直同步等功能。 +- **引擎子系统管理**: + - [场景管理](/docs/core-scene) + - [资源管理](/docs/assets-overall) + - [物理系统](/docs/physics-overall) + - [交互系统](/docs/input) + - [XR 系统](/docs/xr-overall) +- **执行环境的上下文管理**:控制 WebGL 等执行环境的上下文管理。 + +## 初始化 + +为了方便用户直接创建 web 端 engine,Galacean 提供了 [WebGLEngine](${api}rhi-webgl/WebGLEngine) : + +```typescript +const engine = await WebGLEngine.create({ canvas: "canvas" }); +``` + +> `WebGLEngine.create` 不仅承担着实例化引擎的职责,还负责渲染上下文的配置和某些子系统的初始化。 + +### 渲染上下文 + +开发者可以在 [导出界面](/docs/assets-build) 设置上下文的渲染配置。 + + + +您也可以通过脚本设置 [WebGLEngine](${api}rhi-webgl/WebGLEngine) 的第三个参数 [WebGLGraphicDeviceOptions](${api}rhi-webgl/WebGLGraphicDeviceOptions) 来进行管理,拿**画布透明**来举例,引擎默认是将画布的透明通道开启的,即画布会和背后的网页元素混合,如果需要关闭透明,可以这样设置: + +```typescript +const engine = await WebGLEngine.create({ + canvas: htmlCanvas, + graphicDeviceOptions: { alpha: false }, +}); +``` + +类似的,可以用 `webGLMode` 控制 WebGL1/2,除 `webGLMode` 外的属性将透传给上下文,详情可参考 [getContext 参数释义](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext#parameters)。 + +### 物理系统 + +可参考 [物理系统](/docs/physics-overall) 文档 + +### 交互系统 + +可参考 [交互系统](/docs/input) 文档 + +### XR 系统 + +可参考 [XR 系统](/docs/xr-overall) 文档 + +## 属性 + +| 属性名称 | 属性释义 | +| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [time](/apis/core/#Engine-time) | 引擎时间相关的信息。 | +| [vSyncCount](/apis/core/#Engine-vSyncCount) | 引擎默认开启[垂直同步](https://baike.baidu.com/item/%E5%9E%82%E7%9B%B4%E5%90%8C%E6%AD%A5/7263524?fromtitle=V-Sync&fromid=691778)且刷新率 `vSyncCount` 为`1`,即与屏幕刷新率保持一致。如果 `vSyncCount` 设置为`2`,则每刷新 2 帧,引擎更新一次。 | +| [resourceManager](/apis/core/#Engine-resourceManager) | 资源管理 | +| [sceneManager](/apis/core/#Engine-sceneManager) | 场景管理。_Engine_ 是总控制器,_Scene_ 作为场景单元,可以方便大型场景的实体管理;_Camera_ 作为组件挂载在 _Scene_ 中的某一实体下,和现实中的摄像机一样,可以选择拍摄 _Scene_ 中的任何实体 ,最后渲染到屏幕上的一块区域或者离屏渲染。 | +| [inputManager](/apis/core/#Engine-inputManager) | 交互管理 | + +### 刷新率 + +默认情况下引擎采用垂直同步模式并使用 [vSyncCount](/apis/core/#Engine-vSyncCount) 控制渲染刷新率,该模式才渲染帧会等待屏幕的垂直同步信号, [vSyncCount](/apis/core/#Engine-vSyncCount) 代表了渲染帧之间期望的屏幕同步信号次数,默认值为 1,该属性的值必须为整数,例如我们想在一个屏幕刷新率为 60 帧的设备上期望每秒渲染 30 帧,则可以将该值设置为 2。 + +另外用户还可以关闭垂直同步,即将 [vSyncCount](/apis/core/#Engine-vSyncCount) 设置为 0,然后设置 [targetFrameRate](/apis/core/#Engine-targetFrameRate) 为期望的帧数值,该模式下的渲染不考虑垂直同步信号,而是,如 120 表示 120 帧,即每秒期望刷新 120 次。 + +```typescript +// 垂直同步 +engine.vSyncCount = 1; +engine.vSyncCount = 2; + +// 非垂直同步 +engine.vSyncCount = 0; +engine.targetFrameRate = 120; +``` + +> ⚠️ 不建议使用非垂直同步 + +## 方法 + +| 方法名称 | 方法释义 | +| ------------------------------------ | ------------------ | +| [run](/apis/core/#Engine-run) | 执行引擎渲染帧循环 | +| [pause](/apis/core/#Engine-pause) | 暂停引擎渲染帧循环 | +| [resume](/apis/core/#Engine-resume) | 恢复引擎渲渲染循环 | +| [destroy](/apis/core/#Engine-destroy) | 销毁引擎 | diff --git a/docs/core/entity.md b/docs/core/entity.md new file mode 100644 index 000000000..d665ca1c3 --- /dev/null +++ b/docs/core/entity.md @@ -0,0 +1,122 @@ +--- +order: 3 +title: 实体 +type: 核心 +label: Core +--- + +## 编辑器使用 + +**[层级面板](/docs/interface-hierarchy)** 位于编辑器的最左侧,它以树状结构显示当前场景中的所有节点,场景节点是所有其他节点的父节点,包括相机、灯光、网格等等。在节点面板上方有一个搜索框,可以模糊搜索场景中的节点,来快速定位。通过节点面板,你可以添加或删除节点,通过拖拽的方式来排序从而更好的组织节点。 + +image-20230925173904478 + +### 新增节点 + +要新增节点,你可以点击节点面板上的添加按钮,或右键某个节点后选择添加子节点。添加完成后,你可以在 **[检查器面板](/docs/interface-inspector)** 中对新节点的属性进行编辑。如果使用新增节点按钮, 你还可以快速创建立方体/球体等基本模型 + +### 编辑节点 + +点击节点,你就可以对它进行编辑,在右侧的 **[检查器面板](/docs/interface-inspector)** 中你可以编辑它的名字 + +Name + +是否激活 + +IsActive + +Transform + +Transform + +以及为它增删组件 + +AddComponent + +### 删除节点 + +选中一个节点后,可以点击节点面板上的删除按钮或通过右键菜单中的删除选项来删除节点。删除节点会删除节点及其所有的子节点。所以在删除节点时,你需要注意所删除的节点是否会影响场景中其他节点。 + +### 节点排序 + +为了更好的组织节点,你可以通过拖拽的方式来排序节点。选中一个节点后,可以通过鼠标左键拖拽来改变节点在层级树中的位置。glTF 模型节点不能够调整 scale 属性, 所以通常情况下,你需要把 glTF 节点拖拽到一个 entity 节点下, 然后调整 entity 节点的 scale 属性。有关 glTF 详细的介绍可参见后续章节。 + +### 节点搜索 + +节点面板上方有一个搜索框,用户可以输入节点的名称来搜索场景中的节点。搜索框支持模糊搜索,你可以输入节点名称的部分字符来查找节点。 + +### 节点隐藏 + +每个实体节点右侧都有一个眼睛按钮,点击可以切换节点在场景中的显示/隐藏状态。需要注意的是, 此处对节点显示状态的调整仅是工作区的修改,而非在 **[检查器面板](/docs/interface-inspector)** 中的 `isActive` 的属性。 + +## 脚本使用 + +### 创建新实体 + +在[场景](/docs/core-scene)中已经介绍了如何获取激活场景。在新场景中,我们通常会先添加根节点: + +```typescript +const scene = engine.sceneManager.activeScene; +const rootEntity = scene.createRootEntity(); +``` + +一般以添加子实体的方式创建新实体: + +```typescript +const newEntity = rootEntity.createChild("firstEntity"); +``` + +当然,也可以直接创建实体。但这种实体是游离的,在关联层级树上的实体之前不显示在场景中。 + +```typescript +const newEntity = new Entity(engine, "firstEntity"); +rootEntity.addChild(newEntity); +``` + +### 删除实体 + +某个实体在场景中不再需要时,我们可以删除它: + +```typescript +rootEntity.removeChild(newEntity); +``` + +值得注意的是,这种方式仅仅是将物体从层级树上释放出来,不在场景中显示。如果彻底销毁还需要: + +```typescript +newEntity.destroy(); +``` + +### 查找子实体 + +在已知父实体的情况下,通常我们通过父实体来获得子实体: + +```typescript +const childrenEntity = newEntity.children; +``` + +如果明确知道子实体在父实体中的 _index_ 可以直接使用 [getChild](/apis/core/#Entity-getChild): + +```typescript +newEntity.getChild(0); +``` + +如果不清楚子实体的 index,可以使用 [findByName](/apis/core/#Entity-findByName) 通过名字查找。`findByName` 不仅会查找子实体,还会查找孙子实体。 + +```typescript +newEntity.findByName("model"); +``` + +如果有同名的实体可以使用 [findByPath](/apis/core/#Entity-findByPath) 传入路径进行逐级查找,使用此 API 也会一定程度上提高查找效率。 + +```typescript +newEntity.findByPath("parent/child/grandson"); +``` + +### 状态 + +暂时不使用某实体时,可以通过调用实体的 [isActive](/apis/core/#Entity-isActive) 停止激活。同时该实体下的组件被动`component.enabled = false` + +```typescript +newEntity.isActive = false; +``` diff --git a/docs/core/math.md b/docs/core/math.md new file mode 100644 index 000000000..7a361b20c --- /dev/null +++ b/docs/core/math.md @@ -0,0 +1,432 @@ +--- +order: 8 +title: 数学库 +type: 核心 +label: Core +--- + +在一个渲染场景中,我们经常会对物体进行平移、旋转、缩放等操作(这些操作我们统一称为 [变换](/docs/core-transform) ),从而达到我们想要的互动效果。而这些变换的计算,我们一般都是通过向量、四元数、矩阵等来实现的,为此我们提供一个数学库来完成 *向量* 、*四元数* 、*矩阵* 等相关运算。除此之外,数学库还提供了更为丰富的类来帮助我们描述空间中的 *点* *线* *面* *几何体*,以及判断它们在三维空间中的相交、位置关系等。 + + +| 类型 | 解释 | +| :--- | :--- | +| [BoundingBox](/apis/math/#BoundingBox) | AABB 包围盒 | +| [BoundingFrustum](/apis/math/#BoundingFrustum) | 视锥体 | +| [BoundingSphere](/apis/math/#BoundingSphere) | 包围球 | +| [CollisionUtil](/apis/math/#CollisionUtil) | 提供很多静态方式,用来判断空间中各个物体之间的相交、位置关系等 | +| [Color](/apis/math/#Color) | 颜色类,使用 RGBA 描述 | +| [MathUtil](/apis/math/#MathUtil) | 工具类,提供比较、角度弧度转换等常用计算 | +| [Matrix](/apis/math/#Matrix) | 默认的4x4矩阵,提供矩阵基本运算,变换相关运算 | +| [Matrix3x3](/apis/math/#Matrix3x3) | 3x3矩阵,提供矩阵基本运算,变换相关运算 | +| [Plane](/apis/math/#Plane) | 平面类,用来描述三维空间中的平面 | +| [Quaternion](/apis/math/#Quaternion) | 四元数,包含x、y、z、w分量,负责旋转相关的运算 | +| [Ray](/apis/math/#Ray) | 射线类,用来描述三维空间中的射线 | +| [Vector2](/apis/math/#Vector2) | 二维向量,包含x、y分量 | +| [Vector3](/apis/math/#Vector3) | 三维向量,包含x、y、z分量 | +| [Vector4](/apis/math/#Vector4) | 四维向量,包含x、y、z、w分量 | + +## 向量 + +向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)。你可以把向量想像成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。那么这个藏宝图的指示一共有3个向量。向量可以在任意维度(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。 + +在 Galacean 引擎中,向量用来表示物体坐标(position)、旋转(rotation)、缩放(scale)、颜色(color)。 + +```typescript +import { Vector3 } from '@galacean/engine-math'; + +// 创建默认三维向量,即 x,y,z 分量均为0 +const v1 = new Vector3(); + +// 创建三维向量,并用给定值初始化 x,y,z 分量 +const v2 = new Vector3(1, 2, 3); + +// 设置指定值 +v1.set(1, 2, 2); + +// 获取各个分量 +const x = v1.x; +const y = v1.y; +const z = v1.z; + +// 向量相加,静态方式 +const out1 = new Vector3(); +Vector3.add(v1, v2, out1); + +// 向量相加,实例方式 +const out2 = v1.add(v2); + +// 向量的标量长度 +const len: number = v1.length(); + +// 向量归一化 +v1.normalize(); + +// 克隆一个向量 +const c1 = v1.clone(); + +// 将向量的值克隆到另外一个向量 +const c2 = new Vector3(); +v1.cloneTo(c2); + +``` +## 四元数 + +四元数是简单的超复数,而在图形引擎中,四元数主要用于三维旋转([四元数于三维旋转的关系](https://krasjet.github.io/quaternion/quaternion.pdf)),能够表示旋转的不止四元数,还有欧拉角、轴角、矩阵等形式,之所以选择四元数,主要有以下几个优势: + +- 解决了万向节死锁的问题 +- 只需要存储4个浮点数,相比矩阵来说更轻量 +- 无论是求逆、串联等操作,相比矩阵更为高效 + +在 Galacean 引擎中,也是使用四元数来进行旋转相关运算,并提供欧拉角、矩阵等到四元数的转换API。 + +```typescript +import { Vector3, Quaternion, MathUtil } from '@galacean/engine-math'; + +// 创建默认四元数,即 x,y,z 分量均为0,w 分量为1 +const q1 = new Quaternion(); + +// 创建四元数,并用给定值初始化 x,y,z,w 分量 +const q2 = new Quaternion(1, 2, 3, 4); + +// 设置指定值 +q1.set(1, 2, 3, 4); + +// 判断两个四元数的值是否相等 +const isEqual: boolean = Quaternion.equals(q1, q2); + +const xRad = Math.PI * 0.2; +const yRad = Math.PI * 0.5; +const zRad = Math.PI * 0.3; + +// 根据 yaw、pitch、roll 生成四元数 +const out1 = new Quaternion(); +Quaternion.rotationYawPitchRoll(yRad, xRad, zRad, out1); + +// 根据 x,y,z 轴的旋转欧拉角(弧度)生成四元数 +const out2 = new Quaternion(); +// 等价于 Quaternion.rotationYawPitchRoll(yRad, xRad, zRad, out2) +Quaternion.rotationEuler(xRad, yRad, zRad, out2); + +// 绕 X、Y、Z 轴旋转生成四元数,我们以绕 X 轴为例 +const out3 = new Quaternion(); +Quaternion.rotationX(xRad, out3); + +// 当前四元数依次绕 X、Y、Z 轴旋转 +const q3 = new Quaternion(); +q3.rotateX(xRad).rotateY(yRad).rotateZ(zRad); + +// 获取当前四元数的欧拉角(弧度) +const eulerV = new Vector3(); +q3.toEuler(eulerV); + +// 弧度转角度 +eulerV.scale(MathUtil.radToDegreeFactor); +``` + +## 矩阵 + +在 3D 图形引擎中,计算可以在多个不同的笛卡尔坐标空间中执行,从一个坐标空间到另一个坐标空间需要使用变换矩阵,而我们数学库中的Matrix模块正是为提供这种能力而存在的。 + +在 Galacean 引擎中,有局部坐标、全局坐标、观察坐标、裁剪坐标等,而物体在这些坐标之间的转换,正是通过转换矩阵来完成的。 + +```typescript +import { Vector3, Matrix3x3, Matrix } from '@galacean/engine-math'; + +// 创建默认4x4矩阵,默认为单位矩阵 +const m1 = new Matrix(); + +// 创建4x4矩阵,并按给定值初始化 +const m2 = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); + +// 将 m2 设置为单位矩阵 +m2.identity(); + +// 判断两个矩阵的值是否相等 true +const isEqual1: boolean = Matrix.equals(m1, m2); + +// 矩阵相乘 静态方式 +const m3 = new Matrix(1, 2, 3.3, 4, 5, 6, 7, 8, 9, 10.9, 11, 12, 13, 14, 15, 16); +const m4 = new Matrix(16, 15, 14, 13, 12, 11, 10, 9, 8.88, 7, 6, 5, 4, 3, 2, 1); +const out1 = new Matrix(); +Matrix.multiply(m3, m4, out1); + +// 矩阵相乘,实例方式 +const out2 = m3.multiply(m4); + +// 判断两个矩阵的值是否相等 true +const isEqual2: boolean = Matrix.equals(out1, out2); + +// 求矩阵行列式 +const m5 = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10.9, 11, 12, 13, 14, 15, 16); +const det: number = m5.determinant(); + +// 4x4矩阵转3x3矩阵 +const m6 = new Matrix3x3(); +m6.setValueByMatrix(m5); + +// 创建4x4矩阵,并按给定值初始化 +const m7 = new Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10.9, 11, 12, 13, 14, 15, 16); + +// 求矩阵的转置矩阵,静态方式 +Matrix.transpose(m7, m7); + +// 求矩阵的转置矩阵。实例方式 +m7.transpose(); + +// 绕 Y 轴旋转生成4x4矩阵 +const axis = new Vector3(0, 1, 0); +const out4 = new Matrix(); +Matrix.rotationAxisAngle(axis, Math.PI * 0.25, out4); +``` + +## Color + +```typescript +import { Color } from "@galacean/engine-math"; + +// 创建 Color 对象 +const color1 = new Color(1, 0.5, 0.5, 1); +const color2 = new Color(); +color2.r = 1; +color2.g = 0.5; +color2.b = 0.5; +color2.a = 1; + +// linear 空间转 gamma 空间 +const gammaColor = new Color(); +color1.toGamma(gammaColor); + +// gamma 空间转 linear 空间 +const linearColor = new Color(); +color2.toLinear(linearColor); +``` + +## 平面 +```typescript +import { Plane, Vector3 } from "@galacean/engine-math"; + +// 通过三角形的三个顶点创建平面 +const point1 = new Vector3(0, 1, 0); +const point2 = new Vector3(0, 1, 1); +const point3 = new Vector3(1, 1, 0); +const plane1 = new Plane(); +Plane.fromPoints(point1, point2, point3, plane1); +// 通过平面的法线以及法线距离原点距离创建平面 +const plane2 = new Plane(new Vector3(0, 1, 0), -1); +``` + +## 包围盒 + +```typescript +import { BoundingBox, BoundingSphere, Matrix, Vector3 } from "@galacean/engine-math"; + +// 通过不同的方式创建同样的包围盒 +const box1 = new BoundingBox(); +const box2 = new BoundingBox(); +const box3 = new BoundingBox(); + +// 通过中心点和盒子范围来创建 +BoundingBox.fromCenterAndExtent(new Vector3(0, 0, 0), new Vector3(1, 1, 1), box1); + +// 通过很多点来创建 +const points = [ + new Vector3(0, 0, 0), + new Vector3(-1, 0, 0), + new Vector3(1, 0, 0), + new Vector3(0, 1, 0), + new Vector3(0, 1, 1), + new Vector3(1, 0, 1), + new Vector3(0, 0.5, 0.5), + new Vector3(0, -0.5, 0.5), + new Vector3(0, -1, 0.5), + new Vector3(0, 0, -1), +]; +BoundingBox.fromPoints(points, box2); + +// 通过包围球来创建 +const sphere = new BoundingSphere(new Vector3(0, 0, 0), 1); +BoundingBox.fromSphere(sphere, box3); + +// 通过矩阵来对包围盒进行变换 +const box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); +const matrix = new Matrix( + 2, 0, 0, 0, + 0, 2, 0, 0, + 0, 0, 2, 0, + 1, 0.5, -1, 1 +); +const newBox = new BoundingBox(); +BoundingBox.transform(box, matrix, newBox); + +// 合并两个包围盒 box1, box2 成为一个新的包围盒 box +BoundingBox.merge(box1, box2, box); + +// 获取包围盒的中心点和范围 +const center = new Vector3(); +box.getCenter(center); +const extent = new Vector3(); +box.getExtent(extent); + +// 获取包围盒的8个顶点 +const corners = [ + new Vector3(), new Vector3(), new Vector3(), new Vector3(), + new Vector3(), new Vector3(), new Vector3(), new Vector3() +]; +box.getCorners(corners); +``` + +## 包围球 +```typescript +import { BoundingBox, BoundingSphere, Vector3 } from "@galacean/engine-math"; + +// 通过不同方式来创建包围球 +const sphere1 = new BoundingSphere(); +const sphere2 = new BoundingSphere(); + +// 通过很多点来创建 +const points = [ + new Vector3(0, 0, 0), + new Vector3(-1, 0, 0), + new Vector3(0, 0, 0), + new Vector3(0, 1, 0), + new Vector3(1, 1, 1), + new Vector3(0, 0, 1), + new Vector3(-1, -0.5, -0.5), + new Vector3(0, -0.5, -0.5), + new Vector3(1, 0, -1), + new Vector3(0, -1, 0), +]; +BoundingSphere.fromPoints(points, sphere1); + +// 通过包围盒来创建 +const box = new BoundingBox(new Vector3(-1, -1, -1), new Vector3(1, 1, 1)); +BoundingSphere.fromBox(box, sphere2); +``` + +## 视锥体 +```typescript +import { BoundingBox, BoundingSphere, BoundingFrustum,Matrix, Vector3 } from "@galacean/engine-math"; + +// 根据 VP 矩阵创建视锥体,实际项目中,一般从相机中获取 view matrix 和 projection matrix +const viewMatrix = new Matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -20, 1); +const projectionMatrix = new Matrix(0.03954802080988884, 0, 0, 0, 0, 0.10000000149011612, 0, 0, 0, 0, -0.0200200192630291, 0, -0, -0, -1.0020020008087158, 1); +const vpMatrix = new Matrix(); +Matrix.multiply(projectionMatrix, viewMatrix, vpMatrix); +const frustum = new BoundingFrustum(vpMatrix); + +// 判断是否和 AABB 包围盒相交 +const box1 = new BoundingBox(new Vector3(-2, -2, -2), new Vector3(2, 2, 2)); +const isIntersect1 = frustum.intersectsBox(box1); +const box2 = new BoundingBox(new Vector3(-32, -2, -2), new Vector3(-28, 2, 2)); +const isIntersect2 = frustum.intersectsBox(box2); + +// 判断是否和包围球相交 +const sphere1 = new BoundingSphere(); +BoundingSphere.fromBox(box1, sphere1); +const isIntersect3 = frustum.intersectsSphere(sphere1); +const sphere2 = new BoundingSphere(); +BoundingSphere.fromBox(box2, sphere2); +const isIntersect4 = frustum.intersectsSphere(sphere2); +``` +## 射线 + +```typescript +import { BoundingBox, BoundingSphere, Plane, Ray, Vector3 } from "@galacean/engine-math"; + +// 创建 ray +const ray = new Ray(new Vector3(0, 0, 0), new Vector3(0, 1, 0)); +const plane = new Plane(new Vector3(0, 1, 0), -3); +// 判断射线是否和平面相交,相交的话 distance 为射线到平面距离,不相交的话 distance 为 -1 +let distance = ray.intersectPlane(plane); + +const sphere = new BoundingSphere(new Vector3(0, 5, 0), 1); +// 判断射线是否和包围球相交,相交的话 distance 为射线到平面距离,不相交的话 distance 为 -1 +distance = ray.intersectSphere(sphere); + +const box = new BoundingBox(); +BoundingBox.fromCenterAndExtent(new Vector3(0, 20, 0), new Vector3(5, 5, 5), box); +// 判断射线是否和包围盒 (AABB) 相交,相交的话 distance 为射线到平面距离,不相交的话 distance 为 -1 +distance = ray.intersectBox(box); + +// 到射线起点指定距离的点 +const out = new Vector3(); +ray.getPoint(10, out); + +``` + +## Rand + +数学库新增了随机数生成器 `Rand` ,他基于 `xorshift128+` 算法实现(被同样应用在 V8,Safari 与 Firefox 中),是一种快速、高质量且周期完整的伪随机数生成算法。 + +```typescript +// 初始化随机数生成器实例 +const rand = new Rand(0, 0xf3857f6f); + +// 生成区间在[0, 0xffffffff)的随机整数 +const num1 = rand.randomInt32(); +const num2 = rand.randomInt32(); +const num3 = rand.randomInt32(); + +// 生成区间在[0, 1)的随机数 +const num4 = rand.random(); +const num5 = rand.random(); +const num6 = rand.random(); + +// 重置种子 +rand.reset(0, 0x96aa4de3); +``` + +## CollisionUtil + +```typescript +import { + BoundingBox, + BoundingSphere, + BoundingFrustum, + Matrix, + Plane, + Ray, + Vector3, + CollisionUtil +} from "@galacean/engine-math"; + +const plane = new Plane(new Vector3(0, 1, 0), -5); +const viewMatrix = new Matrix(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -20, 1); +const projectionMatrix = new Matrix(0.03954802080988884, 0, 0, 0, 0, 0.10000000149011612, 0, 0, 0, 0, -0.0200200192630291, 0, -0, -0, -1.0020020008087158, 1); +const vpMatrix = new Matrix(); +Matrix.multiply(projectionMatrix, viewMatrix, vpMatrix); +const frustum = new BoundingFrustum(vpMatrix); + +// 点和面之间的距离 +const point = new Vector3(0, 10, 0); +let distance = CollisionUtil.distancePlaneAndPoint(plane, point); + +// 判断点和面的空间关系 +const point1 = new Vector3(0, 10, 0); +const point2 = new Vector3(2, 5, -9); +const point3 = new Vector3(0, 3, 0); +const intersection1 = CollisionUtil.intersectsPlaneAndPoint(plane, point1); +const intersection2 = CollisionUtil.intersectsPlaneAndPoint(plane, point2); +const intersection3 = CollisionUtil.intersectsPlaneAndPoint(plane, point3); + +// 判断面和包围盒的空间关系 +const box1 = new BoundingBox(new Vector3(-1, 6, -2), new Vector3(1, 10, 3)); +const box2 = new BoundingBox(new Vector3(-1, 5, -2), new Vector3(1, 10, 3)); +const box3 = new BoundingBox(new Vector3(-1, 4, -2), new Vector3(1, 5, 3)); +const box4 = new BoundingBox(new Vector3(-1, -5, -2), new Vector3(1, 4.9, 3)); +const intersection11 = CollisionUtil.intersectsPlaneAndBox(plane, box1); +const intersection22 = CollisionUtil.intersectsPlaneAndBox(plane, box2); +const intersection33 = CollisionUtil.intersectsPlaneAndBox(plane, box3); +const intersection44 = CollisionUtil.intersectsPlaneAndBox(plane, box4); + +// 判断射线和平面的空间关系 +const ray1 = new Ray(new Vector3(0, 0, 0), new Vector3(0, 1, 0)); +const ray2 = new Ray(new Vector3(0, 0, 0), new Vector3(0, -1, 0)); +const distance1 = CollisionUtil.intersectsRayAndPlane(ray1, plane); +const distance2 = CollisionUtil.intersectsRayAndPlane(ray2, plane); + +// 判断视锥体和包围盒的空间关系 +const contain1 = CollisionUtil.frustumContainsBox(frustum, box1); +const contain2 = CollisionUtil.frustumContainsBox(frustum, box2); +const contain3 = CollisionUtil.frustumContainsBox(frustum, box3); +``` diff --git a/docs/core/scene.md b/docs/core/scene.md new file mode 100644 index 000000000..ef5d538c5 --- /dev/null +++ b/docs/core/scene.md @@ -0,0 +1,159 @@ +--- +order: 2 +title: 场景 +type: 核心概念 +label: Core +--- + +Scene 作为场景单元,可以方便的进行实体树管理,尤其是大型游戏场景。如: **scene1** 和 **scene2** 作为两个不同的场景,可以各自独立管理其拥有的 **Entity** 树,因此场景下的光照组件、渲染组件和物理组件之间也互相隔离,互不影响。我们可以同时渲染一个或多个 Scene,也可以在特定时机下根据项目逻辑动态切换当前 Scene。 + +从结构上每个 Engine 下可以包含一个和多个激活的场景(目前编辑器还不支持多个)。每个 Scene 可以有多个根实体。 + +## 编辑器使用 + +### 创建和切换 + +在 **[资产面板](/docs/assets-interface)** 右键(或资产面板右上角 + 号)创建场景,双击场景可以切换过去: + +![scene-switch](https://gw.alipayobjects.com/zos/OasisHub/eef870a7-2630-4f74-8c0e-478696a553b0/2024-03-19%25252018.04.02.gif) + +### 属性面板 + +image-20240319180602935 + +### 环境光 + +详情请参照[环境光教程](/docs/graphics-light-ambient) 和 [烘焙教程](/docs/graphics-light-bake)。 + +### 背景 + +详情请参照[背景教程](/docs/graphics-background)。 + +### 阴影 + +详情请参照[阴影教程](/docs/graphics-light-shadow)。 + +### 雾化 + +可以给整个场景增加 **线性、指数、指数平方** 3 种雾化: + +![Fog](https://gw.alipayobjects.com/zos/OasisHub/224fbc16-e60c-47ca-845b-5f7c09563c83/2024-03-19%25252018.08.23.gif) + + +## 脚本使用 + +| 属性名称 | 解释 | +| :--------------------------------------- | :------- | +| [scenes](/apis/core/#SceneManager-scenes) | 场景列表 | + +| 方法名称 | 解释 | +| :------------------------------------------------- | :------- | +| [addScene](/apis/core/#SceneManager-addScene) | 添加场景 | +| [removeScene](/apis/core/#SceneManager-removeScene) | 移除场景 | +| [mergeScenes](/apis/core/#SceneManager-mergeScenes) | 合并场景 | +| [loadScene](/apis/core/#SceneManager-loadScene) | 加载场景 | + +### 加载场景 + +如果想要加载 **Scene** 资产作为应用中的一个场景,可以使用 `engine.resourceManager.load` 传入 url 即可。 + +```typescript +const sceneUrl = "..."; + +engine.resourceManager + .load({ type: AssetType.Scene, url: "..." }) + .then((scene) => { + engine.sceneManager.addScene(scene); + }); +``` + +### 获取场景对象 + +通过调用 `engine.sceneManager.scenes` 可以获取当前引擎运行时激活的全部场景,也可以通过 `entity.scene` 获取对应 `entity` 从属的 `scene`。 + +```typescript +// 获取当前所有激活的场景 +const scenes = engine.sceneManager.scenes; + +// 获取节点属于的场景 +const scene = entity.scene; +``` + +### 添加/移除 Scene + +`engine.sceneManager.scenes` 是只读的,若需要添加和移除 **Scene** ,需要调用 `engine.sceneManager.addScene()` 或 `engine.sceneManager.removeScene()` ,**引擎支持同时渲染多个场景**。 + +```typescript +// 假设已经有两个场景 +const scene1, scene2; + +// 添加 场景1 +engine.sceneManager.addScene(scene1); + +// 添加 场景2 +engine.sceneManager.addScene(scene1); + +// 移除 场景2 +engine.sceneManager.removeScene(scene2); +``` + +多场景渲染示例如下: + + + +### 合并场景 + +可以使用 `engine.sceneManager.mergeScenes` 将 2 个场景进行合并为 1 个场景。 + +```typescript +// 假设已经有两个未激活的场景 +const sourceScene, destScene; + +// 将 sourceScene 合并到 destScene +engine.sceneManager.mergeScenes(sourceScene, destScene); + +// 激活 destScene +engine.sceneManager.addScene(destScene); +``` + +### 销毁场景 + +调用 `scene.destroy()` 即可销毁场景,被销毁的场景也会自动从激活场景列表中移除。 + +### 实体树管理 + +| 方法名称 | 解释 | +| :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | +| [createRootEntity](/apis/core/#Scene-createRootEntity) | 新创建的 _scene_ 默认没有根实体,需要手动创建 | +| [addRootEntity](/apis/core/#Scene-addRootEntity) | 可以直接新建实体,或者添加已经存在的实体 | +| [removeRootEntity](/apis/core/#Scene-removeRootEntity) | 删除根实体 | +| [getRootEntity](/apis/core/#Scene-getRootEntity) | 查找根实体,可以拿到全部根实体,或者单独的某个实体对象。注意,全部实体是只读数组,不能改变长度和顺序 | + +```typescript +const engine = await WebGLEngine.create({ canvas: "demo" }); +const scene = engine.sceneManager.scenes[0]; + +// 创建根实体 +const rootEntity = scene.createRootEntity(); + +// 添加实体到场景 +scene.addRootEntity(rootEntity); + +// 删除根实体 +scene.removeRootEntity(rootEntity); + +// 查找根实体 +const allEntities: Readonly = scene.rootEntities; + +const entity2 = scene.getRootEntity(2); +``` + +### 其他 + +需要注意的是,当我们熟悉了 [Engine](/apis/core/#Engine) 和 [Scene](/apis/core/#Scene) 之后,如果想要将渲染画面输出到屏幕上或者进行离屏渲染,我们还得确保当前 _scene_ 的实体树上挂载了 [Camera](/apis/core/#Camera),挂载相机的方法如下: + +```typescript +const cameraEntity = rootEntity.createChild("camera"); + +cameraEntity.addComponent(Camera); +``` diff --git a/docs/core/space.md b/docs/core/space.md new file mode 100644 index 000000000..70d0cb543 --- /dev/null +++ b/docs/core/space.md @@ -0,0 +1,64 @@ +--- +order: 6 +title: 坐标系统 +type: 核心 +label: Core +--- + +坐标系统在渲染引擎中扮演的角色非常重要,它保证了渲染结果与交互的准确,阅读本文档,你可以了解 Galacean 中涉及的绝大多数坐标系统,需要注意的是,不同渲染引擎中各种空间的定义是有差异的,本文仅讨论 Galacean 中的空间标准。 + +本文会按照`空间的定义`,`坐标系类型`等方面来横向比较各个坐标空间,其中`坐标系类型`具体指`左手坐标系`与`右手坐标系`,如下图所示: + + + +定义为`左手坐标系`或`右手坐标系`会影响 `forward` 的朝向与旋转的方向(逆时针或顺时针),对于朝向的定义可以想象着将右手与 `+X` 重合,头顶方向与 `+Y` 重合,此时面部朝向的方向就是 `forward` ,可以简单对比 Galacean 与 Unity 的差异: + +- Unity 的局部坐标与世界坐标系都是`左手坐标系`,位姿变换时依据顺时针方向进行旋转,对应的 `forward` 方向是 `+Z` ,因此相机的朝向(取景方向)就是 `+Z` 方向 + +- Galacean 的局部坐标与世界坐标系都是`右手坐标系`,位姿变换时依据逆时针方向进行旋转,对应的 `forward` 方向是 `-Z` ,因此相机的朝向(取景方向)就是 `-Z` 方向 + +## 局部空间 + +局部空间是相对的,它以物体的自身位置为参考坐标系的,因此描述的时候通常表示为:“ A 节点局部空间中的某个点”,局部空间是`右手坐标系`, `Transform` 组件会按照以下公式自动计算各个点在世界空间中的位置。 + + + +## 世界空间 + +世界空间是绝对的,根节点放置在`世界空间`中,而其子节点会继承他的空间关系,与`局部空间`相同,`世界空间`也是`右手坐标系`,当两个节点不在同一个`局部空间`时,可以将它们转换至世界空间来比较相对的位置关系。 + +## 编辑器使用 + +merge + +确定 gizmo 在场景中姿态 + +| 图标 | 选项 | 内容 | +| :-------------------------------------------------------------------------------------------------------------------------------- | :--------- | :------------------------------------------------ | +| | `本地坐标` | 保持 Gizmo 相对于选中实体的旋转 | +| | `全局坐标` | 固定 Gizmo 与世界空间方向。即与场景中网格方向一致 | + + +## 观察空间 + +`观察空间`就是相机的局部空间,以透视相机为例: + + + +## 屏幕空间 + +屏幕空间的定义与前端规范保持一致,是以画布的左上角为坐标原点的二维空间坐标系,空间内的取值范围与画布的尺寸保持一致,在交互,屏幕空间转换时经常使用。 + + + +## 视口空间 + +视口空间的定义与前端规范保持一致,通过设置相机的 viewport 可以控制渲染的目标区域, + + + +## 2D 精灵 + +渲染精灵或遮罩等 2D 元素时,默认在局部坐标系中的 XoY 平面上放置这个面片: + + diff --git a/docs/core/time.md b/docs/core/time.md new file mode 100644 index 000000000..8898d93d6 --- /dev/null +++ b/docs/core/time.md @@ -0,0 +1,20 @@ +--- +order: 9 +title: 时间 +type: 核心 +label: Core +--- + +`Time` 包含了引擎时间相关的信息: + +## 属性 + +| 名称 | 释义 | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [timeScale](/apis/core/#Time-timeScale) | 时间的缩放 | +| [maximumDeltaTime](/apis/core/#Time-maximumDeltaTime) | 最大间隔,帧率过低或卡顿时 | +| [frameCount](/apis/core/#Time-frameCount) | 引擎启动后累计的帧数 | +| [deltaTime](/apis/core/#Time-deltaTime) | 上一帧距离当帧的增量时间,以秒为单位,不会超过 [maximumDeltaTime](/apis/core/#Time-maximumDeltaTime) \* [timeScale](/apis/core/#Time-timeScale) | +| [actualDeltaTime](/apis/core/#Time-actualDeltaTime) | 上一帧距离当帧的实际增量时间,以秒为单位,并且忽略了 [timeScale](/apis/core/#Time-timeScale) 和 [maximumDeltaTime](/apis/core/#Time-maximumDeltaTime) 影响 | +| [elapsedTime](/apis/core/#Time-elapsedTime) | 引擎启动后累计经过的时间,以秒为单位 | +| [actualElapsedTime](/apis/core/#Time-actualElapsedTime) | 引擎启动后累计经过的时间,以秒为单位 | diff --git a/docs/core/transform.md b/docs/core/transform.md new file mode 100644 index 000000000..5890a85f9 --- /dev/null +++ b/docs/core/transform.md @@ -0,0 +1,131 @@ +--- +order: 7 +title: 变换 +type: 核心 +label: Core +--- + +## 基础概念 + +`Transform` 是 `Entity` 自带的基础组件,开发者可以通过它管理 `Entity` 在**局部空间**与**世界空间**中的位置、旋转和缩放。 + +> 结合 Galacean 的 **[坐标系统](/docs/core-space)** 会有更深入地了解。 + + + +## 编辑器使用 + +merge + +更改选中实体的可视化变换组件,直接使用鼠标操纵辅助图标轴。 + +

移动

+ +| 图标 | 操作 | 快捷键 | +| :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :----- | +| | `切换到 Gizmo 移动模式` | W | + +点击辅助轴,可在单个方向内拖动选中实体。点击辅助平面,可在单个平面内拖动选中实体。 + +

旋转

+ +| 图标 | 操作 | 快捷键 | +| :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :----- | +| | `切换到 Gizmo 选择模式` | E | + +点击并拖动以更改选中实体的旋转。 +红色代表绕 X 轴进行旋转,绿色代表绕 y 轴进行旋转,蓝色代表绕 z 轴进行旋转。 + +

缩放

+ +| 图标 | 操作 | 快捷键 | +| :-------------------------------------------------------------------------------------------------------------------------------- | :---------------------- | :----- | +| | `切换到 Gizmo 缩放模式` | R | + +点击中心立方体,在所有轴上均匀的缩放选中实体。点击辅助轴,在单个方向缩放选中实体。 + +通过 **[检查器面板](/docs/interface-inspector)** 可以为节点设置更精确的位置、旋转和缩放信息。 + +image.png + +## 脚本使用 + +```typescript +// 创建节点 +const scene = engine.sceneManager.activeScene; +const root = scene.createRootEntity("root"); +const cubeEntity = root.createChild("cube"); + +// entity 在创建后会默认自带变换组件 +// 通过变换组件能够对实体进行几何变换 + +// 修改节点位移,旋转,缩放 +transform.position = new Vector3(); +// 也可以 transform.setPosition(0, 0, 0); + +transform.rotation = new Vector3(90, 0, 0); +// 也可以 transform.setRotation(90, 0, 0); + +// 也可以通过实体的属性获取到 transform 组件 +cubeEntity.transform.scale = new Vector3(2, 1, 1); +// 也可以 cubeEntity.transform.setScale(2, 1, 1); + +// 局部位移 cube 实体 +cubeEntity.transform.translate(new Vector3(10, 0, 0), true); + +// 局部旋转 cube 实体 +cubeEntity.transform.rotate(new Vector3(45, 0, 0), true); +``` + +## 组件属性 + +| 属性名称 | 属性释义 | +| :---------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------- | +| [position](/apis/core/#Transform-position) | 局部位移 | +| [rotation](/apis/core/#Transform-rotation) | 局部旋转 - 欧拉角 | +| [rotationQuaternion](/apis/core/#Transform-rotationquaternion) | 局部旋转 - 四元数 | +| [scale](/apis/core/#Transform-scale) | 局部缩放 | +| [worldPosition](/apis/core/#Transform-worldPosition) | 世界位移 | +| [worldRotation](/apis/core/#Transform-worldRotation) | 世界旋转 - 欧拉角 | +| [worldRotationQuaternion](/apis/core/#Transform-worldRotationQuaternion) | 世界旋转 - 四元数 | +| [lossyWorldScale](/apis/core/#Transform-lossyWorldScale) | 世界有损缩放 - 当父节点有缩放,子节点有旋转时,缩放会倾斜,无法使用 Vector3 正确表示,必须使用 Matrix3x3 矩阵才能正确表示 | +| [localMatrix](/apis/core/#Transform-localMatrix) | 局部矩阵 | +| [worldMatrix](/apis/core/#Transform-worldMatrix) | 世界矩阵 | +| [worldForward](/apis/core/#Transform-worldMatrix) | forward 向量(世界空间中的单位矩阵) | +| [worldRight](/apis/core/#Transform-worldMatrix) | right 向量(世界空间中的单位矩阵) | +| [worldUp](/apis/core/#Transform-worldMatrix) | up 向量(世界空间中的单位矩阵) | + +## 组件方法 + +| 方法名称 | 方法释义 | +| ----------------------------------------------------------------------- | -------------------------------------- | +| [getWorldUp](/apis/core/#Transform-getWorldUp) | 获取世界矩阵上向量 | +| [getWorldRight](/apis/core/#Transform-getWorldRight) | 获取世界矩阵右向量 | +| [getWorldForward](/apis/core/#Transform-getWorldForward) | 获取世界矩阵前向量 | +| [lookAt](/apis/core/#Transform-lookAt) | 旋转并且保证世界前向量指向目标世界位置 | +| [registerWorldChangeFlag](/apis/core/#Transform-registerWorldChangeFlag) | 注册世界变换改变标记 | +| [rotate](/apis/core/#Transform-rotate) | 根据指定欧拉角旋转 | +| [rotateByAxis](/apis/core/#Transform-rotateByAxis) | 根据指定角度绕着指定轴旋转 | +| [translate](/apis/core/#Transform-translate) | 根据指定的方向和距离进行位移 | + +### `registerWorldChangeFlag` 的作用 + +`transform` 组件内部用脏标记作了大量计算优化。由于 `transform` 的 `worldMatrix` 属性也用脏标记进行了优化,若组件外部需要关注当前 `transform` 的 `worldMatrix` 是否发生了变化,需要获取到其脏标记的状态。 `transform` 组件提供了 `registerWorldChangeFlag` 方法:这个方法会返回一个更新标记,当前 `transform` 的 `worldMatrix` 被修改时会触发标记的更改。具体用法可以参考相机组件: + +```typescript +class Camera { + onAwake() { + this._transform = this.entity.transform; + // 注册更新标记 + this._isViewMatrixDirty = this._transform.registerWorldChangeFlag(); + } + get viewMatrix() { + // 当标记更新时,根据 worldMatrix 得到viewMatrix~ + if (this._isViewMatrixDirty.flag) { + this._isViewMatrixDirty.flag = false; + Matrix.invert(this._transform.worldMatrix, this._viewMatrix); + } + return this._viewMatrix; + } +} +``` diff --git a/docs/device/restore.md b/docs/device/restore.md new file mode 100644 index 000000000..974056148 --- /dev/null +++ b/docs/device/restore.md @@ -0,0 +1,85 @@ +--- +order: 0 +title: 设备恢复 +type: 资源 +label: Device +--- + +由于 GPU 是一种共享资源,在某些情况 GPU 可能会回收控制权,导致你的程序 GPU 设备丢失,例如以下几种情况可能会发生设备丢失: + +- 某个页面卡住时间过长 +- 多个页面占用过多的 GPU 资源,所有页面丢失上下文,并只恢复前台页面 +- PC 设备切换显卡或者更新显卡驱动 + +设备丢失后引擎在合适的时机会自动恢复程序所有内容,用户通常无需关心,在必要时用户可以通过以下机制编码处理设备丢失和恢复逻辑。 + +### 丢失和恢复处理 + +当 GPU 设备丢失时,`Engine` 会派发 `devicelost` 事件,用户可以做一些用户提示或保存配置之类的逻辑: + +```typescript +engine.on("devicelost", () => { + // Do some device lost logic here + // For example,prompt user or save configuration etc +}); +``` + +引擎支持自动 GPU 设备自动恢复,当程序可以恢复时,`Engine` 会派发 `devicerestored` 事件,引擎内部会自动重建纹理、缓冲、着色器等低级 GPU 资源,并且会尝试自动恢复其数据内容。通常通过引擎提供的 Loader 和 PrimitiveMesh 等方式创建的资源可以完全自动恢复其内容,开发者无需做任何处理。只有当开发者自行修改资源内容时需要手动处理,比如手动修改了纹理的像素内容。 + +```typescript +engine.on("devicerestored", () => { + // Do some device restore logic here + // For example,restore user-modified texture content + texture.setPixelBuffer(pixels, 0, offsetX, offsetY, width, height); +}); +``` + +### 自定义恢复器 + +还有一种情况是资源完全由开发者自行创建,比如自定义 [Loader](/docs/assets-type) 或程序化生成资源。除了可以通过上面的方式在 `devicerestored` 事件中处理,也可以通过自定义内容恢复器实现,以下案例是为用户自行创建的纹理注册一个自定义恢复器并注册到 `ResourceManager` 中。当设备需要恢复时,`restoreContent` 方法会自动触发并恢复其内容。 + +```typescript +// Step 1: Define content restorer +export class CustomTextureContentRestorer extends ContentRestorer { + /** + * Constructor of CustomTextureContentRestorer. + * @param resource - Texture2D resource + * @param url - Texture2D content source url + */ + constructor(resource: Texture2D, public url: string) { + super(resource); + } + + /** + * @override + */ + restoreContent(): AssetPromise | void { + return request(this.url).then((image) => { + const resource = this.resource; + resource.setImageSource(image); + resource.generateMipmaps(); + return resource; + }); + } +} + +// Step 2: Register Content Restorer +resourceManager.addContentRestorer( + new CustomTextureContentRestorer(texture, url) +); +``` + +> 注意:恢复器实现不建议依赖和占用大量 CPU 内存 + +### 模拟设备丢失和恢复 + +实际项目中触发设备丢失和恢复的概率较小,为了方便开发者测试设备丢失和恢复后的程序表现和逻辑处理,`Engine` 提供了内置方法模拟设备丢失和恢复。 + +| 方法 | 解释 | +| ---------------------------------------------------------- | ------------ | +| [forceLoseDevice](/apis/core/#Engine-forceLoseDevice) | 强制丢失设备 | +| [forceRestoreDevice](/apis/core/#Engine-forceRestoreDevice) | 强制恢复设备 | + +### 参考 + +- 《WebGL 处理上下文丢失》:https://www.khronos.org/webgl/wiki/HandlingContextLost diff --git a/docs/graphics/2D/2d.md b/docs/graphics/2D/2d.md new file mode 100644 index 000000000..478559d36 --- /dev/null +++ b/docs/graphics/2D/2d.md @@ -0,0 +1,28 @@ +--- +order: 0 +title: 2D 总览 +type: 图形 +group: 2D +label: Graphics/2D +--- + +Galacean 是 3D/2D 的互动解决方案,您可以在**编辑器主页**的**菜单视图**侧依次**点击模版**->**像素小鸟**快速体验 2D 互动开发。 + +image.png + +也可以在**编辑器主页**的**项目视图**侧依次**点击新建项目**->**2D Project**创建一个空白的 2D 项目。 + +image.png + +在编辑器中, 2D 项目和 3D 项目没有太大的差别,只是在视角上从 3D 切换成了 2D,并将默认相机设置为**正交**。在**层级界面**依次**选中节点并右键**-> **2D 对象**可以快速创建一个 2D 子节点。 +在 2D 项目中,您可以挂载**精灵渲染器**并设置**精灵资产**来渲染图像,可以通过**文字渲染器**来渲染 2D 文字,可以用 **SpriteMask** 实现 2D 元素的遮罩效果,还可以使用 Lottie 或 Spine( 2D 骨骼动画)表现 2D 特效,在最后性能优化时,您还可以将**精灵**打包成**精灵图集**从而提升请求和渲染的性能。 + +接下来让我们来深入了解以下内容: + +- [精灵](/docs/graphics-2d-sprite) +- [精灵渲染器](/docs/graphics-2d-spriteRenderer) +- [精灵遮罩](/docs/graphics-2d-spriteMask) +- [文字渲染器](/docs/graphics-2d-text) +- [精灵图集](/docs/graphics-2d-spriteAtlas) +- [Lottie](/docs/graphics-2d-lottie) +- [Spine](/docs/graphics-2d-spine) diff --git a/docs/graphics/2D/lottie.md b/docs/graphics/2D/lottie.md new file mode 100644 index 000000000..9ae776f07 --- /dev/null +++ b/docs/graphics/2D/lottie.md @@ -0,0 +1,157 @@ +--- +order: 6 +title: Lottie +type: 图形 +group: 2D +label: Graphics/2D +--- + +[lottie](https://airbnb.io/lottie/) 是 Airbnb 于 2017 年前后发布的一款跨平台的动画解决方案,可应用于 iOS,Android,React Native 和 web,通过 Bodymovin 插件解析 [AE](https://www.adobe.com/products/aftereffects.html) 动画,并导出可在移动端和 web 端渲染动画的 json 文件。设计师通过 AE 来制作动画,再用 Bodymovin 导出相应的 json 文件给到前端,前端可以使用这个 json 文件直接生成 100% 还原的动画。 + +用户可以在 Galacean 中轻松完成 Lottie 资产的处理和组件添加。 + +### 资源上传 + +建议设计师在 AE 中导出 lottie 文件的时候,图片采用 base64 格式写入 lottie 的 json 文件中。 + +开发者拿到 `.json` 文件后,首先需要把 `.json` 文件上传到 Galacean Editor。通过资产面板的上传按钮选择 “lottie” 资产,选择本地一个 [lottie json](https://github.com/galacean/galacean.github.io/files/14106485/_Lottie.3.json) 文件,然后上传: + + + +### 添加组件 + +选择一个实体,添加 Lottie 组件,选择 resource 为上一步上传的资产,即可显示并且播放 Lottie 特效: + +![lottie](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*ehFMT7vBaCAAAAAAAAAAAAAADjCHAQ/original) + +开发者可以通过调整属性面板中的各个参数来对 Lottie 进行相关设置: + +![lottie](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*OswOQI837OkAAAAAAAAAAAAADjCHAQ/original) + + +| 属性 | 功能说明 | +| :--- | :--- | +| `resource` | 选择 Lottie 资产 | +| `autoPlay` | 是否自动播放,默认自动 | +| `isLooping` | 是否循环播放,默认循环 | +| `speed` | 播放速度,`1` 为原速度播放,数值越大播放约快 | +| `priority` | 渲染优先级,值越小,渲染优先级越高,越优先被渲染 | + +有时候开发者可能需要在运行时动态对 Lottie 进行设置,在脚本组件中添加代码如下: +```typescript +// 先找到 Lottie 所在的实体 lottieEntity,然后获取 Lottie 组件 +const lottie = lottieEntity.getComponent(LottieAnimation); +// 设置 lottie 属性 +lottie.speed = 2; +``` +另外,Lottie 组件还提供了 2 个 API 来控制动画的播放和暂停,如下: + +| 方法 | 描述 | +| :--- | :--- | +| `play` | 播放动画,传入动画片段名参数会播放特定的动画片段 | +| `pause` | 暂停动画 | + +### 监听播放结束 + +很多时候我们有监听 Lottie 动画播放结束的需求,比如在动画结束的时候运行一些业务逻辑。`LottieAnimation` 的 `play` 方法会返回一个 `Promise`,所以可以很方便地监听动画结束的时机: + +```typescript + const lottie = lottieEntity.getComponent(LottieAnimation); + await lottie.play(); + // do something next.. +``` + +### 切片功能 + +编辑器提供了动画切片的功能,可以把设计师提供的整个片段切成多段,每个片段需要定义片段名、开始帧、结束帧三个字段。 + + + + + +该操作会在 Lottie 协议中添加 `lolitaAnimations` 字段实现动画的切片: + +```json +"lolitaAnimations": [ + { + "name": "clip1", + "start": 0, + "end": 30 + }, + { + "name": "clip2", + "start": 50, + "end": 100 + }, +] +``` + + +### 安装依赖包 + +@galacean/engine-lottie 是 Galacean Engine 的二方包,项目中用到了 Lottie 的时候,需要确保项目中安装了该包: + +```bash +npm i @galacean/engine-lottie --save +``` + +### pro code 开发模式 + +在进行 `Pro Code` 开发的时候,需要一个 `json` 文件和一个 `atlas` 文件来实现 `lottie` 动画,通常美术同学通过 `AE` 导出的给到开发的只有 `json` 文件,此时需要使用 [tools-atlas-lottie](https://www.npmjs.com/package/@galacean/tools-atlas-lottie) `CLI` 工具生成 `atlas` 文件。 + +```typescript +import { LottieAnimation } from "@galacean/engine-lottie"; + +// Load lottie json、atlas file with engine's `resourceManager` +engine.resourceManager.load({ + urls: [ + "https://gw.alipayobjects.com/os/bmw-prod/b46be138-e48b-4957-8071-7229661aba53.json", + "https://gw.alipayobjects.com/os/bmw-prod/6447fc36-db32-4834-9579-24fe33534f55.atlas" + ], + type: 'lottie' +}).then((lottieEntity) => { + // Add lottie entity created to scene + root.addChild(lottieEntity); + + // Get `LottieAnimation` component and play the animation + const lottie = lottieEntity.getComponent(LottieAnimation); + lottie.isLooping = true; + lottie.speed = 1; + lottie.play(); +}); +``` + + + + +### 3D 变换 + +业务场景中经常会出现 3D 变换的需求,比如一些弹窗的入场动画。以旋转为例,由于传统的 lottie-web 方案只能沿着 **Z轴** 旋转(也就是说垂直于屏幕法线方向旋转),即使我们在 AE 中实现了沿着 **X轴** 或 **Y轴** 的旋转效果,使用 lottie-web 播放时也会被忽略。 + +3D rotation + +得益于 Galacean Engine 2D/3D 引擎统一架构的优势,轻松地实现 3D 变换功能。 + + + +## 性能方面的建议 + +- 动画简单化。创建动画时需时刻记着保持 json 文件的精简,比如尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得 json 文件变得非常大且耗性能。 +- 如果有循环的帧,请不要在动画文件里面循环,请数出帧数,让开发自行控制这段动画的循环,能节省相同图层和动画的体积。 +- 建立形状图层。将 AI、EPS、SVG 和 PDF 等资源转换成形状图层否则无法在 lottie 中正常使用,转换好后注意删除该资源以防被导出到 json 文件。 +- 设置尺寸。在 AE 中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。 +- 在尽量满足效果的情况下,请对路径做适当的裁剪,这个对性能影响很大。 +- lottie 进行动画的时候会按照 AE 的设计进行分层,所以要尽量减少层数。 +- 若确实没有必要使用路径动画,请将矢量图形替换为 png 图片,并用 transform 属性完成动画。 +- 可以根据实际状况,斟酌降低动画帧率或者减少关键帧数量,这会减少每秒绘制的次数。 +- 精简动画时长,可以循环的动作,就不要在时间轴做两遍,每一次读取关键帧都会消耗性能。编排上尽量避免 a 动作结束,b 动作开始,可以让动作有所重叠,减少动画长度。 +- 同类项合并,有些元素是相似的,或者相同的用在了不同的地方,那就把这个元素预合成重复使用这一个元件,可以通过对该预合成的动画属性的调整达到想要的动画效果。 +- 尽量减少图层个数。每个图层都会导出成相应的 json 数据,图层减少能从很大程度上减小 json 大小。 +- 尽可能所有的图层都是在 AE 里面画出来的,而不是从其他软件引入的。如果是其他软件引入的,很可能导致描述这个图形的 json 部分变得很大。 +- 制作的时候,请将动画元素**铺满**整个画布,这样可以避免浪费,也方便前端进行尺寸的调整。 +- 如果矢量图形是在 AI 中导出的,请将多余的“组”等没有任何实际效用的元素删掉。 +- 删除那些关闭了和无用的属性。 +- 只导出 1x 图。 +- 为了防止 lottie 导出的兼容性问题,请尽量使用英文版本 AE ,图层需简洁,命名清晰 +- 避免大面积矢量部分,以及大面积粒子效果 + diff --git a/docs/graphics/2D/spine.md b/docs/graphics/2D/spine.md new file mode 100644 index 000000000..7aa6238fe --- /dev/null +++ b/docs/graphics/2D/spine.md @@ -0,0 +1,161 @@ +--- +order: 7 +title: Spine +type: 图形 +group: 2D +label: Graphics/2D +--- + +Spine 动画是一款针对游戏开发的 `2D 骨骼动画`,它通过将图片绑定到骨骼上,然后再控制骨骼实现动画,它可以满足程序对动画的`控制`与`自由度`,同时也为美术与设计提供了更`高效`和`简洁`的工作流。 + +| | 表现效果 | 性能 | 文件体积 | 灵活程度 | 上手难度 | 是否免费 | +| ------ | -------- | ---- | -------- | -------- | -------- | -------- | +| Spine | 最优 | 次之 | 最优 | 最优 | 最复杂 | 工具收费 | +| Lottie | 次之 | 最差 | 次之 | 次之 | 次之 | 免费 | +| 帧动画 | 最差 | 最优 | 最差 | 最差 | 最简单 | 免费 | + +Spine 动画支持换皮换肤,动画混合以及使用代码控制骨骼。 + +## 在编辑器中使用 + +在编辑器中使用 spine 包含下面几个步骤: + +```mermaid +flowchart LR + 资源导出 --> 资源上传 --> 添加组件 --> 编写脚本 +``` + +### 资源导出 + +下载 [Spine 编辑器](https://zh.esotericsoftware.com/),并选择 3.8 版本制作动画(目前仅支持 3.8 版本)。通过 spine 编辑器的导出功能能够导出所需的资源文件。导出后,在目标文件夹内会看到 .json(或者.bin), atlas, png 三种格式的资源文件。[点击下载示例文件](https://mdn.alipayobjects.com/portal_h1wdez/afts/file/A*uhFUSbeI5z0AAAAAAAAAAAAAAQAAAQ) + +> Galacean Spine 运行时目前只支持加载单张纹理,所以当贴图尺寸过大时,需要对图片资源进行缩放处理,把贴图的张数控制在一张。 +> 文件导出的详细配置见 spine 官方文档:[http://zh.esotericsoftware.com/spine-export](http://zh.esotericsoftware.com/spine-export/) + +### 资源上传 + +资源导出后,开发者需要同时把三个文件上传到 Galacean Editor。通过 **[资产面板](/docs/assets-interface)** 的上传按钮选择 “spine” 资产,选择本地的这三个文件,上传成功后能够在资产面板看到上传的 spine 资产: + + +也可以直接把三个文件拖动到资产区域完成上传: + + +完成上传后,能够在 Asset 面板看到上传的 spine 素材: + + +### 添加组件 + +完成资源上传后,在编辑器左侧节点树中添加一个 spine 渲染节点(一个自带 SpineRenerer 组件的节点),选择 resource 为上一步上传的资产,选择动画名称即可播放 spine 动画(如果不选择,默认第一个)。 + +![spine](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*tqm4R51gYxEAAAAAAAAAAAAADjCHAQ/original) + +Spine 渲染组件的属性如下: + +| 属性 | 功能说明 | +| :---------- | :----------------------------------------------- | +| `Resource` | 选择 Spine 资产 | +| `AutoPlay` | 是否自动播放 | +| `loop` | 是否循环播放 | +| `Animation` | 动画名称 | +| `SkinName` | 皮肤名称 | +| `Scale` | 动画缩放 | +| `priority` | 渲染优先级,值越小,渲染优先级越高,越优先被渲染 | + +### 编写脚本 + +如果需要对 spine 动画进行额外的逻辑编写,就需要借助编辑器的脚本功能了。创建一个脚本资源,并给上一小节创建的节点添加一个脚本组件,选择创建好的脚本。 + + +双击素材面板中的脚本,或者点击脚本组件的编辑按钮,能够进入脚本编辑器,在脚本编辑器中可以从当前 entity 上获取 spine 渲染组件,通过组件 API 进行更多操作。比如,主动播放某一段动画: + + +更详细的 API 请参考下面的章节。 + +## 在代码中使用 + +### 安装 + +首先需要手动添加 [@galacean/engine-spine](https://github.com/galacean/engine-spine) 二方包。 + +```bash +npm i @galacean/engine-spine --save +``` + +### 资源导出 + +安装了二方包后,与在编辑器中使用一样,需要下载 [Spine 编辑器](https://zh.esotericsoftware.com/),并选择 3.8 版本制作动画(目前仅支持 3.8 版本)。通过 spine 编辑器的导出功能能够导出所需的资源文件。导出后,在目标文件夹内会看到 .json(或者.bin), atlas, png 三种格式的资源文件。[点击下载示例文件](https://mdn.alipayobjects.com/portal_h1wdez/afts/file/A*uhFUSbeI5z0AAAAAAAAAAAAAAQAAAQ) + +### 资源加载 + +在代码中,引入了 _@galacean/engine-spine_ 后,会自动在 [engine]($%7Bapi%7Dcore/Engine) 的 [resourceManager]($%7Bapi%7Dcore/Engine#resourceManager) 上注册 spine 资源的资源加载器。通过 resourceManager 的 [load]($%7Bapi%7Dcore/ResourceManager/#load) 方法能够加载 spine 动画资源。 + +- 当传递参数为 url 时,默认 spine 动画的资源拥有同样的 baseUrl,仅需传递 json(或者 bin) 文件的 cdn 即可。 +- 当传递参数为 urls 数组时,需要传递 json(或者 bin),atlas, image(png,jpg)三个资源的 cdn 地址。 +- 资源的 type 必须指定为 spine。 + +加载完毕后,会返回一个 SpineResouce。我们需要创建一个节点,添加 Spine 渲染器,然后指定渲染器的资源为返回的 SpineResouce。请参考下方示例中的代码: + + + +### 动画播放 + +Spine 渲染器(SpineRenderer) 提供了多种动画播放的方法。 + +1. 通过 animationName,autoPlay,loop 属性播放。当设置了 animationName 为待播放的动画名称,且 autoplay 为 true 时,对应名称动画会自动播放。通过 loop 能够控制是否循环播放。 + +```javascript +const spineRenderer = spineEntity.getComponent(SpineRenderer); +spineRenderer.animationName = "idle"; +spineRenderer.autoPlay = true; +spineRenderer.loop = true; +``` + +2. 调用 play 方法播放。play 方法支持传入动画名称和是否循环两个参数。 + +```javascript +const spineRenderer = spineEntity.getComponent(SpineRenderer); +spineRenderer.play("idle", true); +``` + +3. 从 spineAnimation 属性上,能够获取 spine 的 [AnimationState](http://zh.esotericsoftware.com/spine-api-reference#AnimationState) 以及 [Skeleton](http://zh.esotericsoftware.com/spine-api-reference#Skeleton) 接口,能够借助 spine-core 原生 API 来播放动画。 + +```javascript +const spineRenderer = spineEntity.getComponent(SpineRenderer); +spineRenderer.spineAnimation.state.setAnimation(0, "idle", true); +``` + +#### 动画控制 + +借助 Spine 渲染器(SpineRenderer) 的 spineAnimation 暴露的 AnimationState 对象,能够实现动画的控制,比如循环播放动画,暂停动画播放等。这里可以参考下面的示例。 +详细的 API 可以参考 AnimationState 的官方文档:[http://zh.esotericsoftware.com/spine-api-reference#AnimationState](http://zh.esotericsoftware.com/spine-api-reference#AnimationState) + +### 动画事件机制 + +spine 还提供了一些事件方便用户进行开发。动画事件的机制如下图所示: +![](https://gw.alipayobjects.com/mdn/mybank_yul/afts/img/A*fC1NT5tTET8AAAAAAAAAAAAAARQnAQ#crop=0&crop=0&crop=1&crop=1&id=JUZeZ&originHeight=280&originWidth=640&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=) +详细文档: +[http://esotericsoftware.com/spine-unity-events](http://esotericsoftware.com/spine-unity-events) +通过 AnimationState 的 addListener 方法,能够在不同的事件触发时,添加回调方法。 + +### 换肤 + +运行时提供了多种方法来进行换肤。最简单的方式是通过 Spine 渲染器的 skinName 属性进行换肤。 + +```javascript +const spineRenderer = spineEntity.getComponent(SpineRenderer); +spineRenderer.skinName = "skin1"; +``` + +也可以通过 spine-core 原生 API 来进行换肤。请参考下方示例: + + +#### 附件替换 + +借助原生 API 能够实现 spine 的附件替换,从而实现部分换装的效果。请参考下方示例: + + +#### 插槽拆分 + +spine 组件会合并 spine 动画的所有顶点生成一个 `Mesh`。使用 `addSeparateSlot` 方法能够将指定名称的插槽拆分成单独的 `SubMesh`,然后使用 `hackSeparateSlotTexture` 方法,能够替换拆分插槽的材质。通过这种方式,也能实现局部换装的效果。请参考下方示例: + + diff --git a/docs/graphics/2D/sprite.md b/docs/graphics/2D/sprite.md new file mode 100644 index 000000000..ea7bef9d6 --- /dev/null +++ b/docs/graphics/2D/sprite.md @@ -0,0 +1,65 @@ +--- +order: 1 +title: 精灵 +type: 图形 +group: 2D +label: Graphics/2D +--- + +[Sprite](/apis/core/#Sprite) 是 2D 项目中最重要的资产,他从 [Texture2D](/docs/graphics-texture-2d) 中获取图形源数据,通过设置 [region](/apis/core/#Sprite-region),[pivot](/apis/core/#Sprite-pivot) 等属性定制期望的渲染结果,若将其赋予[SpriteRenderer](/apis/core/#SpriteRenderer),挂载了精灵渲染器的节点就可以在三维空间中展示 2D 图片,若将其赋予[SpriteMask](/docs/graphics-2d-spriteMask),挂载了精灵遮罩的节点就可以对相应的 2D 元素实现遮罩效果,接下来就让我们深入了解精灵的属性和用法。 + +## 属性 + +| 属性名 | 属性类型 | 描述 | +| :----------------------------------- | :-------------------------------- | :------------------------------------------------------------------------------------------------------ | +| [texture](/apis/core/#Sprite-texture) | [Texture2D](/apis/core/#Texture2D) | 使用纹理的引用 | +| [width](/apis/core/#Sprite-width) | Number | 精灵的宽,若开发者未自定义精灵宽度,则默认为纹理像素宽 / 100 | +| [height](/apis/core/#Sprite-height) | Number | 精灵的高,若开发者未自定义精灵高度,则默认为纹理像素高 / 100 | +| [region](/apis/core/#Sprite-region) | [Rect](/apis/math/#Rect) | 精灵在原始纹理上的位置,范围 0 ~ 1 | +| [pivot](/apis/core/#Sprite-pivot) | [Vector2](/apis/math/#Vector2) | 精灵中心点在原始纹理上的 region 中的位置,范围 0 ~ 1 | +| [border](/apis/core/#Sprite-border) | [Vector4](/apis/math/#Vector4) | 渲染器的绘制模式为九宫或平铺时,边界配置会影响最终的渲染效果,其中 x,y,z,w 分别对应距左下右上四条边距离 | + +region 决定精灵的显示内容,可以在纹理中选择一个矩形区域进行显示,超出部分会自动过滤掉,如下: + +avatar + +pivot 代表精灵中心在 region 中的位置,如下: + +avatar + +## 使用 + +### 创建 + +#### 上传精灵 + +在 **[资产面板](/docs/assets-interface)** 空白处依次 **右键** → **Upload** → **Sprite** → **选中对应图片** 即可上传精灵资产,上传成功后当前资产列表会同步添加一份名为 `图片名.png` 的纹理资产和一份 `图片名-spr.png` 的精灵资产 + +avatar + +#### 创建空白精灵 + +在 **[资产面板](/docs/assets-interface)** 空白处依次 **右键** → **Create** → **Sprite** 即可创建一份空白的精灵资产。 + +avatar + +#### 脚本创建 + +同样地,在脚本中我们可以用如下代码创建精灵: + +```typescript +// 创建一个空白精灵 +const sprite = new Sprite(engine); +// 创建一个带纹理的精灵 +const spriteWithTexture = new Sprite(engine, texture2D); +``` + +### 设置属性 + +这里特别说明下 pivot 在编辑器中的设置。对于 pivot 来说,纹理左下角为 `(0, 0)`,X 轴从左到右,Y 轴从下到上。编辑器中内置了一些常用的 pivot 快捷值,如下: + +avatar + +如果内置值无法满足需求,可以自定义自己的 pivot,如下: + +avatar diff --git a/docs/graphics/2D/spriteAtlas.md b/docs/graphics/2D/spriteAtlas.md new file mode 100644 index 000000000..ec2f6970e --- /dev/null +++ b/docs/graphics/2D/spriteAtlas.md @@ -0,0 +1,163 @@ +--- +order: 5 +title: 精灵图集 +type: 图形 +group: 2D +label: Graphics/2D +--- + +[SpriteAtlas](/apis/core/#SpriteAtlas) 是一种精灵集合资源,通过将多个精灵纹理打包成一张精灵图集从而在绘制时合并绘制指令,它拥有以下优势: + +- 更好的性能(合并绘制指令); +- 更少的显存(打包算法降低纹理尺寸); +- 更少的请求次数(通过减少碎片文件来减少加载的请求次数); + +下图精灵图集例子里每帧只调用了一次绘制指令: + + + +## 编辑器使用 + +### 创建精灵图集 + +在 **[资产面板](/docs/assets-interface)** 内右键,选择`功能列表`中的`创建`,并选中`精灵图集`,此时,一个空白的精灵图集资产就创建成功了。 + +buildBox + +选中`精灵图集`资产,可以在 **[检查器面板](/docs/interface-inspector)** 查看资产的详细信息。 + +buildBox + +### 添加精灵 + +在确定`精灵图集`与`精灵`之间的包含关系后,需要将`精灵`添加至对应的`精灵图集`,此步骤即可通过操作`精灵`资产实现,也可通过操作`精灵图集`资产实现,接下来就分别实践两种方式。 + +#### 方式一:操作精灵 + +左键选中需要添加的`精灵`资产,可以在 **[检查器面板](/docs/interface-inspector)** 找到精灵的`从属关系`,选择`打包进图集`就可以选取希望打包进的`精灵图集`资产了。 + +buildBox + +#### 方式二:操作精灵图集 + +左键选中目标`精灵图集`资产,可以在 **[检查器面板](/docs/interface-inspector)** 找到图集打包的精灵列表,选择`添加精灵`就可以选取希望打包的`精灵`资产了。(若选取文件夹,则会添加文件夹目录下的所有精灵) + +buildBox + +### 移除精灵 + +#### 方式一:操作精灵 + +左键选中需要从图集中移除的的`精灵`资产,可以在 **[检查器面板](/docs/interface-inspector)** 找到精灵的`从属关系`(注意需确认目标图集的路径是否匹配),点击移除按钮就可以从目标图集中移除该精灵。 + +buildBox + +#### 方式二:操作精灵图集 + +左键选中需要操作的`精灵图集`资产,可以在 **[检查器面板](/docs/interface-inspector)** 找到图集的精灵列表,找到要移除的精灵对象并点击移除按钮即可。 + +buildBox + +### 快速操作精灵 + +`精灵`资产被加入`精灵图集`后,可以在`精灵图集`的 **[检查器面板](/docs/interface-inspector)** 中快速操作精灵,他的属性会同步修改到`精灵`资产中 + +buildBox + +### 设置 + +#### 打包设置 + +image-20231208165843716 + +| 设置名称 | 释义 | +| ------------------ | ---------------------------------------- | +| 纹理最大宽度 | 打包得到纹理的最大限制宽度 | +| 纹理最大高度 | 打包得到纹理的最大限制高度 | +| 边缘填充 | 打包精灵的边缘填充宽度 | +| 允许旋转(未启用) | 是否通过旋转提高图集打包的空间利用率 | +| 空白裁减(未启用) | 是否通过空白裁减提高图集打包的空间利用率 | + +#### 导出设置 + +image-20231208165430415 + +| 属性 | 值 | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 循环模式 U([wrapModeU](/apis/core/#Texture-wrapModeU)) | 截取模式([Clamp](/apis/core/#TextureWrapMode-Clamp))、 重复模式([Repeat](/apis/core/#TextureWrapMode-Repeat))、镜像重复模式([Mirror](/apis/core/#TextureWrapMode-Mirror)) | +| 循环模式 V([wrapModeV](/apis/core/#Texture-wrapModeV)) | 截取模式([Clamp](/apis/core/#TextureWrapMode-Clamp))、重复模式([Repeat](/apis/core/#TextureWrapMode-Repeat))、 镜像重复模式([Mirror](/apis/core/#TextureWrapMode-Mirror)) | +| 过滤模式([filterMode](/apis/core/#Texture-filterMode)) | 点过滤([Point](/apis/core/#TextureFilterMode-Point))、双线性过滤([Bilinear](/apis/core/#TextureFilterMode-Bilinear))、 三线性过滤([Trilinear](/apis/core/#TextureFilterMode-Trilinear)) | +| 各向异性过滤等级([anisoLevel](/apis/core/#Texture-anisoLevel)) | 向向异性等级,1 ~ 16 | +| 纹理映射([Mipmap](/apis/core/#Texture-generateMipmaps)) | true , false | + +### 最佳实践 + +点击`精灵图集`资产,通过调整`打包设置`的`纹理最大宽度`与`纹理最大高度`,同时调用`打包对象`中的`打包并预览`,可以保证图集利用率在一个较高的水平。 + +![image-20210901171947471](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*lyhRSY63HJgAAAAAAAAAAAAADjCHAQ/original) + +预览图中左侧表示导出图片的大小信息,右侧表示图集利用率信息(代表所有散图面积的和占用最终大图的面积百分比),可依据此值调整打包设置以达到较佳的结果。 + +## 脚本使用 + +### 图集生成 + +Galacean 为精灵图集提供了命令行工具,开发者可以按照以下步骤生成图集: + +1. 安装包 + +```bash +npm i @galacean/tools-atlas -g +``` + +2. 执行打包命令 + +```bash +galacean-tool-atlas p inputPath -o outputName +``` + +其中 `inputPath` 表示需要打包的文件夹路径,而 `outputName` 则表示打包输出的精灵图集文件名,如果你得到下图所示结果,那么说明打包成功了。 + +image.png + +| 属性 | 解释 | +| -------------- | -------------------------------------------- | +| f/format | 打包输出的精灵图集格式 (默认: "galacean") | +| o/output | 打包输出的精灵图集文件名 (默认: "galacean") | +| a/algorithm | 打包精灵图集的算法 (默认: "maxrects") | +| ar/allowRotate | 打包精灵图集是否支持旋转 (默认: false) | +| p/padding | 图集中每个精灵和这个精灵边框的距离 (默认: 1) | +| mw/maxWidth | 最后打包出的精灵图集的最大宽度 (默认: 1024) | +| mh/maxHeight | 最后打包出的精灵图集的最大高度 (默认: 1024) | +| s/square | 强制打包成正方形 (默认: false) | +| pot | 宽高强制打包成 2 的幂 (默认: false) | + +更多请参照[图集打包工具](https://github.com/galacean/tools/blob/main/packages/atlas/README.md)。 + +### 使用 + +1. 上传图集图片和图集文件至 CDN 同一目录下,例如文件和图片的地址应分别为 `https://*cdnDir*/*atlasName*.atlas` 和 `https://*cdnDir*/*atlasName*.png`。 + +2. 加载与使用 + +```typescript +engine.resourceManager + .load({ + url: "https://*cdnDir*/*atlasName*.atlas", + type: AssetType.SpriteAtlas, + }) + .then((atlas) => { + // Get all sprites. + const allSprites = atlas.sprites; + // Get sprite by spriteName. + atlas.getSprite("spriteName"); + // If multiple sprites have the same name, we can get all like this. + const allSpritesWithSameName = atlas.getSprites("spriteName", []); + }); +``` + +## 注意事项 + +1. 请将绘制时序相连的精灵打包进同一图集,可显著提升性能(降低绘制指令的调用次数); +2. 清理精灵图集时,需要确保图集内的所有精灵都已不使用; +3. 打包精灵图集是需要统筹精灵数目与尺寸,避免一次打包生成多张精灵图集; diff --git a/docs/graphics/2D/spriteMask.md b/docs/graphics/2D/spriteMask.md new file mode 100644 index 000000000..60d7ae9ee --- /dev/null +++ b/docs/graphics/2D/spriteMask.md @@ -0,0 +1,78 @@ +--- +order: 3 +title: 精灵遮罩 +type: 图形 +group: 2D +label: Graphics/2D +--- + +精灵遮罩组件用于对 3D/2D 场景中的[精灵](/docs/graphics-2d-sprite)实现遮罩效果。 + + + +通过 [SpriteMask](/apis/core/#SpriteMask) 提供的参数来控制和 [精灵](/docs/graphics-2d-sprite) 发生作用。 + +| 参数 | 类型 | 说明 | +| :-------------- | :----- | :----------------------------------------------------------------------------------------------- | +| influenceLayers | number | 当前 mask 影响的遮罩层,默认值为 SpriteMaskLayer.Everything,表示对所有遮罩层都有影响 | +| alphaCutoff | number | 当前 mask 有效 alpha 值的下限(范围:0~1),即 sprite 的纹理中 alpha 值小于 alphaCutoff 的将被丢弃 | + +[SpriteMaskLayer](/apis/core/#SpriteMaskLayer) 里面声明了引擎提供的遮罩层,一共声明了 32 个遮罩层,分别是 Layer0~Layer31,遮罩层和渲染无关,只是为了帮助开发者设置 `SpriteMask` 和 `SpriteRenderer` 如何进行关联,一个 `SpriteMask` 对象要对一个 `SpriteRenderer` 对象产生遮罩作用的一个前提就是两者的遮罩层有交集。 + +`SpriteMask` 的 `influenceLayers` 表示该 mask 对处于哪些遮罩层内的 `SpriteRenderer` 会起到遮罩作用,`SpriteRenderer` 的 `maskLayer` 表示该精灵处于哪些遮罩层,如下: + +070C8B9F-14E2-4A9A-BFEC-4BC3F2BB564F + +上图中,spriteMask 对处于 `Layer1` 和 `Layer30` 的精灵有遮罩作用,spriteRenderer0 处于 `Layer2`,不存在交集,所以 spriteRenderer0 不与 spriteMask 起作用,spriteRenderer1 处于 `Layer1`,和 spriteMask 影响的遮罩层有交集,所以 spriteRenderer1 与 spriteMask 起作用。 + +## 使用 + +### 添加精灵遮罩组件 + +当我们需要对一个精灵进行遮罩的时候,首先需要创建一个实体,并添加精灵遮罩组件,如下: + +![mask-create](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*GYVBTbTvqU4AAAAAAAAAAAAADjCHAQ/original) + +### 设置遮罩区域 + +精灵遮罩组件通过图片来表示遮罩区域,这里我们通过组件的 `sprite` 参数来设置精灵资源,如下: + +![mask-sprite](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*k5GsSYqQTKoAAAAAAAAAAAAADjCHAQ/original) + +### 设置精灵的遮罩类型 + +通过以上两个步骤,会发现遮罩还是没有任何效果,这是因为当前的精灵的遮罩类型还是默认的(None),我们设置场景中精灵的 `mask interaction` 为内遮罩类型,效果如下: + +![mask-interaction](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*GdxhSYLY4EIAAAAAAAAAAAAADjCHAQ/original) + +### 设置 alpha cutoff + +这个参数表示当前 mask 有效 `alpha` 值的下限(范围:`0~1`),即 sprite 的纹理中 alpha 值小于 alpha cutoff 的将被丢弃(也就是不会当作遮罩区域)。我们可以通过动态调整这个属性的值来看下实际效果,如下: + +![mask-alpha](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*2CLjT7UTVa8AAAAAAAAAAAAADjCHAQ/original) + +同样的,在脚本中我们可以用如下代码使用精灵遮罩: + +```typescript +// 创建一个遮罩实体 +const spriteEntity = rootEntity.createChild(`spriteMask`); +// 给实体添加 SpriteMask 组件 +const spriteMask = spriteEntity.addComponent(SpriteMask); +// 通过 texture 创建 sprite 对象 +const sprite = new Sprite(engine, texture); +// 设置 sprite +spriteMask.sprite = sprite; +// mask 的 sprite 中纹理 alpha 小于 0.5 的将被丢弃 +spriteMask.alphaCutoff = 0.5; +// mask 对所有遮罩层的精灵都生效 +spriteMask.influenceLayers = SpriteMaskLayer.Everything; +// mask 只对处于遮罩层 Layer0 的精灵有效 +spriteMask.influenceLayers = SpriteMaskLayer.Layer0; +// mask 对处于遮罩层 Layer0 和 Layer1 的精灵有效 +spriteMask.influenceLayers = SpriteMaskLayer.Layer0 | SpriteMaskLayer.Layer1; + +// 设置遮罩类型 +spriteRenderer.maskInteraction = SpriteMaskInteraction.VisibleInsideMask; +// 设置精灵处于哪个遮罩层 +spriteRenderer.maskLayer = SpriteMaskLayer.Layer0; +``` diff --git a/docs/graphics/2D/spriteRenderer.md b/docs/graphics/2D/spriteRenderer.md new file mode 100644 index 000000000..9cbd393e0 --- /dev/null +++ b/docs/graphics/2D/spriteRenderer.md @@ -0,0 +1,96 @@ +--- +order: 2 +title: 精灵渲染器 +type: 图形 +group: 2D +label: Graphics/2D +--- + +[SpriteRenderer](/apis/core/#SpriteRenderer) 组件用于在 3D/2D 场景中显示图片。 + +> 注意:精灵渲染器默认在 XoY 平面上放置图片。 + +avatar + +## 属性 + +![属性面板](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*pcbLSahH--YAAAAAAAAAAAAADjCHAQ/original) + +| 属性名 | 属性类型 | 描述 | +| :----------------------------------------------------------- | :-------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | +| [sprite](/apis/core/#SpriteRenderer-sprite) | [Sprite](/apis/core/#Sprite) | 使用精灵的引用 | +| [width](/apis/core/#SpriteRenderer-width) | Number | 精灵渲染器的宽,若开发者未自定义渲染器宽度,则默认为精灵宽度 | +| [height](/apis/core/#SpriteRenderer-height) | Number | 精灵渲染器的高,若开发者未自定义渲染器高度,则默认为精灵高度 | +| [color](/apis/core/#SpriteRenderer-color) | [Color](/apis/math/#Color) | 精灵颜色 | +| [flipX](/apis/core/#SpriteRenderer-flipX) | Boolean | 渲染时是否 X 轴翻转 | +| [flipY](/apis/core/#SpriteRenderer-flipY) | Boolean | 渲染时是否 Y 轴翻转 | +| [drawMode](/apis/core/#SpriteRenderer-drawMode) | [SpriteDrawMode](/apis/core/#SpriteDrawMode) | 绘制模式,支持普通,九宫和平铺绘制模式 | +| [maskInteraction](/apis/core/#SpriteRenderer-maskInteraction) | [SpriteMaskInteraction](/apis/core/#SpriteMaskInteraction) | 遮罩类型,用于设置精灵是否需要遮罩,以及需要遮罩的情况下,是显示遮罩内还是遮罩外的内容 | +| [maskLayer](/apis/core/#SpriteRenderer-maskLayer) | [SpriteMaskLayer](/apis/core/#SpriteMaskLayer) | 精灵所属遮罩层,用于和 SpriteMask 进行匹配,默认为 Everything,表示可以和任何 SpriteMask 发生遮罩 | + +## 使用 + +### 创建 + +#### 创建带精灵渲染器的节点 + +通过在 **[层级面板](/docs/interface-hierarchy)** 选中某个节点,依次 **右键** -> **2D Object** -> **Sprite Renderer** 即可快速为选中节点添加一个装载了精灵渲染器的子节点。 + +avatar + +#### 为节点挂载精灵渲染器 + +为已存在的节点挂载精灵渲染器,只需在选中节点的 **[检查器面板](/docs/interface-inspector)** ,依次选择 **Add Component** -> **2D** -> **Sprite Renderer** 即可为该节点挂载精灵渲染器。 + +avatar + +#### 脚本创建 + +同样的,在脚本中我们可以用如下代码为节点挂载精灵渲染器: + +```typescript +const spriteRenderer = entity.addComponent(SpriteRenderer); +spriteRenderer.sprite = sprite; +``` + +### 设置精灵 + +需要显示图片的时候,需要先给一个实体添加精灵组件,然后设置精灵资产,如下: + +avatar + +### 渲染尺寸 + +设置 `SpriteRenderer` 的 `width` 与 `height` 可以明确指定精灵在三维空间中显示的尺寸,若没有设置,则会将 `Sprite` 的尺寸作为默认值。 + + + +### 设置颜色 + +可以通过设置 `color` 属性来调整颜色,从而实现一些淡入淡出的效果,如下: + +avatar + +### 图片翻转 + +除了基本的图片显示,`SpriteRenderer` 还支持图片的翻转,只需要通过设置属性 `flipX/flipY` 即可完成翻转,如下: + +avatar + + + +### 绘制模式 + +精灵渲染器目前提供三种绘制模式,分别是普通绘制,九宫绘制与平铺绘制(默认为普通绘制),在不同的绘制模式下,修改绘制宽高可以直观地感受到各种模式之间的渲染差异,如下: + + + +### 遮罩 + +请参考[精灵遮罩](/docs/graphics-2d-spriteMask)文档。 + +## 自定义材质 + +请参考[自定义着色器](/docs/graphics-shader-custom)文档。 + + diff --git a/docs/graphics/2D/text.md b/docs/graphics/2D/text.md new file mode 100644 index 000000000..9babdc145 --- /dev/null +++ b/docs/graphics/2D/text.md @@ -0,0 +1,185 @@ +--- +order: 4 +title: 文字渲染器 +type: 图形 +group: 2D +label: Graphics/2D +--- + +[TextRenderer](/apis/core/#TextRenderer) 组件用于在 3D/2D 场景中显示文字。 + +## 编辑器使用 + +### 添加文本组件 + +需要显示文本的时候,需要先给一个实体添加文本组件,如下: + +![添加文本组件](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*3d5AQYTtcNkAAAAAAAAAAAAADjCHAQ/original) + +### 参数说明 + +选中一个带有 TextRenderer 组件的实体,可以在右侧 inspector 中设置所有相关属性来对文本组件进行各种设置: +![添加文本组件](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*9XKjSYHZQWsAAAAAAAAAAAAADjCHAQ/original) + +属性说明如下: +| 属性 | 功能说明 | +| :--- | :--- | +| `Text` | 需要显示的文本 | +| `Color` | 文本颜色 | +| `FontSize` | 文本的字体大小 | +| `Font` | 自定义字体 | +| `Width` | 文本在三维空间中的宽,用于包围盒的计算和在需要多行显示文本时会结合宽高来确认换行原则 | +| `Height` | 文本在三维空间中的高,用于包围盒的计算和在需要多行显示文本时会结合宽高来确认换行原则| +| `LineSpacing` | 行间距 | +| `FontStyle` | 字体样式设置:是否加粗/是否斜体 | +| `HorizontalAlignment` | 水平对齐方式,可选值有:Left/Center/Right | +| `VerticalAlignment` | 竖直对齐方式,可选值有:Top/Center/Bottom | +| `EnableWrapping` | 是否开启换行模式,打开换行模式后,会根据设置的宽来进行换行,如果这时候宽设置为 0,那么文本将不渲染 | +| `OverflowMode` | 当文本总高度超出设置的高的时候的处理方式,可选值有:Overflow/Truncate, Overflow 表示直接溢出显示, Truncate 表示只保留设置高度以内的内容显示,具体显示内容还和文本在竖直方向上的对齐方式有关| +| `Mask Interaction` | 遮罩类型,用于设置文本是否需要遮罩,以及需要遮罩的情况下,是显示遮罩内还是遮罩外的内容 | +| `Mask Layer` | 文本所属遮罩层,用于和 SpriteMask 进行匹配,默认为 Everything,表示可以和任何 SpriteMask 发生遮罩 | +| `priority` | 渲染优先级,值越小,渲染优先级越高,越优先被渲染 | + +### 设置显示文本 + +添加完文本组件后,可以设置 Text 属性来显示需要的文本,如下: + +![设置显示文本](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*J6nKTJOOm4kAAAAAAAAAAAAADjCHAQ/original) + +### 设置自定义字体 + +为了让文本的显示更为丰富,开发者可以上传自己的字体文件,目前编辑器支持的字体文件格式有:**.ttf**、**.otf**、**.woff** + +![设置字体](https://mdn.alipayobjects.com/huamei_w6ifet/afts/img/A*CgA5S5vneeMAAAAAAAAAAAAADjCHAQ/original) + +## 脚本使用 + + + +1、创建 [TextRenderer](/apis/core/#TextRenderer) 组件显示文本 +2、通过 font 设置 [Font](/apis/core/#Font) 对象 +3、通过 text 设置需要显示的文本 +3、通过 fontSize 设置字体大小 +4、通过 color 设置文本颜色 + +```typescript +import { + Camera, + Color, + Font, + FontStyle, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +const textEntity = rootEntity.createChild("text"); +// 给实体添加 TextRenderer 组件 +const textRenderer = textEntity.addComponent(TextRenderer); +// 通过 font 设置 Font 对象 +textRenderer.font = Font.createFromOS(engine, "Arial"); +// 通过 text 设置需要显示的文本 +textRenderer.text = "Galacean 会写字了!"; +// 通过 fontSize 设置字体大小 +textRenderer.fontSize = 36; +// 通过 color 设置文本颜色 +textRenderer.color.set(1, 0, 0, 1); +``` + +### 设置宽高 + +可以通过 width/height 来设置文本在三维空间中的大小,主要有以下几个用处: +1、用于包围盒的计算 +2、在需要多行显示文本时会结合宽高来确认换行原则 + +```typescript +// 设置宽 +textRenderer.width = 10; +// 设置高 +textRenderer.height = 10; +``` + +### 设置行间距 + +当需要显示多行文本时,可以通过 lineSpacing 来设置两行文本在竖直方向的间距 + +```typescript +// 设置行间距 +textRenderer.lineSpacing = 0.1; +``` + +### 多行文本显示 + +当文本过长时,可能希望文本能够多行来显示,这时候可以通过 enableWrapping 字段来设置开启换行模式,打开换行模式后,会根据前面设置的宽来进行换行,如果这时候宽设置为 0,那么文本将不渲染 + +```typescript +// 打开换行模式 +textRenderer.enableWrapping = true; +``` + +### 文本截取 + +当显示多行文本时,可能存在文本行数过多,这时候可以通过 overflowMode 字段设置是否截取一部分显示,只保留设置高度以内的内容显示,具体显示内容还和文本在竖直方向上的对齐方式有关(相见:文本对齐),如下: + +```typescript +// 文本溢出 +textRenderer.overflowMode = OverflowMode.Overflow; +// 文本截取 +textRenderer.overflowMode = OverflowMode.Truncate; +``` + +### 文本对齐 + +文本对齐用来设置在指定宽高的情况下,文本如何在宽高内显示,如下: + +| 属性名 | 属性类型 | 描述 | +| :----------------------------------------------------------------- | :------------------------------------------------------------ | :------------------------------------------------------------------------------ | +| [horizontalAlignment](/apis/core/#TextRenderer-horizontalAlignment) | [TextHorizontalAlignment](/apis/core/#TextHorizontalAlignment) | 水平方向对齐方式:Left/Center/Right 分别代表 左对齐/居中对齐/右对齐 显示 | +| [verticalAlignment](/apis/core/#TextRenderer-horizontalAlignment) | [TextVerticalAlignment](/apis/core/#TextVerticalAlignment) | 竖直方向对齐方式:Top/Center/Bottom 分别代表 顶部开始显示/居中显示/底部开始显示 | + +### 文本的字体样式 + +文本的字体样式用来设置文本是否加粗显示,是否斜体显示,如下: + +| 属性名 | 属性类型 | 描述 | +| :--------------------------------------------- | :-------------------------------- | :------------------------------------------------------ | +| [fontStyle](/apis/core/#TextRenderer-fontStyle) | [FontStyle](/apis/core/#FontStyle) | 字体样式:None/Bold/Italic 分别代表 正常/加粗/斜体 显示 | + +使用方式如下: + +```typescript +// 正常显示 +textRenderer.fontStyle = FontStyle.None; +// 加粗显示 +textRenderer.fontStyle = FontStyle.Bold; +// 斜体显示 +textRenderer.fontStyle = FontStyle.Italic; +// 既加粗又斜体显示 +textRenderer.fontStyle = FontStyle.Bold | FontStyle.Italic; +``` + +### 多行文本 + + + +### 自定义字体 + +[Font](/apis/core/#Font) 是字体资源,用于表示文本使用的字体。 + +| 属性名 | 属性类型 | 描述 | +| :----------------------------- | :------- | :------------------------------------------------------------------------- | +| [name](/apis/core/#Sprite-name) | string | 字体资源名称,用来唯一标识一个字体资源,目前用这个字段来表示需要的系统字体 | + +```typescript +const font = Font.createFromOS(engine, "Arial"); +``` + +目前支持格式:ttf/otf/woff + +```typescript +const font = await engine.resourceManager.load({ + url: "https://lg-2fw0hhsc-1256786476.cos.ap-shanghai.myqcloud.com/Avelia.otf", +}); +``` + + diff --git a/docs/graphics/_meta.json b/docs/graphics/_meta.json new file mode 100644 index 000000000..b7ad4ab62 --- /dev/null +++ b/docs/graphics/_meta.json @@ -0,0 +1,35 @@ +{ + "camera": { + "title": "相机" + }, + "background": { + "title": "背景" + }, + "light": { + "title": "灯光" + }, + "renderer": { + "title": "渲染器" + }, + "model": { + "title": "模型" + }, + "mesh": { + "title": "网格" + }, + "material": { + "title": "材质" + }, + "shader": { + "title": "着色器" + }, + "texture": { + "title": "纹理" + }, + "2D": { + "title": "2D" + }, + "particle": { + "title": "粒子" + } +} diff --git a/docs/graphics/background/background.md b/docs/graphics/background/background.md new file mode 100644 index 000000000..4cad60cac --- /dev/null +++ b/docs/graphics/background/background.md @@ -0,0 +1,21 @@ +--- +order: 0 +title: 背景总览 +type: 图形 +group: 背景 +label: Graphics/Background +--- + +开发者可以为场景定制背景,背景会在场景渲染前被渲染。当前 Galacean 主要有以下几种背景类型: + +- [纯色背景](/docs/graphics-background-solidColor) +- [纹理背景](/docs/graphics-background-texture) +- [天空背景](/docs/graphics-background-sky) + +开发者可依据自己的需求设置不同的背景: + + + +在[天空盒](/docs/graphics-background-sky)模式下,通过设置特定的网格和材质,可以实现各种定制背景,如`视频背景`: + + diff --git a/docs/graphics/background/sky.md b/docs/graphics/background/sky.md new file mode 100644 index 000000000..1ce85f0a5 --- /dev/null +++ b/docs/graphics/background/sky.md @@ -0,0 +1,96 @@ +--- +order: 3 +title: 天空 +type: 图形 +group: 背景 +label: Graphics/Background +--- + +天空是摄像机在渲染之前绘制的一种背景类型。此类型的背景对于 3D 游戏和应用程序非常有用,因为它可以提供深度感,使环境看上去比实际大小大得多。天空本身可以包含任何对象(例如云、山脉、建筑物和其他无法触及的对象)以营造遥远三维环境的感觉。Galacean 还可以将天空用于在场景中产生真实的环境光照,详情可参考[烘焙](/docs/graphics-light-bake)。 + +天空模式下,开发者可以自行设置`材质`和`网格`,通过 Galacean 内置的`天空盒`与`程序化天空`可以一键设置期望的天空效果。 + +## 设置天空盒 + +在编辑器中,只需按照如下步骤即可为背景设置天空盒: + +### 1. 创建天空盒纹理 + +> 可以在 [Poly Haven](https://polyhaven.com/) 或 [BimAnt HDRI](http://hdri.bimant.com/) 下载免费的 HDR 贴图 + +天空盒纹理就是一张[立方纹理](/docs/graphics-texture-cube),首先在准备好 HDR 后,依照路径 **[资产面板](/docs/assets-interface)** -> **右键上传** -> **选择 TextureCube(.hdr)** -> **选择对应 HDR 贴图** -> **立方纹理资产创建完毕** 操作即可。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*Oi3FSLEEaYgAAAAAAAAAAAAADhuCAQ/original) + +### 2. 创建天空盒材质 + +创建完立方纹理资产后,依照路径 **[资产面板](/docs/assets-interface)** -> **右键创建** -> **选择 Material** -> **选中生成的资产** -> **[检查器面板](/docs/interface-inspector)** -> **点击 Base 栏的 Shader 属性** -> **选择 Sky Box** -> **点击 Base 栏的 HDR** -> **选择第一步创建的立方纹理** 创建天空盒材质。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*9j2eSYkwg8MAAAAAAAAAAAAADhuCAQ/original) + +### 3. 设置天空盒 + +最后只需依照路径 **[层级面板](/docs/interface-hierarchy)** -> **选中 Scene** -> **[检查器面板](/docs/interface-inspector)** -> **Background 栏** -> **Mode 设置 Sky** -> **Material 选择第二步创建的材质** -> **Mesh 设置内置的 Cuboid** 可以看到场景的背景变成了天空盒。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*rqvsSpkGJ6UAAAAAAAAAAAAADhuCAQ/original) + +### 代码设置天空盒 + +```typescript +// 创建天空盒纹理 +const textureCube = await engine.resourceManager.load({ + urls: [ + "px - right 图片 url", + "nx - left 图片 url", + "py - top 图片 url", + "ny - bottom 图片 url", + "pz - front 图片 url", + "nz - back 图片 url", + ], + type: AssetType.TextureCube, +}); +// 创建天空盒材质 +const skyMaterial = new SkyBoxMaterial(engine); +skyMaterial.texture = textureCube; +// 设置天空盒 +const background = scene.background; +background.mode = BackgroundMode.Sky; +background.sky.material = skyMaterial; +background.sky.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); +``` + +## 设置程序化天空 + +程序化天空是编辑器在 3D 项目中的默认背景,您也可以依照路径 **[层级面板](/docs/interface-hierarchy)** -> **选中 Scene** -> **[检查器面板](/docs/interface-inspector)** -> **Background 栏** -> **Mode 设置 Sky** -> **Material 选择内置的 SkyMat 材质** -> **Mesh 设置内置的 Sphere** + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*Qe3IRJ9ciNoAAAAAAAAAAAAADhuCAQ/original) + +### 代码设置程序化天空 + +```typescript +// 创建大气散射材质 +const skyMaterial = new SkyProceduralMaterial(engine); +// 设置天空盒 +const background = scene.background; +background.mode = BackgroundMode.Sky; +background.sky.material = skyMaterial; +background.sky.mesh = PrimitiveMesh.createSphere(engine); +``` + +### 属性 + +在大气散射材质的 **[检查器面板](/docs/interface-inspector)** 可以看到很多可调整的属性: + +image-4 + +> 内置的大气散射材质无法随意调整属性,开发者可以自行创建并调整。 + +| 属性名称 | 解释 | +| :-------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | +| [exposure](/apis/core/#SkyProceduralMaterial-exposure) | 天空的曝光,值越大天空越亮。 | +| [sunMode](/apis/core/#SkyProceduralMaterial-sunMode) | 天空中生成太阳所使用的方法,包含 `None`,`Simple` 和 `HighQuality`,其中 None 不生成太阳,Simple 生成简单的太阳,而 HighQuality 生成可定义外观的太阳。 | +| [sunSize](/apis/core/#SkyProceduralMaterial-sunSize) | 太阳的尺寸,值越大太阳越大。 | +| [sunSizeConvergence](/apis/core/#SkyProceduralMaterial-sunSizeConvergence) | 太阳的大小收敛,当且仅当太阳的生成模式为 `HighQuality` 时有效 | +| [atmosphereThickness](/apis/core/#SkyProceduralMaterial-atmosphereThickness) | 大气的密度,更高的密度会吸收更多的光线 | +| [skyTint](/apis/core/#SkyProceduralMaterial-skyTint) | 天空的颜色 | +| [groundTint](/apis/core/#SkyProceduralMaterial-groundTint) | 地面的颜色 | diff --git a/docs/graphics/background/solidColor.md b/docs/graphics/background/solidColor.md new file mode 100644 index 000000000..56c1397e1 --- /dev/null +++ b/docs/graphics/background/solidColor.md @@ -0,0 +1,36 @@ +--- +order: 1 +title: 纯色 +type: 图形 +group: 背景 +label: Graphics/Background +--- + +当场景的背景类型设置为纯色时,画布的渲染区域会在相机渲染前被填充上对应的纯色背景。 + +## 设置纯色背景 + +依据路径 **[层级面板](/docs/interface-hierarchy)** -> **选中 Scene** -> **[检查器面板](/docs/interface-inspector)** -> **Background 栏** 设置 **Mode** 为 **Solid Color**,然后选择期望的背景色,可以看到场景的背景发生实时变化。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*RDQ-T5h7YdEAAAAAAAAAAAAADhuCAQ/original) + +同样的,在脚本中也可通过如下代码进行设置: + +```typescript +// 获取当前场景的背景实例 +const background = scene.background; +// 设置背景类型为纯色 +background.mode = BackgroundMode.SolidColor; +// 设置特定的背景色 +background.solidColor.set(0.25, 0.25, 0.25, 1.0); +// 设置为(0,0,0,0) 可以透出网页背景 +background.solidColor.set(0, 0, 0, 0); +``` + +## 属性 + +需要注意的是,背景的相关属性都在场景的 `background` 属性中,获取到该属性实例后才修改相关属性才能生效。 + +| 属性 | 作用 | +| :--------- | :----------- | +| solidColor | 设置背景颜色 | diff --git a/docs/graphics/background/texture.md b/docs/graphics/background/texture.md new file mode 100644 index 000000000..d8b0498ac --- /dev/null +++ b/docs/graphics/background/texture.md @@ -0,0 +1,52 @@ +--- +order: 2 +title: 纹理 +type: 图形 +group: 背景 +label: Graphics/Background +--- + +当场景的背景类型设置为纹理时,画布的渲染区域会在相机渲染前会按照填充规则填上对应的纹理。 + +## 设置纯色背景 + +依据路径 **[层级面板](/docs/interface-hierarchy)** -> **选中 Scene** -> **[检查器面板](/docs/interface-inspector)** -> **Background 栏** 设置 **Mode** 为 **Texture**,然后选择期望的纹理,可以看到场景的背景发生实时变化。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*-JYDQoHcbSsAAAAAAAAAAAAADhuCAQ/original) + +同样的,在脚本中也可通过如下代码进行设置: + +```typescript +// 获取当前场景的背景实例 +const background = scene.background; +// 设置背景类型为纹理 +background.mode = BackgroundMode.Texture; +// 将一张 2D 纹理设置为背景纹理 +background.texture = await engine.resourceManager.load({ + url: "XXX.jpg", + type: AssetType.Texture2D, +}); +// 设置填充模式为铺满 +background.textureFillMode = BackgroundTextureFillMode.Fill; +``` + +## 属性 + +需要注意的是,背景的相关属性都在场景的 `background` 属性中,获取到该属性实例后才修改相关属性才能生效。 + +| 属性 | 作用 | +| :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| texture | 设置背景纹理 | +| textureFillMode | 设置背景纹理的填充模式,可选 [AspectFitWidth](/apis/core/#BackgroundTextureFillMode-AspectFitWidth), [AspectFitHeight](/apis/core/#BackgroundTextureFillMode-AspectFitHeight) 或 [Fill](/apis/core/#BackgroundTextureFillMode-Fill) , 默认为 `BackgroundTextureFillMode.AspectFitHeight` | + +### 填充模式 + +通过 `background.textureFillMode = BackgroundTextureFillMode.AspectFitWidth` 设置纹理适配模式。 + +目前纹理适配模式有以下三种: + +| 填充模式 | 说明 | +| ----------------------------------------------------------------------- | -------------------------------------------------- | +| [AspectFitWidth](/apis/core/#BackgroundTextureFillMode-AspectFitWidth) | 保持宽高比,把纹理宽缩放至 Canvas 的宽,上下居中。 | +| [AspectFitHeight](/apis/core/#BackgroundTextureFillMode-AspectFitHeight) | 保持宽高比,把纹理高缩放至 Canvas 的高,左右居中。 | +| [Fill](/apis/core/#BackgroundTextureFillMode-Fill) | 把纹理的宽高填满 Canvas 的宽高。 | diff --git a/docs/graphics/camera/camera.md b/docs/graphics/camera/camera.md new file mode 100644 index 000000000..dca30df30 --- /dev/null +++ b/docs/graphics/camera/camera.md @@ -0,0 +1,56 @@ +--- +order: 0 +title: 相机总览 +type: 图形 +group: 相机 +label: Graphics/Camera +--- + +相机是一个图形引擎对 [3D 投影](https://en.wikipedia.org/wiki/3D_projection)的抽象概念,作用好比现实世界中的摄像机或眼睛。Galacean 的相机实现了自动视锥剔除,只渲染视锥体内的物体。 + +## 相机的类型 + +### 透视投影 + +透视投影符合我们的近大远小模型,可以看一下透视模型示意图: + +image.png + +如上图所示,近裁剪平面([nearClipPlane](/apis/core/#Camera-nearClipPlane)),远裁剪平面([farClipPlane](/apis/core/#Camera-farClipPlane))和 视角([fieldOfView](/apis/core/#Camera-fieldOfView)) 会形成一个视椎体 ([_View Frustum_](https://en.wikipedia.org/wiki/Viewing_frustum))。在视椎体内部的物体是会被投影到摄像机里的,也就是会渲染在画布上,而视椎体外的物体则会被裁剪。 + +### 正交投影 + +正交投影就是可视区近处和远处看到的物体是等大小的。由正交投影模型产生的可视区称为盒状可视区,盒状可视区模型如下: + +image.png + +如上图所示,有 top、bottom、left 和 right,Galacean 对正交属性做了一些简化,更符合开发者的使用习惯,只有 [orthographicSize](/apis/core/#Camera-orthographicSize)。下面是针对各项属性和 [orthographicSize](/apis/core/#Camera-orthographicSize) 的关系 + +- `top = orthographicSize` +- `bottom = -orthographicSize` +- `right = orthographicSize * aspectRatio` +- `left = -orthographicSize * aspectRatio` + +### 如何选择 + +经过对透视投影和正交投影的比较,可发现他们的不同点: + +- 可视区域模型 +- 是否有近大远小的效果 + +通过以下示例能直观感受到正交相机与透视相机渲染效果的差异,简而言之,当需要展示 2D 效果时,就选择正交相机,当需要展示 3D 效果时,就选择透视相机。 + + + +## 相机的朝向 + +Galacean 中的局部坐标与世界坐标遵循`右手坐标系`,因此相机的 `forward` 方向为 `-Z` 轴,相机取景的方向也是 `-Z` 方向。 + +## 上手 + +介绍了相机的基本概念,接下来让我们上手: + +- 在场景中添加[相机组件](/docs/graphics-camera-component) +- 通过[相机控件](/docs/graphics-camera-control)来更方便地操控[相机组件](/docs/graphics-camera-component) +- 在场景中使用[多相机](graphics-camera-multiCamera) +- 获取[相机深度纹理](graphics-camera-depthTexture) diff --git a/docs/graphics/camera/component.md b/docs/graphics/camera/component.md new file mode 100644 index 000000000..3f06e8688 --- /dev/null +++ b/docs/graphics/camera/component.md @@ -0,0 +1,138 @@ +--- +order: 1 +title: 相机组件 +type: 图形 +group: 相机 +label: Graphics/Camera +--- + +相机组件可以将 3D 场景投影到 2D 屏幕上,基于相机组件,我们可以定制各种不同的渲染效果。 + +首先需要将相机组件挂载到在场景中已激活的 [Entity](/docs/core-entity) 上,编辑器项目通常已经自带了相机组件,当然我们也可以自己手动添加。 + +image-20231009114711623 + +添加完毕后,就可以在检查器里可以查看相机属性,并且左下角的相机预览可以方便地查看项目实际运行时的相机效果: + +image-20231009114816056 + +您也可以在脚本中通过如下代码为 [Entity](/docs/core-entity) 挂载相机组件: + +```typescript +// 创建实体 +const entity = root.createChild("cameraEntity"); +// 创建相机组件 +const camera = entity.addComponent(Camera); +``` + +## 属性 + +通过修改相机组件的属性可以定制渲染效果。下方是相机组件在 **[检查器面板](/docs/interface-inspector)** 暴露的属性设置。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*Za1RSJcYrSMAAAAAAAAAAAAADhuCAQ/original) + +也可以通过脚本去获取相机组件并设置相应的属性。 + +```typescript +// 从挂载相机的节点上获取相机组件 +const camera = entity.getComponent(Camera); +// 设置相机类型 +camera.isOrthographic = true; +// 设置相机的近平面 +camera.nearClipPlane = 0.1; +// 设置相机的远平面 +camera.farClipPlane = 100; +// 设置相机的 FOV(角度制) +camera.fieldOfView = 45; +// 设置相机在画布上的渲染区域(归一化) +camera.viewport = new Vector4(0, 0, 1, 1); +// 设置相机的渲染优先级(值越小,渲染优先级越高) +camera.priority = 0; +// 设置相机是否开启视锥体裁剪 +camera.enableFrustumCulling = true; +// 设置相机渲染前的清除标记 +camera.clearFlags = CameraClearFlags.All; +``` + +其中每个属性对应的功能如下: + +| 类型 | 属性 | 解释 | +| :------- | :------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | +| 通用 | [isOrthographic](/apis/core/#Camera-isOrthographic) | 通过设置 [isOrthographic](/apis/core/#Camera-isOrthographic) 来决定透视投影或正交投影。,默认是 `false` | +| | [nearClipPlane](/apis/core/#Camera-nearClipPlane) | 近裁剪平面 | +| | [farClipPlane](/apis/core/#Camera-farClipPlane) | 远裁剪平面 | +| | [viewport](/apis/core/#Camera-viewport) | 视口,确定内容最后被渲染到目标设备里的范围。 | +| | [priority](/apis/core/#Camera-priority) | 渲染优先级,用来确定在多相机的情况下按照什么顺序去渲染相机包含的内容。 | +| | [enableFrustumCulling](/apis/core/#Camera-enableFrustumCulling) | 是否开启视锥剔除,默认为 `true` | +| | [clearFlags](/apis/core/#Camera-clearFlags) | 在渲染这个相机前清理画布缓冲的标记 | +| | [cullingMask](/apis/core/#Camera-cullingMask) | 裁剪遮罩,用来选择性地渲染场景中的渲染组件。 | +| | [aspectRatio](/apis/core/#Camera-aspectRatio) | 渲染目标的宽高比,一般是根据 canvas 大小自动计算,也可以手动改变(不推荐) | +| | [renderTarget](/apis/core/#Camera-renderTarget) | 渲染目标,确定内容被渲染到哪个目标上。 | +| | [pixelViewport](/apis/core/#Camera-pixelViewport) | 屏幕上相机的视口(以像素坐标表示)。 在像素屏幕坐标中,左上角为(0, 0),右下角为(1.0, 1.0)。 | +| 透视投影 | [fieldOfView](/apis/core/#Camera-fieldOfView) | 视角 | +| 正交投影 | [orthographicSize](/apis/core/#Camera-orthographicSize) | 正交模式下相机的一半尺寸 | +| 渲染相关 | [depthTextureMode](<(/apis/core/#Camera-depthTextureMode)>) | 深度纹理模式,默认为`DepthTextureMode.None`,如果开启,可以在 shader 中使用 `camera_DepthTexture` 深度纹理。 +| | [opaqueTextureEnabled](<(/apis/core/#Camera-opaqueTextureEnabled)>) | 是否启用非透明纹理,默认关闭,如果启用,可以在透明队列的 shader 中使用 `camera_OpaqueTexture` 非透明纹理。 | +| | [opaqueTextureDownsampling](<(/apis/core/#Camera-opaqueTextureDownsampling)>) | 启用非透明纹理时,可以设置降采样,可以根据清晰度需求和性能要求来进行设置。 | +| | [msaaSamples](<(/apis/core/#Camera-msaaSamples)>) | 可以设置独立画布的多样本抗锯齿采样样本数量,仅当非透明纹理开启,且没有设置 renderTarget 时才生效。 | +| | [independentCanvasEnabled](<(/apis/core/#Camera-independentCanvasEnabled)>) | 只读属性,是否启用独立画布,仅当非透明纹理开启,且没有设置 renderTarget 时才生效。 | + +### 裁剪遮罩 + +相机组件可以通过设置 `cullingMask` 选择性地渲染场景内的渲染组件 + + + +### 渲染目标 + +相机组件可以通过设置 `renderTarget` 将渲染结果渲染到不同的目标上。 + + + +### 视锥剔除 + +`enableFrustumCulling` 属性默认是开启的,因为对于三维世界来说,“看不见的东西就不需要渲染”是个很自然的逻辑,属于最基本的性能优化。关闭视锥剔除意味着关闭此项优化。如果你想保留此项优化,而只想让某个节点始终渲染,可以把节点的渲染器的包围盒设置成无限大。 + + + +## 方法 + +相机组件提供各种方法(主要涉及`渲染`与`空间转换`)方便开发者实现期望的定制能力。 + +| 类型 | 属性 | 解释 | +| :------- | :----------------------------------------------------------------- | :--------------------------------------- | +| 渲染 | [resetProjectionMatrix](/apis/core/#Camera-resetProjectionMatrix) | 重置自定义投影矩阵,恢复到自动模式。 | +| | [resetAspectRatio](/apis/core/#Camera-resetAspectRatio) | 重置自定义渲染横纵比,恢复到自动模式。 | +| | [render](/apis/core/#Camera-render) | 手动渲染。 | +| | [setReplacementShader](/apis/core/#Camera-setReplacementShader) | 设置全局渲染替换着色器。 | +| | [resetReplacementShader](/apis/core/#Camera-resetReplacementShader) | 清空全局渲染替换着色器。 | +| 空间转换 | [worldToViewportPoint](/apis/core/#Camera-worldToViewportPoint) | 将一个点从世界空间转换到视口空间。 | +| | [viewportToWorldPoint](/apis/core/#Camera-viewportToWorldPoint) | 将一个点从视口空间转换到世界空间。 | +| | [viewportPointToRay](/apis/core/#Camera-viewportPointToRay) | 通过视口空间中的一个点生成世界空间射线。 | +| | [screenToViewportPoint](/apis/core/#Camera-screenToViewportPoint) | 将一个点从屏幕空间转换到视口空间。 | +| | [viewportToScreenPoint](/apis/core/#Camera-viewportToScreenPoint) | 将一个点从视口空间转换到屏幕空间。 | +| | [worldToScreenPoint](/apis/core/#Camera-worldToScreenPoint) | 将一个点从世界空间转换到屏幕空间。 | +| | [screenToWorldPoint](/apis/core/#Camera-screenToWorldPoint) | 将一个点从屏幕空间转换到世界空间。 | +| | [screenPointToRay](/apis/core/#Camera-screenPointToRay) | 通过屏幕空间中的一个点生成世界空间射线。 | + +## 获取相机组件 + +在清楚相机组件挂载在哪个节点的前提下,可直接通过 `getComponent` 或 `getComponentsIncludeChildren` 获取: + +```typescript +// 从挂载相机的节点上获取相机组件 +const camera = entity.getComponent(Camera); +// 从挂载相机节点的父节点上获取相机组件(不推荐) +const cameras = entity.getComponentsIncludeChildren(Camera, []); +``` + +若不清楚相机组件挂载的节点,也可以通过较为 Hack 的方式获取场景中的所有相机组件: + +```typescript +// 获取这个场景中的所有相机组件(不推荐) +const cameras = scene._activeCameras; +``` + +## onBeginRender 与 onEndRender + +相机组件额外包含了 [onBeginRender](/apis/core/#Script-onBeginRender) 与 [onEndRender](/apis/core/#Script-onEndRender) 两个生命周期回调,它们的时序可参考[脚本生命周期时序图](/docs/script) diff --git a/docs/graphics/camera/control.md b/docs/graphics/camera/control.md new file mode 100644 index 000000000..3725b05f2 --- /dev/null +++ b/docs/graphics/camera/control.md @@ -0,0 +1,60 @@ +--- +order: 2 +title: 相机控件 +type: 图形 +group: 相机 +label: Graphics/Camera +--- + +相机控件就是和相机组件一起搭配来展示三维场景的组件,这类组件根据不同的功能定制相应的参数,通过影响着相机的属性来控制三维场景的展示。 + +相机控件继承于功能强大的脚本,挂载在包含 `Camera` 组件的 `Entity` 上,因此可以顺其自然地拿到 `Camera` ,在生命周期函数中响应外部输入并执行相应的操作,**此类控件目前无法在编辑器中操作添加,需开发者在脚本中自行添加。** + +## 轨道控制器 + +`OrbitControl`  用来模拟轨道交互,适用于围绕一个目标对象进行 360 度旋转交互,需要注意的是,**请务必在添加相机组件后再添加轨道控制器**。 + + + +| 属性 | 解释 | +| :---------------- | :--------------------------------------------------------------- | +| `target` | 观察的目标位置 | +| `autoRotate` | 是否自动旋转,默认为 false ,可通过 autoRotateSpeed 调整旋转速度 | +| `autoRotateSpeed` | 自动旋转的速度 | +| `enableDamping` | 是否开启相机阻尼,默认为 true | +| `dampingFactor` | 旋转阻尼参数,默认为 0.1 | +| `enableKeys` | 是否支持键盘操作(上下左右键) | +| `enablePan` | 是否支持相机平移,默认为 true | +| `keyPanSpeed` | 键盘持续按下时操作的幅度 | +| `enableRotate` | 是否支持相机旋转,默认为 true | +| `rotateSpeed` | 相机旋转速度,默认为 1.0 | +| `enableZoom` | 是否支持相机缩放,默认为 true | +| `minAzimuthAngle` | onUpdate 时,水平方向操作合理范围的最小弧度,默认为负无穷大 | +| `maxAzimuthAngle` | onUpdate 时,水平方向操作合理范围的最大弧度,默认为正无穷大 | +| `minDistance` | onUpdate 时,判定的距离操作合理范围的最小值 | +| `maxDistance` | onUpdate 时,判定的距离操作合理范围的最大值 | +| `minPolarAngle` | onUpdate 时,竖直方向操作合理范围的最小弧度 | +| `maxPolarAngle` | onUpdate 时,竖直方向操作合理范围的最大弧度 | + +## 自由控制器 + +`FreeControl`  一般用于漫游控制,常见于游戏场景,需要注意的是,**请务必在添加相机组件后再添加自由控制器**。 + + + +| 属性 | 解释 | +| :-------------- | :---------------------------------------- | +| `floorMock` | 是否模拟地面,默认为 true | +| `floorY` | 配合 `floorMock` 使用,声明地面的位置信息 | +| `movementSpeed` | 移动速度 | +| `rotateSpeed` | 旋转速度 | + +#### 正交控制器 + +`OrthoControl`  一般用于控制 2D 场景中的缩放和位移: + + + +| 属性 | 解释 | +| :---------- | :------- | +| `zoomSpeed` | 缩放速度 | diff --git a/docs/graphics/camera/depthTexture.md b/docs/graphics/camera/depthTexture.md new file mode 100644 index 000000000..fbcccefa9 --- /dev/null +++ b/docs/graphics/camera/depthTexture.md @@ -0,0 +1,14 @@ +--- +order: 4 +title: 相机深度纹理 +type: 图形 +group: 相机 +label: Graphics/Camera +--- + + +相机可以通过 [depthTextureMode](<(/apis/core/#Camera-depthTextureMode)>) 属性开启深度纹理,开启深度纹理后可以通过 `camera_DepthTexture` 属性在 Shader 中访问深度纹理。深度纹理可以用于实现软粒子和水面边缘过渡,以及一些简单的后处理效果。 + + + +注意:深度纹理仅渲染非透明物体。 diff --git a/docs/graphics/camera/multiCamera.md b/docs/graphics/camera/multiCamera.md new file mode 100644 index 000000000..1c41a0a5f --- /dev/null +++ b/docs/graphics/camera/multiCamera.md @@ -0,0 +1,17 @@ +--- +order: 3 +title: 多相机渲染 +type: 图形 +group: 相机 +label: Graphics/Camera +--- + +在多个相机的情况下,通过结合相机组件的 [viewport](/apis/core/#Camera-viewport), [cullingMask](/apis/core/#Camera-cullingMask), [clearFlags](/apis/core/#Camera-clearFlags) 等属性完成许多定制化的渲染效果。 + +比如通过设置 [viewport](/apis/core/#Camera-viewport) 让多个相机分别在画布的不同位置渲染场景内容。 + + + +又比如通过设置 [cullingMask](/apis/core/#Camera-cullingMask) 实现画中画的效果。 + + diff --git a/docs/graphics/light/ambient.md b/docs/graphics/light/ambient.md new file mode 100644 index 000000000..4dde19dbf --- /dev/null +++ b/docs/graphics/light/ambient.md @@ -0,0 +1,55 @@ +--- +order: 4 +title: 环境光 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +除了实时计算的直接光源,我们一般还要提前离线烘焙光照作为环境光照来实时采样,这种方式可以有效地捕捉环境的全局光照和氛围,使物体更好地融入其环境。 + +![image-20231227151844040](https://gw.alipayobjects.com/zos/OasisHub/23397353-5434-4bde-ace7-72c8731d5581/image-20231227151844040.png) + +## 编辑器使用 + +### 1. 环境漫反射 + +image-20240219163916257 + +| 属性 | 作用 | +| :-- | :-- | +| Source | 指定漫反射来源是 `Background` 还是 `Solid Color`,默认来源 `Background`。`Background` 表示使用烘焙得到的球谐参数作为漫反射颜色; `Solid Color` 表示使用纯色作为漫反射颜色 | +| Intensity | 漫反射强度 | + +### 2. 环境镜面反射 + +image-20240219163941010 + +| 属性 | 作用 | +| :-- | :-- | +| Source | 指定镜面反射来源是 `Background` 还是 `Custom`,默认来源 `Background`。`Background` 表示使用根据背景烘焙得到的预滤波环境贴图作为镜面反射; `Custom` 表示可以单独烘焙一张 HDR 贴图作为环境反射。 | +| Intensity | 镜面反射强度 | + +## 脚本使用 + +通过[烘焙教程](/docs/graphics-light-bake)拿到烘焙产物的 url 后,通过引擎的 EnvLoader 进行加载解析: + +```typescript +engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin" + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + + // 可以调节漫反射、镜面反射强度,默认为1 + // ambientLight.diffuseIntensity = 1; + // ambientLight.specularIntensity = 1; + + // 预滤波环境贴图(ambientLight.specularTexture)还可以作为场景的背景 + // skyMaterial.texture = ambientLight.specularTexture; + // 由于烘焙产物的颜色编码方式是 RGBM,因此作为背景时需要将解码设置为 textureDecodeRGBM + // skyMaterial.textureDecodeRGBM = true; + }); +``` diff --git a/docs/graphics/light/bake.md b/docs/graphics/light/bake.md new file mode 100644 index 000000000..9e7a51f06 --- /dev/null +++ b/docs/graphics/light/bake.md @@ -0,0 +1,43 @@ +--- +order: 5 +title: 烘焙 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +烘焙指 Galacean 提前执行光照计算并将结果烘焙到二进制文件(包含[漫反射球谐参数](https://www.wikiwand.com/zh-hans/%E7%90%83%E8%B0%90%E5%87%BD%E6%95%B0)和[预滤波环境贴图](https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/)),然后在运行时实时采样。 + +bake + +我们在[编辑器](https://galacean.antgroup.com/editor) 和 [glTF 查看器](https://galacean.antgroup.com/#/gltf-viewer) 提供了烘焙工具。 + +## 编辑器使用 + +### 1. 烘焙开关 + +编辑器默认打开自动烘焙,会在修改背景(颜色、曝光、旋转等配置)或者修改烘焙分辨率等操作后,自动进行烘焙。 + +image-20240219164704802 + +也可以关闭自动烘焙,在需要的时候进行手动烘焙。 + +image-20240219164728187 + +### 2. 烘焙分辨率 + +表示烘焙后的预滤波环境贴图的分辨率,默认 128 分辨率,烘焙产物约为 500KB;64 分辨率的烘焙产物约为 125KB,可以根据场景选择合适的烘焙分辨率。 + +image-20240219164802607 + +### 3. 设置背景 + +参考 [背景教程](/docs/graphics-background-sky) 设置完场景的背景后,编辑器会根据上面设置的烘焙分辨率和烘焙开关进行光照烘焙,期间针对背景的修改(颜色、旋转、曝光、换 HDR 贴图等)都会根据烘焙开关来决定是否自动烘焙。**如果想要设置纯色背景或者透明背景,但是又不想要烘焙纯色背景,可以先关闭自动烘焙开关,然后再切换到[纯色背景](/docs/graphics-background-solidColor)。** + +image-20231009114455268 + +## glTF 查看器 + +我们在官网的 [glTF 查看器](https://galacean.antgroup.com/#/gltf-viewer) 也提供了烘焙工具,直接拖拽 HDR 贴图到网页即可自动下载烘焙产物 : + +![gltf viewer](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*9mGbSpQ4HngAAAAAAAAAAAAAARQnAQ) diff --git a/docs/graphics/light/directional.md b/docs/graphics/light/directional.md new file mode 100644 index 000000000..5a96cd853 --- /dev/null +++ b/docs/graphics/light/directional.md @@ -0,0 +1,37 @@ +--- +order: 1 +title: 方向光 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +**方向光**表示的是光线从以某个方向均匀射出,光线之间是平行的,太阳照射在地球表面的光可以认为是方向光,因为太阳和地球距离的远大于地球半径,所以照射在地球的阳光可以看作是来自同一个方向的一组平行光,即方向光。 + +image-20240319173643671 + +方向光有 3 个主要个特性:_颜色_([color](/apis/core/#DirectLight-color))、_强度_([intensity](/apis/core/#DirectLight-intensity))、_方向_([direction](/apis/core/#DirectLight-direction))。_方向_ 则由方向光所在的节点的朝向表示。 + +| 属性 | 作用 | +| :-------- | :------------------------------- | +| Intensity | 控制平行光的强度,**值越高越亮** | +| Color | 控制平行光的颜色,默认白色 | +| Culling Mask | 控制灯光需要照亮的物体,默认 Everything。 需要配合 Entity 的 Layer 来使用 | + + + +## 脚本使用 + +```typescript +const lightEntity = rootEntity.createChild("light"); +const directLight = lightEntity.addComponent(DirectLight); + +// 调整颜色 +directLight.color.set(0.3, 0.3, 1, 1); + +// 调整强度 +directLight.intensity = 2; + +// 调整方向 +lightEntity.transform.setRotation(-45, -45, 0); +``` diff --git a/docs/graphics/light/light.md b/docs/graphics/light/light.md new file mode 100644 index 000000000..7ac5a3c14 --- /dev/null +++ b/docs/graphics/light/light.md @@ -0,0 +1,33 @@ +--- +order: 0 +title: 光照总览 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +合理使用光照,能够提供逼真的渲染效果。本节包含以下相关信息: + +- 光源类型 + - [方向光](/docs/graphics-light-directional) + - [点光源](/docs/graphics-light-point) + - [聚光灯](/docs/graphics-light-spot) + - [环境光](/docs/graphics-light-ambient) +- [烘焙](/docs/graphics-light-bake) +- [阴影](/docs/graphics-light-shadow) + +## 直接光 + +直接光一般从一个区域或者朝特定方向照射,经过一次反射直接进入眼睛(相机),如下案例: + + + +## 环境光 + +环境光从四周发射进入眼睛,如下案例: + + + +## 实时光照和烘焙光照 + +实时光照指 Galacean 在运行时实时计算光照。烘焙光照指 Galacean 提前执行光照计算并将结果[烘焙](/docs/graphics-light-bake)到二进制文件(包含[漫反射球谐参数](https://www.wikiwand.com/zh-hans/%E7%90%83%E8%B0%90%E5%87%BD%E6%95%B0)和[预滤波环境贴图](https://learnopengl-cn.github.io/07%20PBR/03%20IBL/02%20Specular%20IBL/)),然后在运行时实时采样。 diff --git a/docs/graphics/light/point.md b/docs/graphics/light/point.md new file mode 100644 index 000000000..a70e10953 --- /dev/null +++ b/docs/graphics/light/point.md @@ -0,0 +1,34 @@ +--- +order: 2 +title: 点光源 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +**点光源**存在于空间中的一点,由该点向四面八方发射光线,比如生活中的灯泡就是点光源。 + +image-20240319174317201 + +点光源有 3 个主要特性:_颜色_([color](/apis/core/#PointLight-color))、_强度_([intensity](/apis/core/#PointLight-intensity))、_有效距离_([distance](/apis/core/#PointLight-distance)))。超过有效距离的地方将无法接受到点光源的光线,并且离光源越远光照强度也会逐渐降低。 + +| 属性 | 作用 | +| :----------- | :------------------------------------------------------------------------ | +| Intensity | 控制点光源的强度,**值越高越亮** | +| Color | 控制点光源的颜色,默认白色 | +| Distance | 有效距离,光照强度随距离衰减 | +| Culling Mask | 控制灯光需要照亮的物体,默认 Everything。 需要配合 Entity 的 Layer 来使用 | + +### 脚本使用 + +```typescript +const lightEntity = rootEntity.createChild("light"); +const pointLight = lightEntity.addComponent(PointLight); + +// 调整距离 +pointLight.distance = 100; +// 调整颜色 +pointLight.color.set(0.3, 0.3, 1, 1); +// 调整点光源位置 +lightEntity.transform.setPosition(-10, 10, 10); +``` diff --git a/docs/graphics/light/shadow.md b/docs/graphics/light/shadow.md new file mode 100644 index 000000000..723b0f823 --- /dev/null +++ b/docs/graphics/light/shadow.md @@ -0,0 +1,80 @@ +--- +order: 6 +title: 阴影 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +阴影能够有效增强渲染画面的立体感和真实感。在实时渲染中,一般使用所谓的 ShadowMap 技术来进行阴影的绘制,简单来说就是把光源作为一个虚拟的相机渲染场景的深度,然后从场景相机渲染画面时,比较渲染的物体与深度信息的关系,如果物体的深度比深度信息中的要深,会导致被其他物体遮挡,由此渲染阴影。 + +## 光照与阴影 + +image-20240319174904033 + +基于这样的原理就比较好理解在 `Light` 组件中有关阴影的各项属性设置: + +| 参数 | 应用 | +| :---------------------------------------------------- | :------------------- | +| [shadowType](/apis/core/#Light-shadowType) | 阴影投射类型 | +| [shadowBias](/apis/core/#Light-shadowBias) | 阴影的偏移 | +| [shadowNormalBias](/apis/core/#Light-shadowNormalBias) | 阴影的法向偏移 | +| [shadowNearPlane](/apis/core/#Light-shadowNearPlane) | 渲染深度图时的近裁面 | +| [shadowStrength](/apis/core/#Light-shadowStrength) | 阴影强度 | + +这里需要特别说明一下阴影偏移: + +![shadow-bias](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8q5MTbrlC7QAAAAAAAAAAAAAARQnAQ) + +因为深度精度问题,从相机采样时会产生伪影。所以通常需要设置阴影的偏移量,以便产生干净的阴影,如右图所示。但如果偏移量过大,阴影就会偏离投射物,可以看到右图中的影子和脚后跟分离了。因此,这个参数是使用阴影时需要仔细调整的参数。 + +除了上述位于 `Light` 组件当中的阴影配置外,还有一些有关阴影的全局配置位于 `Scene` 当中: + +image-20240319175051723 + +| 参数 | 应用 | +| :-- | :-- | +| [castShadows](/apis/core/#Scene-castShadows) | 是否投射阴影 | +| [shadowResolution](/apis/core/#Scene-shadowResolution) | 阴影的分辨率 | +| [shadowCascades](/apis/core/#Scene-shadowCascades) | 级联阴影的数量 | +| [shadowTwoCascadeSplits](/apis/core/#Scene-shadowTwoCascadeSplits) | 划分二级级联阴影的参数 | +| [shadowFourCascadeSplits](/apis/core/#Scene-shadowFourCascadeSplits) | 划分四级级联阴影的参数 | +| [shadowDistance](/apis/core/#Scene-shadowDistance) | 最大阴影距离 | +| [shadowFadeBorder](/apis/core/#Scene-shadowFadeBorder) | 阴影衰减比例,表示从阴影距离的多少比例开始衰减,范围为 [0~1],为 0 时表示没有衰减 | + +上述参数可以通过在 Playground 的例子中进行调试进行理解: + + + +目前引擎**只支持为一盏有向光 `DirectLight` 开启阴影**,这主要是因为阴影的渲染使得 DrawCall 翻倍,会严重影响渲染的性能。一般来说都会使用 `DirectLight` 模仿太阳光,所以才只支持一盏。对于有向光的阴影,有两点需要注意。 + +### 级联阴影 + +首先是级联阴影。由于有向光只是光照的方向,光源的位置没有什么意义。所以很难确定如何设置从光源出发的深度图绘制时使用的视锥体。且如果在整个场景中只渲染一次深度图,那么远处的物体很小,会严重浪费深度贴图,产生大量空白。所以引擎使用了稳定性级联阴影技术(CSSM): + +![shadow-cascade](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*R_ESQpQuP3wAAAAAAAAAAAAAARQnAQ) + +这种技术将相机的视锥体划分为两个或者四个块,然后沿着光照的方向渲染两次或者四次场景,通过划分参数确定每一个块的大小,由此尽可能提高深度贴图的利用率。引擎在开启阴影时会默认使用四级级联阴影,因此可以通过调整 shadowFourCascadeSplits 控制每一级的大小。 + +### 阴影的选择 + +上面提到**只支持为一盏有向光 `DirectLight` 开启阴影**,但如果给场景中的两盏 `DirectLight` 开启了阴影会发生什么呢?在没有确定主光的情况下,引擎会默认选择光强最强的那一盏灯投射阴影。光强由光照的 Intensity 和光照颜色的亮度共同决定,光照颜色是用 Hue-Saturation-Brightness 公式转换成去亮度值。 + +## 投射物与接受物 + +在光照中配置 enableShadow 只能控制深度图是否被渲染,还需要在 Renderer 当中对应选项,才能控制该物体是否投射阴影,或者是否接受其他物体的阴影。 + +| 参数 | 应用 | +| :--------------------------------------------------- | :------------------- | +| [receiveShadows](/apis/core/#Renderer-receiveShadows) | 该物体是否接受阴影 | +| [castShadows](/apis/core/#Renderer-castShadows) | 该物体是否会投射阴影 | + +开启 receiveShadows 的 Renderer,如果被其他物体遮挡则会渲染出阴影。开启 castShadows 的 Renderer,则会向其他物体投射阴影。 + +## 透明阴影 + +对于大多数需要阴影的场景,上述的控制参数基本够用了。但有时候我们希望在一个透明物体上投射阴影,例如场景中其实没有地面(比如 AR 的画面),但也希望物体能够拥有一个阴影,用以增强画面立体感。如果给地面设置标准的渲染材质,并且使得 alpha 设置为 0,那么地面上不会看到任何阴影。因为在真实世界中,光线会直接穿过透明物体。因此,对于透明地面这样的场景,需要一个特殊的材质进行渲染。可以参考 Playground 当中阴影绘制方式: + + + +在这一案例当中,背景其实只是一张贴图,但通过增加一个透明阴影,可以使得 3D 物体更加自然地融合到场景当中。 diff --git a/docs/graphics/light/spot.md b/docs/graphics/light/spot.md new file mode 100644 index 000000000..4e163c5af --- /dev/null +++ b/docs/graphics/light/spot.md @@ -0,0 +1,40 @@ +--- +order: 3 +title: 聚光灯 +type: 图形 +group: 光照 +label: Graphics/Light +--- + +**聚光灯**就像现实生活中的手电筒发出的光,从某个点朝特定方向锥形发射。 + +image-20240319174652884 + +聚光灯有几个主要特性:_颜色_([color](/apis/core/#SpotLight-color))、_强度_([intensity](/apis/core/#SpotLight-intensity))、_有效距离_([distance](/apis/core/#SpotLight-distance))、_散射角度_([angle](/apis/core/#SpotLight-angle))、_半影衰减角度_([penumbra](/apis/core/#SpotLight-penumbra))。散射角度表示与光源朝向夹角小于多少时有光线,半影衰减角度表示在有效的夹角范围内,随着夹角增大光照强度逐渐衰减至 0 。 + +| 属性 | 作用 | +| :--------------------- | :------------------------------------------------------------------------ | +| Angle(散射角度) | 表示与光源朝向夹角小于多少时有光线 | +| Intensity(强度) | 控制聚光灯的强度,**值越高越亮** | +| Color(颜色) | 控制聚光灯的颜色 | +| Distance(距离) | 有效距离,光照强度随距离衰减 | +| Penumbra(半影衰减角度) | 表示在有效的夹角范围内,随着夹角增大光照强度逐渐衰减至 0 | +| Culling Mask | 控制灯光需要照亮的物体,默认 Everything。 需要配合 Entity 的 Layer 来使用 | + +### 脚本使用 + +```typescript +const lightEntity = rootEntity.createChild("light"); + +const spotLight = lightEntity.addComponent(SpotLight); +// 散射角度 +spotLight.angle = Math.PI / 6; +// 半影衰减角度,为 0 时没有衰减 +spotLight.penumbra = Math.PI / 12; +// 颜色 +spotLight.color.set(0.3, 0.3, 1, 1); +// 位置 +lightEntity.transform.setPosition(-10, 10, 10); +// 朝向 +lightEntity.transform.setRotation(-45, -45, 0); +``` diff --git a/docs/graphics/material/composition.md b/docs/graphics/material/composition.md new file mode 100644 index 000000000..6a5e28a40 --- /dev/null +++ b/docs/graphics/material/composition.md @@ -0,0 +1,52 @@ +--- +order: 1 +title: 材质组成 +type: 材质 +group: 网格 +label: Graphics/Material +--- + +Galacean 材质包含 **[着色器(shader)](/docs/graphics-shader)、渲染状态(renderStates)、[着色器数据(shaderData)](/docs/graphics-shader-shaderData)**。着色器可以编写顶点、片元代码来决定渲染管线输出到屏幕上像素的颜色;渲染状态可以对渲染管线的上下文做一些额外配置;着色器数据封装了 CPU 传到 GPU 的一些数据集,比如颜色、矩阵、纹理等。 + +## 渲染状态 + +Galacean 将对渲染管线的配置封装在了 [RenderState 对象](/apis/core/#RenderState) 中,可以分别对[混合状态(BlendState)](/apis/core/#RenderState-BlendState)、[深度状态(DepthState)](/apis/core/#RenderState-DepthState)、[模版状态(StencilState)](/apis/core/#RenderState-StencilState)、[光栅状态(RasterState)](/apis/core/#RenderState-RasterState)进行配置。我们拿一个透明物体的标准渲染流程来举例,我们希望开启混合模式并设置混合因子,并且因为透明物体是叠加渲染的,所以我们还要关闭深度写入; + +```typescript +const renderState = material.renderState; + +// 1. 设置颜色混合因子。 +const blendState = renderState.blendState; +const target = blendState.targetBlendState; + +// src 混合因子为(As,As,As,As) +target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = BlendFactor.SourceAlpha; +// dst 混合因子为(1 - As,1 - As,1 - As,1 - As)。 +target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; +// 操作方式为 src + dst */ +target.colorBlendOperation = target.alphaBlendOperation = BlendOperation.Add; + +// 2. 开启颜色混合 +target.enabled = true; + +// 3. 关闭深度写入。 +const depthState = renderState.depthState; +depthState.writeEnabled = false; + +// 4. 设置透明渲染队列 +renderState.renderQueueType = RenderQueueType.Transparent; +``` + +> 有关渲染状态的更多选项可以分别查看相应的[API 文档](/apis/core/#RenderState)。 + +其中渲染队列可以决定这个材质在当前场景中的渲染顺序,引擎底层会对不同范围的渲染队列进行一些特殊处理,如 [RenderQueueType.Transparent](/apis/core/#RenderQueueType-transparent) 会从远到近进行渲染, [RenderQueueType.Opaque](/apis/core/#RenderQueueType-Opaque) 则会从近到远进行渲染。 + +```typescript +material.renderQueueType = RenderQueueType.Opaque; +``` + +针对相同的渲染队列,我们还可以设置 [Renderer](/apis/core/#Renderer) 的 `priority` 属性来强制决定渲染顺序,默认为 0,数字越大越后面渲染,如: + +```typescript +renderer.priority = -1; // 优先渲染 +``` diff --git a/docs/graphics/material/editor.md b/docs/graphics/material/editor.md new file mode 100644 index 000000000..f0f683c73 --- /dev/null +++ b/docs/graphics/material/editor.md @@ -0,0 +1,27 @@ +--- +order: 2 +title: 编辑器使用 +type: 材质 +group: 网格 +label: Graphics/Material +--- + +## 编辑器使用 + +### 1. 手动创建材质 + +image-20240206163405147 + +### 2. 导入模型 + +参考[模型的导入和使用](/docs/graphics-model-use)教程,我们可以先将模型导入到编辑器,一般情况下,模型已经自动绑定好材质,用户可以不用做任何操作;如果想要修改材质,我们需要点击 `duplicate & remap` 按钮来生成一份该材质的副本,然后再编辑该材质副本。 + +remap + +切换着色器时不会重置着色器数据,比如基础颜色为红色,那么即使切换着色器,基础颜色仍为红色。 + +image-20231009112713870 + +### 3. 调整材质 + +具体操作详见[着色器教程](/docs/graphics-shader)。 diff --git a/docs/graphics/material/material.md b/docs/graphics/material/material.md new file mode 100644 index 000000000..f011d6233 --- /dev/null +++ b/docs/graphics/material/material.md @@ -0,0 +1,17 @@ +--- +order: 0 +title: 材质总览 +type: 材质 +group: 网格 +label: Graphics/Material +--- + +材质是指用于描述物体外观和表面特性的属性集合。材质决定了模型在渲染过程中如何与光线交互,从而影响其视觉呈现效果。 + +image-20240206153815596 + +本节包含以下相关信息: + +- [材质组成](/docs/graphics-material-composition) +- [编辑器使用](/docs/graphics-material-editor) +- [脚本使用](/docs/graphics-material-script) diff --git a/docs/graphics/material/script.md b/docs/graphics/material/script.md new file mode 100644 index 000000000..2a556aa20 --- /dev/null +++ b/docs/graphics/material/script.md @@ -0,0 +1,83 @@ +--- +order: 3 +title: 脚本使用 +type: 材质 +group: 网格 +label: Graphics/Material +--- + +编辑器导出的材质只有 [Material](/apis/core/#Material) 基础类,而通过代码可以创建引擎已经封装好的 [PBRMaterial](/apis/core/#PBRMaterial),[UnlitMaterial](/apis/core/#UnlitMaterial),[BlinnPhongMaterial](/apis/core/#BlinnPhongMaterial)。 + +## 获取材质 + +### 1. 从已有 renderer 中获取 + +```typescript +// 获取想要修改的 renderer +const renderer = entity.getComponent(MeshRenderer); + +// 或者获取所有 renderer +const renderers = []; +entity.getComponentsIncludeChildren(MeshRenderer, renderers); + +// 通过 `getMaterial` 获取当前 renderer 的第 i 个材质, 默认第 0 个。 +const material = renderer.getMaterial(); +``` + +### 2. 替换 renderer 中的材质 + +我们也可以直接替换材质类型,比如将模型重新赋予一个 PBR 材质: + +```typescript +// 获取想要修改的 renderer +const renderer = entity.getComponent(MeshRenderer); + +// 创建材质 +const material = new PBRMaterial(engine); + +// 通过 `setMaterial` 设置当前 renderer 的第 i 个材质, 默认第 0 个。 +const material = renderer.setMaterial(material); +``` + +### 3. 创建内置材质 + +```typescript +const pbrMaterial = new PBRMaterial(engine); +const bpMaterial = new BlinnPhongMaterial(engine); +const unlitMaterial = new UnlitMaterial(engine); +``` + +### 4. 创建自定义材质 + +```typescript +// Shader.create 的具体步骤参考着色器教程 +const customMaterial = new Material(engine, Shader.find("***")); +``` + +## 修改材质 + +### 1. 修改内置材质 + +```typescript +// 设置透明,引擎已经封装好对应渲染状态的设置 +pbrMaterial.isTransparent = true; +// 设置透明度 +pbrMaterial.baseColor.a = 0.5; +// 金属、粗糙度等其他配置 +pbrMaterial.metallic = 1; +pbrMaterial.baseTexture = **; +``` + +### 2. 修改自定义材质 + +```typescript +const shaderData = material.shaderData; +// 获取想要设置的着色器数据 +const baseColor = shaderData.setFloat("material_BaseColor"); + +// 修改着色器数据 +baseColor.a = 0.5; +shaderData.setTexture("material_BaseTexture", texture); +shaderData.enable("MATERIAL_HAS_BASETEXTURE"); +// 更多的着色器操作,详见着色器文档 +``` diff --git a/docs/graphics/mesh/bufferMesh.md b/docs/graphics/mesh/bufferMesh.md new file mode 100644 index 000000000..99a0c217d --- /dev/null +++ b/docs/graphics/mesh/bufferMesh.md @@ -0,0 +1,192 @@ +--- +order: 2 +title: Buffer Mesh +type: 图形 +group: 网格 +label: Graphics/Mesh +--- + +[BufferMesh](/apis/core/#BufferMesh) 可以自由操作顶点缓冲和索引缓冲数据,以及一些与几何体绘制相关的指令。具备高效、灵活、简洁等特点。开发者如果想高效灵活的实现自定义几何体就可以使用该类。 + +## 原理图 + +我们先概览一下 `BufferMesh`  的原理图 + +![image.png](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*piB3Q4501loAAAAAAAAAAAAAARQnAQ) + +`BufferMesh`  有三大核心元素分别是: + +| 名称 | 解释 | +| :---------------------------------------------------- | :----------------------------------------------------------------------- | +| [VertexBufferBinding](/apis/core/#VertexBufferBinding) | 顶点缓冲绑定,用于将顶点缓冲和顶点跨度(字节)打包。 | +| [VertexElement](/apis/core/#VertexElement) | 顶点元素,用于描述顶点语义、顶点偏移、顶点格式和顶点缓冲绑定索引等信息。 | +| [IndexBufferBinding](/apis/core/#IndexBufferBinding) | 索引缓冲绑定(可选),用于将索引缓冲和索引格式打包。 | + +其中 [IndexBufferBinding](/apis/core/#IndexBufferBinding)  为可选,也就是说必要核心元素只有两个,分别通过 [setVertexBufferBindings()](/apis/core/#BufferMesh-setVertexBufferBindings)  接口和 [setVertexElements()](/apis/core/#BufferMesh-setVertexElements)  接口设置。 最后一项就是通过 [addSubMesh](/apis/core/#BufferMesh-addSubMesh)  添加子 [SubMesh](/apis/core/#SubMesh),并设置顶点或索引绘制数量, [SubMesh](/apis/core/#SubMesh)  包含三个属性分别是起始绘制偏移、绘制数量、图元拓扑,并且开发者可以自行添加多个 [SubMesh](/apis/core/#SubMesh),每个子几何体均可对应独立的材质。 + +## 常用案例 + +这里列举几个 [MeshRenderer](/apis/core/#MeshRenderer) 和 [BufferMesh](/apis/core/#BufferMesh) 的常用使用场景,因为这个类的功能偏底层和灵活,所以这里给出了比较详细的代码。 + +### 交错顶点缓冲 + + + +常用方式,比如自定义 Mesh、Particle 等实现,具有显存紧凑,每帧 CPU 数据上传至 GPU 次数少等优势。这个案例的主要特点是多个 [VertexElement](/apis/core/#VertexElement)  对应一个 *VertexBuffer* ([Buffer](/apis/core/#Buffer)),仅使用一个 *VertexBuffer*  就可以将不同顶点元素与 Shader 关联。 + +```typescript +// add MeshRenderer component +const renderer = entity.addComponent(MeshRenderer); + +// create mesh +const mesh = new BufferMesh(engine); + +// create vertices. +const vertices = new ArrayBuffer(vertexByteCount); + +// create vertexBuffer and upload vertices. +const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices); + +// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16. +mesh.setVertexBufferBinding(vertexBuffer, 16); + +// add vertexElement to tell GPU how to read vertex from vertexBuffer. +mesh.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("COLOR", 12, VertexElementFormat.NormalizedUByte4, 0), +]); + +// add one subMesh and set how many vertex you want to render. +mesh.addSubMesh(0, vertexCount); + +// set mesh +renderer.mesh = mesh; +``` + +### 独立顶点缓冲 + + + +动态顶点 buffer 和静态顶点 buffer 混用时具有优势,比如 _position_ 为静态,但 _color_ 为动态,独立顶点缓冲可以仅更新颜色数据至 GPU。这个案例的主要特点是一个 [VertexElement](/apis/core/#VertexElement) 对应一个 _VertexBuffer_ ,可以分别调用 [Buffer](/apis/core/#Buffer)  对象的 [setData](/apis/core/#Buffer-setData)  方法独立更新数据。 + +```typescript +// add MeshRenderer component +const renderer = entity.addComponent(MeshRenderer); + +// create mesh +const mesh = new BufferMesh(engine); + +// create vertices. +const positions = new Float32Array(vertexCount); +const colors = new Uint8Array(vertexCount); + +// create vertexBuffer and upload vertices. +const positionBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + positions +); +const colorBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, colors); + +// bind vertexBuffer with stride,stride is every vertex byte length,so the value is 12. +mesh.setVertexBufferBindings([ + new VertexBufferBinding(positionBuffer, 12), + new VertexBufferBinding(colorBuffer, 4), +]); + +// add vertexElement to tell GPU how to read vertex from vertexBuffer. +mesh.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("COLOR", 0, VertexElementFormat.NormalizedUByte4, 1), +]); + +// add one subMesh and set how many vertex you want to render. +mesh.addSubMesh(0, vertexCount); + +// set mesh +renderer.mesh = mesh; +``` + +### Instance 渲染 + + + +GPU Instance 渲染是三维引擎的常用技术,比如可以把相同几何体形状的物体一次性渲染到不同的位置,可以大幅提升渲染性能。这个案例的主要特点是使用了 [VertexElement](/apis/core/#VertexElement) 的实例功能,其构造函数的最后一个参数表示实例步频(在缓冲中每前进一个顶点绘制的实例数量,非实例元素必须为 0),[BufferMesh](/apis/core/#BufferMesh)  的 [instanceCount](/apis/core/#BufferMesh-instanceCount)  表示实例数量。 + +```typescript +// add MeshRenderer component +const renderer = entity.addComponent(MeshRenderer); + +// create mesh +const mesh = new BufferMesh(engine); + +// create vertices. +const vertices = new ArrayBuffer( vertexByteLength ); + +// create instance data. +const instances = new Float32Array( instanceDataLength ); + +// create vertexBuffer and upload vertex data. +const vertexBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, vertices ); + +// create instance buffer and upload instance data. +const instanceBuffer = new Buffer( engine, BufferBindFlag.VertexBuffer, instances ); + +// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16. +mesh.setVertexBufferBindings([new VertexBufferBinding( vertexBuffer, 16 ), + new VertexBufferBinding( instanceBuffer, 12 )]); + +// add vertexElement to tell GPU how to read vertex from vertexBuffer. +mesh.setVertexElements([new VertexElement( "POSITION", 0, VertexElementFormat.Vector3, 0 ), + new VertexElement( "COLOR", 12, VertexElementFormat.NormalizedUByte4, 0 ), + new VertexElement( "INSTANCE_OFFSET", 0, VertexElementFormat.Vector3, 1 , 1 ), + new VertexElement( "INSTANCE_ROTATION", 12, VertexElementFormat.Vector3, 1 , 1 )]]); + +// add one sub mesh and set how many vertex you want to render, here is full vertexCount. +mesh.addSubMesh(0, vertexCount); + +// set mesh +renderer.mesh = mesh; +``` + +## 索引缓冲 + +使用索引缓冲可以复用顶点缓冲内的顶点,从而达到节省显存的目的。其使用方式很简单,就是在原基础上增加了索引缓冲对象,以下代码是在第一个 **交错顶点缓冲** 案例的基础上修改而来的 + +```typescript +// add MeshRenderer component +const renderer = entity.addComponent(MeshRenderer); + +// create mesh +const mesh = new BufferMesh(engine); + +// create vertices. +const vertices = new ArrayBuffer(vertexByteCount); + +// create indices. +const indices = new Uint16Array(indexCount); + +// create vertexBuffer and upload vertices. +const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices); + +// create indexBuffer and upload indices. +const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, indices); + +// bind vertexBuffer with stride, stride is every vertex byte length,so the value is 16. +mesh.setVertexBufferBinding(vertexBuffer, 16); + +// bind vertexBuffer with format. +mesh.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16); + +// add vertexElement to tell GPU how to read vertex from vertexBuffer. +mesh.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("COLOR", 12, VertexElementFormat.NormalizedUByte4, 0), +]); + +// add one subMesh and set how many vertex you want to render. +mesh.addSubMesh(0, vertexCount); + +// set mesh +renderer.mesh = mesh; +``` diff --git a/docs/graphics/mesh/mesh.md b/docs/graphics/mesh/mesh.md new file mode 100644 index 000000000..0771a3ccf --- /dev/null +++ b/docs/graphics/mesh/mesh.md @@ -0,0 +1,48 @@ +--- +order: 0 +title: 网格总览 +type: 图形 +group: 网格 +label: Graphics/Mesh +--- + +网格是[网格渲染器](/docs/graphics-renderer-meshRenderer)的数据对象,它描述了顶点的各种信息(位置,拓扑,顶点色,UV 等)。 + +## 网格资产 + +网格资产一般来源于: + +- 通过[导入模型](/docs/graphics-model-importGlTF),获取第三方工具创建的[模型内置网格资产](/docs/graphics-model-assets) +- 编辑器的[内置网格资产](/docs/graphics-mesh-primitiveMesh) +- 开发者自身[创建网格资产](/docs/graphics-mesh-primitiveMesh) + +## 使用 + +当需要为网格渲染器设置网格时,只需要选择对应的网格资产即可。 + +import + +相应的,在脚本中,网格的使用会更加自由,同时复杂度也会高一些,首先我们看下 + +| 类型 | 描述 | +| :----------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| [ModelMesh](/docs/graphics-mesh-modelMesh) | 封装了常用的设置顶点数据和索引数据的方法,非常简单易用。开发者若想要快速地去自定义几何体可以使用该类 | +| [BufferMesh](/docs/graphics-mesh-bufferMesh) | 可以自由操作顶点缓冲和索引缓冲数据,以及一些与几何体绘制相关的指令。具备高效、灵活、简洁等特点。开发者如果想高效灵活的实现自定义几何体就可以使用该类 | +| [内置几何体](/docs/graphics-mesh-primitiveMesh) | 本质上是预置的 ModelMesh , 包含常用的长方体,球体,平面,圆柱,圆环,圆柱与胶囊体。 | + +## 使用 + +在编辑器中,网格以网格资产的形式出现,我们可以通过 + +```typescript +const meshRenderer = entity.addComponent(MeshRenderer); +meshRenderer.mesh = new ModelMesh(engine); +// or +meshRenderer.mesh = new BufferMesh(engine); +``` + +## 常用几何体 + +自己构造几何体网格数据是一个比较痛苦的过程,因此 Galacean 内置了一些较为实用的几何体。 + +- [内置几何体](/docs/graphics-model):包含常用的长方体,球体,平面,圆柱,圆环,圆柱与胶囊体。 diff --git a/docs/graphics/mesh/modelMesh.md b/docs/graphics/mesh/modelMesh.md new file mode 100644 index 000000000..dd90f43b9 --- /dev/null +++ b/docs/graphics/mesh/modelMesh.md @@ -0,0 +1,211 @@ +--- +order: 1 +title: Model Mesh +type: 图形 +group: 网格 +label: Graphics/Mesh +--- + +[ModelMesh](/apis/core/#ModelMesh) 是一个用于描述几何体轮廓的网状渲染数据类,主要包含了顶点(位置、法线和 UV 等)、索引和混合形状等数据。不仅可以使用建模软件制作并导出 glTF 在引擎中解析还原,还可以方便的使用脚本直接写入数据创建。 + + + +## 代码示例 + +```typescript +const entity = rootEntity.createChild("mesh-example"); +const meshRenderer = entity.addComponent(MeshRenderer); + +const modelMesh = new ModelMesh(engine); + +// Set vertieces data +const positions = [ + new Vector3(-1.0, -1.0, 1.0), + new Vector3(1.0, -1.0, 1.0), + new Vector3(1.0, 1.0, 1.0), + new Vector3(1.0, 1.0, 1.0), + new Vector3(-1.0, 1.0, 1.0), + new Vector3(-1.0, -1.0, 1.0), +]; +modelMesh.setPositions(positions); + +// Add SubMesh +modelMesh.addSubMesh(0, 6); + +// Upload data +modelMesh.uploadData(false); + +meshRenderer.mesh = modelMesh; +meshRenderer.setMaterial(new UnlitMaterial(engine)); +``` + +## 详细介绍 + +`ModelMesh` 的使用分为以下几步: + +### **设置数据** + +`ModelMesh` 可以通过**高级数据**或**低级数据**设置顶点数据,也可以根据需求选择性设置,但需要注意位置是必要数据且需要最先设置。 + +#### 通过高级数据设置 + +可以直接通过设置 `position`, `normal` , `uv` 等**高级数据**生成 ModelMesh,然后调用 `uploadData` 方法统一上传数据至 GPU 完成应用。 + +```typescript +const positions = new Array(4); +positions[0] = new Vector3(-1, 1, 1); +positions[1] = new Vector3(1, 1, 1); +positions[2] = new Vector3(1, -1, 1); +positions[3] = new Vector3(-1, -1, 1); +const uvs = new Array(4); +uvs[0] = new Vector2(0, 0); +uvs[1] = new Vector2(1, 0); +uvs[2] = new Vector2(1, 1); +uvs[3] = new Vector2(0, 1); + +modelMesh.setPositions(positions); +modelMesh.setUVs(uvs); +modelMesh.uploadData(false); +``` + +设置高级数据的 API 有: + +| API | 说明 | +| ----------------------------------------------------- | ---------------------- | +| [setPositions](/apis/core/#ModelMesh-setPositions) | 设置顶点坐标 | +| [setIndices](/apis/core/#ModelMesh-setIndices) | 设置索引数据 | +| [setNormals](/apis/core/#ModelMesh-setNormals) | 设置逐顶点法线数据 | +| [setColors](/apis/core/#ModelMesh-setColors) | 设置逐顶点颜色数据 | +| [setTangents](/apis/core/#ModelMesh-setTangents) | 设置逐顶点切线 | +| [setBoneWeights](/apis/core/#ModelMesh-setBoneWeights) | 设置逐顶点骨骼权重 | +| [setBoneIndices](/apis/core/#ModelMesh-setBoneIndices) | 设置逐顶点骨骼索引数据 | +| [setUVs](/apis/core/#ModelMesh-setUVs) | 设置逐顶点 uv 数据 | + +#### 通过低级数据设置 + +相比于高级数据,通过低级接口设置数据可以自由操作顶点缓冲数据,不仅灵活还可能带来性能提升。但需要理解 Vertex Buffer 和 Vertex Element 之间的关系,如下图: + +![image.png](https://mdn.alipayobjects.com/huamei_jvf0dp/afts/img/A*68IjSo2kwUAAAAAAAAAAAAAADleLAQ/original) + +```typescript +const pos = new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0]); +const posBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + pos, + BufferUsage.Static, + true +); +const mesh = new ModelMesh(engine); +mesh.setVertexBufferBinding(posBuffer, 12, 0); +const vertexElements = [ + new VertexElement( + VertexAttribute.Position, + 0, + VertexElementFormat.Vector3, + 0 + ), +]; +mesh.setVertexElements(vertexElements); +mesh.uploadData(false); +``` + +### **添加 SubMesh** + +[SubMesh](/apis/core/#SubMesh) 主要包含了绘制范围和绘制方式等信息。调用 [addSubMesh](/apis/core/#ModelMesh-addSubMesh)。 + +```typescript +modelMesh.addSubMesh(0, 2, MeshTopology.Triangles); +``` + +### **上传数据** + +调用 [uploadData()](/apis/core/#ModelMesh-uploadData) 方法。 + +如果不再需要修改 `ModelMesh` 数据,`releaseData` 参数设置为 `true`: + +```typescript +modelMesh.uploadData(true); +``` + +如果需要持续修改 `ModelMesh` 数据,`releaseData` 参数设置为 `false`: + +```typescript +modelMesh.uploadData(false); +``` + + + +### **读取高级数据** + +若要让 `ModelMesh` 中的顶点数据可读,需注意: + +- 在上传数据时将 `releaseData` 参数设置为 `false` +- 若顶点数据是通过**低级数据**设置的,低级数据的可读属性([readable](/apis/core/#Buffer-readable))需设置为 `true` + +```typescript +const pos = new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0]); +const posBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + pos, + BufferUsage.Static, + true +); +const mesh = new ModelMesh(engine); +mesh.setVertexBufferBinding(posBuffer, 12, 0); +const vertexElements = [ + new VertexElement( + VertexAttribute.Position, + 0, + VertexElementFormat.Vector3, + 0 + ), +]; +mesh.setVertexElements(vertexElements); +mesh.uploadData(false); +// 期望得到的高级数据 +const result = mesh.getPositions(); +``` + +## 脚本添加 BlendShape 动画 + +`BlendShape` 通常用于制作精细程度非常高的动画,比如表情动画等。其原理也比较简单,主要通过权重混合基础形状和目标形状的网格数据来表现形状之间过度的动画效果。 + +**glTF 导入 BlendShape 动画案例:** + + +**脚本自定义 BlendShape 动画案例:** + + +### 详细步骤 + +#### **组织`BlendShape`数据** + +首先我们先创建一个`BlendShape` 对象,然后调用 [addFrame()](/apis/core/#ModelMesh-addFrame)添加混合形状的帧数据,一个 `BlendShape` 可以添加多个关键帧,每一帧由**权重**和**几何体偏移数据**组成 其中**偏移位置**是必要数据,**偏移法线**和**偏移切线**为可选数据。 + +然后我们通过`Mesh`的`addBlendShape()` 方法添加创建好的`BlendShape`。 + +```typescript +// Add BlendShape +const deltaPositions = [ + new Vector3(0.0, 0.0, 0.0), + new Vector3(0.0, 0.0, 0.0), + new Vector3(-1.0, 0.0, 0.0), + new Vector3(-1.0, 0.0, 0.0), + new Vector3(1.0, 0.0, 0.0), + new Vector3(0.0, 0.0, 0.0), +]; +const blendShape = new BlendShape("BlendShapeA"); +blendShape.addFrame(1.0, deltaPositions); +modelMesh.addBlendShape(blendShape); +``` + +#### **通过权重调整至目标 `BlendShape`** + +现在我们要将网格的形状完全调整为刚才添加的`BlendShape`,我们需要设置一个权重数组,由于我们只添加了一个`BlendShape`,所以权重数组长度为 1 即可,并把第一个元素的值设置为 1.0。 + +```typescript +// Use `blendShapeWeights` property to adjust the mesh to the target BlendShape +skinnedMeshRenderer.blendShapeWeights = new Float32Array([1.0]); +``` diff --git a/docs/graphics/mesh/primitiveMesh.md b/docs/graphics/mesh/primitiveMesh.md new file mode 100644 index 000000000..f7ba72e90 --- /dev/null +++ b/docs/graphics/mesh/primitiveMesh.md @@ -0,0 +1,120 @@ +--- +order: 3 +title: Primitive Mesh +type: 图形 +group: 网格 +label: Graphics/Mesh +--- + +常用几何体统一在 [PrimitiveMesh](/apis/core/#PrimitiveMesh) 中提供。 + +## 编辑器使用 + +编辑器已经内置了`立方体`、`球`、`圆柱体` 等基础几何体,可以直接在左侧节点树点击 `+` 置入模型: + +image-20231009111916680 + +当然,我们也可以在组件面板点击 `1` 添加 `Mesh Renderer`组件,点击 `2` 绑定想要的基础几何体: + +image-20231009112014068 + +内置几何体无法满足需求?您可以在 **[资产面板](/docs/assets-interface)** 中 **右键** → **Create** → **PrimitiveMesh** 创建一个 `Mesh` 资产,并通过调整 `Mesh` 的各项参数来满足需求。 + +image-20231009111916680 + +## 脚本使用 + + + +目前提供的几何体如下: + +- [createCuboid](/apis/core/#PrimitiveMesh-createCuboid) **立方体** + +```typescript +const entity = rootEntity.createChild("cuboid"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createCuboid(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createSphere](/apis/core/#PrimitiveMesh-createSphere) **球体** + +```typescript +const entity = rootEntity.createChild("sphere"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createSphere(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createPlane](/apis/core/#PrimitiveMesh-createPlane) **平面** + +```typescript +const entity = rootEntity.createChild("plane"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createPlane(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createCylinder](/apis/core/#PrimitiveMesh-createCylinder) **圆柱** + +```typescript +const entity = rootEntity.createChild("cylinder"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createCylinder(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createTorus](/apis/core/#PrimitiveMesh-createTorus) **圆环** + +```typescript +const entity = rootEntity.createChild("torus"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createTorus(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createCone](/apis/core/#PrimitiveMesh-createCone) **圆锥** + +```typescript +const entity = rootEntity.createChild("cone"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createCone(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` + +- [createCapsule](/apis/core/#PrimitiveMesh-createCapsule) **胶囊体** + +```typescript +const entity = rootEntity.createChild("capsule"); +entity.transform.setPosition(0, 1, 0); +const renderer = entity.addComponent(MeshRenderer); +renderer.mesh = PrimitiveMesh.createCapsule(engine); +// Create material +const material = new BlinnPhongMaterial(engine); +material.emissiveColor.set(1, 1, 1, 1); +renderer.setMaterial(material); +``` diff --git a/docs/graphics/model/assets.md b/docs/graphics/model/assets.md new file mode 100644 index 000000000..341135ad4 --- /dev/null +++ b/docs/graphics/model/assets.md @@ -0,0 +1,61 @@ +--- +order: 3 +title: 模型资产 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +模型导入完毕后, **[资产面板](/docs/assets-interface)** 中会新增导入的模型资产,点击资产缩略图,可以看到这个模型的基本信息。 + +image-20231009112328575 + +| 区域 | 功能 | 解释 | +| :--------- | :--------------- | :----------------------------------------------------------------- | +| 视图区 | 预览 | 类似 glTF viewer,开发者可以方便地在不同角度观察模型不同动画的形态 | +| 基本信息 | URL | 模型的 CDN 链接 | +| | DrawCall | 绘制这个模型调用绘制的次数 | +| | ComputeTangents | 对模型顶点数据中切线信息的处理 | +| 材质重映射 | 模型中的材质列表 | 对应重映射的材质 | +| 导出 | Cut first frame | 是否裁剪第一帧 | +| | isGLB | 是否导出 GLB 格式 | +| | Export glb/glTF | 导出模型到本地 | + +## 模型的子资产 + +将鼠标悬停在模型资产缩略图上,点击右侧出现的三角按钮,模型资产包含的网格、贴图、动画、材质等子资产信息都会被展示在资源面板当中。 + +image-20231009112328575 + +### 网格子资产 + +点击网格子资产缩略图,可以看到网格的基本信息如下: + +image-20231009112328575 + +| 区域 | 功能 | 解释 | +| :------- | :----------- | :----------------------- | +| 顶点数据 | 顶点信息列表 | 顶点信息对应的格式与步长 | +| 子网格 | 子网格列表 | 子网格的绘制信息 | + +### 纹理子资产 + +纹理子资产的基本信息与[纹理](/docs/graphics-texture)资产唯一的区别是纹理信息基本都是只读的。 + +image-20231009112328575 + +### 材质子资产 + +同理,[材质](/docs/graphics-material)子资产也是如此: + +image-20231009112328575 + +一般情况下,用户不用对模型自带的材质做任何操作;但是在一定场景下,开发者可能想要手动微调材质,比如修改颜色,那么我们可以将原材质进行复制,即点击 **duplicate & remap**,然后就可以在原材质参数的基础上进行修改: + +image-20231009112328575 + +### 动画子资产 + +动画子资产以[动画片段](/docs/animation-clip)的形式出现在模型资产中,它也是**只读**的。 + +image-20231009112328575 diff --git a/docs/graphics/model/glTF.md b/docs/graphics/model/glTF.md new file mode 100644 index 000000000..c8dd57f53 --- /dev/null +++ b/docs/graphics/model/glTF.md @@ -0,0 +1,138 @@ +--- +order: 1 +title: glTF +type: 图形 +group: 模型 +label: Graphics/Model +--- + +> 更多详情可跳转 [glTF 官方网站](https://www.khronos.org/gltf/) + +**glTF**(GL Transmission Format)是 [khronos ](https://www.khronos.org/)发布的一种能高效传输和加载 3D 场景的规范,是 3D 领域中的 "JPEG" 格式,其功能涵盖了 FBX、OBJ 等传统模型格式,基本支持 3D 场景中的所有特性,其[插件机制](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos)也使用户可以灵活地自定义实现想要的功能。 + +## 生态 + +image-20231009112129853 + +## 导出产物 + +glTF 的导出产物一般分为两种: + +- **(.gltf + .bin + png)**:适合图片体积大的场景,所以将图片和模型拆分开来,可以异步加载模型和纹理 +- **(.glb)**:适合模型文件较大的场景,会将所有数据进行二进制保存,需要等所有数据解析完毕才能展示模型,Galacean 对这两种产物 + +以上两种产物在 Galacean 中都已支持,如何选择产物的导出类型可以按照项目实际情况决定。 + +## Galacean 对 glTF 的支持 + +**glTF2.0** 是目前 Galacean 推荐的首选 3D 场景传输格式,Galacean 对 **glTF2.0** 的核心功能和插件都做了很好的支持: + +- 支持 glTF 中的网格,材质和纹理信息,并将它编译为运行时的网格资产,材质资产与纹理资产。 +- 支持 glTF 中的动画(包含骨骼动画和 BlendShape ) +- 支持 glTF 中的节点信息(包含姿态信息),它们会被编译为运行时的 entity 对象,并保持原先的层级结构。 +- 支持 glTF 的相机,并将它编译为运行时的相机组件 +- 支持 glTF 的部分插件。 + +glTF 拥有非常多的特性,官网提供了大量的[示例](https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0)进行参考,Galacean 也提供了一份复刻版本进行快速浏览,可以通过以下 **glTF List** 切换不同的 glTF 模型。 + + + +### 插件支持 + +Galacean 目前支持以下 glTF 插件,若 glTF 文件中包含相应插件,则会自动加载相应功能: + +| 插件 | 功能 | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [KHR_draco_mesh_compression](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_draco_mesh_compression.ts) | 支持 Draco 压缩模型,节省显存 | +| [KHR_lights_punctual](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_lights_punctual.ts) | 支持多光源组合,会解析成引擎的光源,详见[光照教程](/docs/graphics-light) | +| [KHR_materials_pbrSpecularGlossiness](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_materials_pbrSpecularGlossiness.ts) | 支持 PBR [高光-光泽度工作流](/apis/core/#PBRSpecularMaterial) | +| [KHR_materials_unlit](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_materials_unlit.ts) | 支持 [Unlit 材质](/docs/graphics-shader-unlit) | +| [KHR_materials_variants](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_materials_variants.ts) | 允许渲染器存在多个材质,然后通过 [setMaterial](/apis/core/#Renderer-setMaterial) 接口进行材质切换 | +| [KHR_mesh_quantization](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_mesh_quantization.ts) | 支持[顶点数据压缩](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_mesh_quantization#extending-mesh-attributes),节省显存,如顶点数据一般都是浮点数,此插件可以保存为整型 | +| [KHR_texture_transform](https://github.com/oasis-engine/engine/blob/main/packages/loader/src/gltf/extensions/KHR_texture_transform.ts) | 支持纹理的缩放位移变换,可以参考 [TilingOffset](https://oasisengine.cn/#/examples/latest/tiling-offset) 案例 | +| [KHR_materials_clearcoat](https://github.com/ant-galaxy/oasis-engine/blob/main/packages/loader/src/gltf/extensions/KHR_materials_clearcoat.ts) | 支持材质的透明清漆度拓展,可以参考 [Clearcoat](https://oasisengine.cn/#/examples/latest/pbr-clearcoat) 案例 | +| [GALACEAN_materials_remap](https://github.com/ant-galaxy/oasis-engine/blob/main/packages/loader/src/gltf/extensions/GALACEAN_materials_remap.ts) | 支持编辑器材质映射 | + +### 插件拓展 + +如果官方内置的插件不能满足您的需求,我们还提供了拓展插件的方法。 + +举个例子,如果 Unity 导出了以下 glTF 插件,希望能根据材质拓展 `Unity_Material_Plugin` 生成新的自定义材质,然后根据灯光插件 `Unity_Light_Plugin` 表示想在某个节点上面加一个灯光: + +```json +{ + ... + materials:[{ + extensions:{ + Unity_Material_Plugin:{ + color: [1,1,1], + ... + } + } + }], + nodes:[{ + extensions:{ + Unity_Light_Plugin:{ + type:"point", + ... + } + } + }] + +} +``` + +#### 1. 自定义创建解析 + +按照上面的例子,我们注册一个材质插件,第二个参数 `GLTFExtensionMode.CreateAndParse` 表示这个插件是用来创建实例和解析的: + +```ts +@registerGLTFExtension("Unity_Material_Plugin", GLTFExtensionMode.CreateAndParse) +class UnityMaterialPluginParser extends GLTFExtensionParser { + createAndParse(context: GLTFParserContext, schema: {color,...other}}): Promise { + const { engine } = context.glTFResource; + const yourCustomMaterial = new Material(engine,customShader); + ... + return yourCustomMaterial; + } +} +``` + +#### 2. 增量解析 + +按照上面的例子,我们注册一个灯光插件,第二个参数 `GLTFExtensionMode.AdditiveParse` 表示这个插件是在原来实例的基础上进行一些增量解析的,比如在这个实体上添加一个光源: + +```ts +@registerGLTFExtension("Unity_Light_Plugin", GLTFExtensionMode.AdditiveParse) +class UnityLightPlugin extends GLTFExtensionParser { + additiveParse(context: GLTFParserContext, entity: Entity, extensionSchema: {type,...other}): void { + entity.addComponent(type==="point"?PointLight:DirectLight); + ... + } +} +``` + +#### 3. 自定义管线 + +如果上面的方法还不能满足您的需求,还可以完全自定义解析管线,用来重写解析的逻辑: + +```ts +@registerGLTFParser(GLTFParserType.Material) +class CustomMaterialParser extends GLTFParser{ + parse(context: GLTFParserContext, index: number): Promise { + const materialInfo = context.glTF.materials[index]; + ... + return materialPromise; + } +} + +engine.resourceManager + .load({ + type: AssetType.GLTF, + url: "https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf" + }) + .then((gltf) => { + const entity = rootEntity.createChild(); + entity.addChild(gltf.defaultSceneRoot); + }) +``` diff --git a/docs/graphics/model/importGlTF.md b/docs/graphics/model/importGlTF.md new file mode 100644 index 000000000..653364336 --- /dev/null +++ b/docs/graphics/model/importGlTF.md @@ -0,0 +1,38 @@ +--- +order: 2 +title: 导入模型 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +> 模型使用 [Blender](https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html) 等建模软件导出 FBX 或 glTF 格式,也可从 [Sketchfab](https://sketchfab.com/) 等模型网站下载。 + +准备好模型后,就可以将模型导入到 Galacean 编辑器中进行编辑了,你可以通过以下文件格式导入模型: + +- **(.gltf + .bin + 图片)** +- **(.glb + 图片)** +- **(.fbx)** + +需要注意的是,编辑器会将 FBX 转换成运行时也可以解析的[ glTF 格式](/docs/graphics-model-glTF)。接下来,让我们实操一下如何将模型文件导入编辑器。 + +## 拖拽导入 + +把模型文件,或者压缩成的 **.zip** 文件拖进资源面板: + +import + +## 按钮上传 + +点击右上角 **资源面板** -> **GLTF/GLB/FBX** + +image-20231009112129853 + +## 右键上传 + +依照 **资源面板** -> **右键** -> **Upload** -> **GLTF/GLB/FBX** + +image-20231009112129853 + +导入完毕后, **[资产面板](/docs/assets-interface)** 中就会新增导入的模型资产,让我们[看看模型资产包含了什么内容](/docs/graphics-model-assets)吧。 diff --git a/docs/graphics/model/model.md b/docs/graphics/model/model.md new file mode 100644 index 000000000..0ed2e70c3 --- /dev/null +++ b/docs/graphics/model/model.md @@ -0,0 +1,23 @@ +--- +order: 0 +title: 模型总览 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +模型通常指的是由设计师通过三维建模软件创建的,包含一系列[网格](/docs/graphics-mesh),[材质](/docs/graphics-material),[纹理](/docs/graphics-texture)和[动画](/docs/animation-overview)信息的三维模型,在 Galacean 中,它也被视作一种资产,模型资产工作流通常如下: + +```mermaid + flowchart LR + 建模软件导出模型 --> 导入模型到Galacean编辑器 --> 调整模型 +``` + +本章主要解答如下开发者可能遇到的问题: + +- 模型格式的要求,编辑器目前支持导入 `glTF` 或者 `FBX` 格式的模型,但是最后编辑器都会转换成运行时也可以解析的 [glTF](/docs/graphics-model-glTF) 格式。 +- [导入模型](/docs/graphics-model-importGlTF)到编辑器 +- 什么是[模型资产](/docs/graphics-model-assets) +- [模型的加载与使用](/docs/graphics-model-use) +- [在编辑器中还原美术效果](/docs/graphics-model-restoration) +- [模型优化](/docs/graphics-model-opt) diff --git a/docs/graphics/model/opt.md b/docs/graphics/model/opt.md new file mode 100644 index 000000000..7516ed5d5 --- /dev/null +++ b/docs/graphics/model/opt.md @@ -0,0 +1,24 @@ +--- +order: 6 +title: 模型优化 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +模型的优化一般从以下几点入手: + +- 网格:**缩减顶点数与面数**,**压缩网格数据** +- 纹理:**调整纹理尺寸**(如从 **1024 \* 1024** -> **512 \* 512**),使用**压缩纹理** +- 动画:**压缩动画数据** + +## 最佳实践 + +在编辑器中,我们可以通过以下方式对模型进行优化: + +1. 使用 [Quantize](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_mesh_quantization/README.md) 来压缩网格数据,在导出项目时勾选 GlTF Quantize 选项, 对网格进行量化压缩 +1. 使用 [Meshopt](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) 进一步压缩网格数据 + +image-20240228171935612 + +压缩可能会对模型网格精度造成一些影响,不过大部分情况下,肉眼很难区分。 diff --git a/docs/graphics/model/restoration.md b/docs/graphics/model/restoration.md new file mode 100644 index 000000000..a453e50e5 --- /dev/null +++ b/docs/graphics/model/restoration.md @@ -0,0 +1,137 @@ +--- +order: 5 +title: 在编辑器中还原美术效果 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/5dd84590-7c37-4156-bb1a-498207880c75/1635493348596-92184a82-6aaa-4ab8-95e5-2d88762df213.png) + +## 背景 + +Galacean 引擎目前有 3 种方式调试材质: + +1. 通过代码修改材质属性,参考[教程](/docs/graphics-material)。 + +2. 通过 Galacean Editor 可视化调试,参考[教程](/docs/graphics-material)。 + +3. **通过 3D 建模软件调好后导出 [glTF](/docs/graphics-model-glTF)** + +前两种方式直接使用引擎渲染,所见即所得,没有视觉上的差异。 + +但是设计师一般会使用第 3 种方式,即在 C4D、Blender 等建模软件中调好了视觉效果,然后导出到引擎中进行预览,发现渲染结果不一致,甚至有很大的偏差,主要原因在于: + +- **不同软件的渲染算法不同。** + +- **光照不一样。** + +- **部分资产无法导出到 glTF 文件。** + +针对造成差异的这些原因,可以通过以下方法来获取最大程度的视觉还原度: + +- **通过烘焙贴图,[导出 Unlit 材质到引擎](/docs/graphics-material-Unlit)** + +- **使用相同的环境贴图(一般为 HDRI 文件)、直接光照等变量。** + +- **在建模软件中只调试可以导出到 glTF 的属性和资产。** + +如果你也遇到了上述问题,可以先参考本教程,找到具体的原因,然后再参照相应的解决方法。如果还是无法解决问题,可以联系我们团队,我们会不断改进本教程。 + +## 原因 + +### 渲染算法差异 + +目前在实时渲染领域应用的最多的是 PBR 算法,拥有能量守恒、物理正确、易操作等优点,但是不同软件的具体实现算法是不一样的,使得渲染结果也不一样。Galacean 使用的是 **Cook-Torrance BRDF** 反射率方程,并针对移动端做了优化。 + +值得一提的是,虽然算法不同会造成一定的视觉差异,但是其物理规律还是一致的。比如,金属度越大,环境反射越强,漫反射越弱;粗糙度越大,环境反射越模糊,如下图: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/ddfe44e2-c9ab-4692-b62f-b43b8965ee4c/1635432936926-b26c9652-6d95-4160-9743-b954025dfe32.png) + +### 光照差异 + +跟现实世界一样,3D 场景也可以添加[直接光与环境光](/docs/graphics-light)。Galacean 场景中默认是**没有**光源的,只有一个偏向蓝色的[纯色漫反射](/apis/core/#AmbientLight-diffuseSolidColor),如下图左一;而很多建模软件中是自带光源的: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/391e9bd9-945d-474d-b3fb-8cb0490e2b6f/1635434650361-60d7f40f-9f22-4e48-8865-141415d638f9.png) + +环境光基于 [立方纹理](/docs/graphics-texture-cube) 开启 IBL 模式,需要绑定一张 HDRI 贴图用来模拟周边环境,可以从[网上下载](https://polyhaven.com/hdris)。Galacean 场景中默认是没有绑定 HDRI 贴图的,而很多建模软件是自带了一张比较好看的周边环境的: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/61c2287b-0793-4763-a5f5-70567fcdf106/1635477315862-08b0c680-029b-400b-8600-1d8cf7a20c60.png) + +### glTF 支持度差异 + +Galacean 引擎和建模软件的连通渠道是 [glTF 文件](/docs/graphics-model-glTF)。glTF 支持标准的 [PBR 属性](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-material-pbrmetallicroughness)和[通用材质属性](https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html#reference-material),并支持 [ClearCoat](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_clearcoat) 等插件,如下图。因此建模软件中的操作只要能导出到 glTF,引擎都能通过加载器加载,而那些额外的操作,比如 [vRay](https://www.chaosgroup.com/cn/vray/3ds-max) 材质的一些参数,是无法导出到 glTF 文件的。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/2010b748-ab8b-4e46-8b15-3aee4daa71f9/1635434775734-f8454efe-d268-4f80-87ab-40f1cddf96ea.png) + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/acd35018-dc09-404b-a735-85a981384df1/1635434736607-cc408f27-a7d7-4a30-a7ea-e083f209d2c9.png) + +## 解决方法 + +保证视觉还原度的首要前提是在同一个场景下调试材质,即相同的光照,相同的环境光等等,然后再选择实时渲染方案或者是烘焙方案。 + +### 统一光照 + +- 直接光 + +前面说到,引擎默认不带直接光,那么保持还原度最简单的方法,就是删除建模软件中的灯光,保证建模软件和 Galacean 引擎中都只有环境光(性能最好)。 + +image.png + +如果某些场景确实需要添加直接光,那么请保证建模软件可以导出 [glTF 灯光插件](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_lights_punctual) (Google 搜索关键词 "\***\* 建模软件 KHR_lights_punctual" ),比如 Blender 导出 glTF 的时候勾选上 **Punctual Lights。\*\* + +image.png + +如果建模软件不支持导出该光照插件,可以中转到 Blender 进行导出,或者和开发人员口头描述一下光照数据。 + +- 环境光 + +前面说到,引擎默认不带环境贴图,即 HDRI 贴图,但是建模软件一般都是自带的,比如 Blender: + +image.png + +我们可以先从[网上下载](https://polyhaven.com/hdris)喜欢的 HDRI 图片,然后在建模软件中进行调试,觉得满意后,将该最终 HDRI 交付给开发人员(因为 glTF 不支持导出 HDR)。 + +在建模软件中绑定环境贴图的方法很简单,可以 Google 搜索关键词 "\*\*\* 建模软件 environment IBL" ,拿 Blender 举例: + +image.png + +### 实时渲染方案 + +- 渲染方案 + +统一光照之后,我们就可以选择渲染方案了,如果你希望材质受到光照影响,能够实时光影交互,或者有一些透明、折射方面的需求,那么你应该选择实时渲染方案,即引擎的 PBR 方案。 + +- 调试材质 + +前面说到 Galacean PBR 使用的是 **Cook-Torrance BRDF** 反射率方程,在 Blender 中比较接近的是 Principled BSDF - GGX 算法: + +image.png + +可以通过 [Blender 官网教程](https://docs.blender.org/manual/en/2.80/addons/io_scene_gltf2.html#)参考如何调试可以导出到 glTF 的材质参数,其他建模软件同理,可以 Google 搜索关键词 “\*\*\* 建模软件 export glTF”。 + +还有一个比较简便的参考方式,就是在建模软件里面导入 glTF demo([点击下载](https://gw.alipayobjects.com/os/bmw-prod/85faf9f8-8030-45b2-8ba3-09a61b3db0c3.glb)),这个 demo 里面的 PBR 属性比较全面,可以参考着调试,比如 Blender 导入后,材质面板显示如下: + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/6643f12a-6226-490f-b853-f962a38cb09b/1635499476109-753aae7a-5ffa-4d52-ace1-4eaaef81919f.png) + +- 校验导出 + +导出 glTF 后,可以将文件拖拽到 [glTF 查看器](https://galacean.antgroup.com/#/gltf-viewer) 中,查看相应的颜色、纹理、参数等是否正确: + +image.png + +### 烘焙方案 + +不同于实时渲染,如果你的渲染场景完全静态,不需要光影交互,不需要要折射、透明等效果,那么使用烘焙方案会更加满足你的艺术创作,因为烘焙方案可以无视上文说的光照、glTF 支持度等问题;可以放心地使用建模软件的自带渲染器,[vRay](https://www.chaosgroup.com/cn/vray/3ds-max) 等强大插件,最后通过烘焙贴图,导出到 [glTF Unlit 插件](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Khronos/KHR_materials_unlit)。 + +我们针对烘焙方案也提供了几篇教程,你也可以通过 Google 搜索“\*\*\* 建模软件 烘焙 KHR_materials_unlit” 等关键词学习更多细节: + +- [《C4D 烘焙教程》](/docs/art-bake-c4d) + +- [《Blender 烘焙教程》](/docs/art-bake-blender) + +- [《导出 Unlit 材质》](/docs/graphics-material-Unlit) + +### Galacean 预览插件(规划中) + +我们后期还会投入插件开发人员,在各种建模软件中内置 Galacean 预览插件,保证所见即所得,省去 glTF 文件校验等步骤。 diff --git a/docs/graphics/model/use.md b/docs/graphics/model/use.md new file mode 100644 index 000000000..154510a19 --- /dev/null +++ b/docs/graphics/model/use.md @@ -0,0 +1,133 @@ +--- +order: 4 +title: 模型的加载与使用 +type: 图形 +group: 模型 +label: Graphics/Model +--- + +加载与使用模型资产,一般会遇到以下两种情况: + +- 已经随场景文件预加载的模型,在脚本中的使用 +- 未预加载的模型,在脚本中的加载与使用 + +在编辑器中,**放置在场景中的模型**都会随着场景文件被预先加载,依照步骤 **资产界面** -> **左键拖动模型缩略图** -> **拖动至[视图界面](/docs/interface-viewport)** -> **松开左键** -> **调整坐标** 即可将模型放置在对应场景中。 + +> 编辑器不能直接调整模型节点的 scale 属性, 所以通常情况下, 你需要把模型节点拖拽到一个 entity 节点下, 然后调整 entity 节点的 scale 属性。 + +import + +这种情况下,在运行时只需寻找场景中特定的节点即可获取对应的模型对象。 + +```typescript +// 根据节点名寻找模型节点 +const model1 = scene.findEntityByName("ModelName"); +// 根据节点路径寻找模型节点 +const model2 = scene.findEntityByPath("ModelPath"); +``` + +## 加载模型 + +只要有模型的 URL 信息,我们就可以很方便地加载这个模型。 + +```typescript +engine.resourceManager + .load({ url: "glTF's URL", type: AssetType.GLTF }) + .then((glTF: GLTFResource) => { + // 获取 glTF 模型实例化的模型对象 + const root = glTF.instantiateSceneRoot(); + // 将模型对象添加到场景中 + scene.addRootEntity(root); + }); +``` + +在编辑器中,可以直接获取模型资产的 URL ( **[资产面板](/docs/assets-interface)** -> **右键模型资产缩略图** -> **Copy file info / Copy relative path**): + +import + +没有导入编辑器的模型,对应的 URL 就是存放模型资产的路径。 + +## 加载进度 + +加载模型时也可以通过 [onProgress](/apis/core/#AssetPromise-onProgress) 事件来获取总任务/详细任务的加载进度。 + +```typescript +this.engine.resourceManager + .load(["b.gltf"]) + .onProgress( + (loaded, total) => { + console.log("task loaded:", loaded, "task total:", total); + }, + (url, loaded, total) => { + console.log("task detail:", url, "loaded:", loaded, "total:", total); + } +``` + +image-20240313112859472 + +## 使用模型 + +加载完毕的模型对象会返回包含了渲染信息和动画信息的根节点,它的使用和普通节点没有什么区别。 + + + +### 1. 选择场景根节点 + +glTF 可能包含多个场景根节点 `sceneRoots`,开发者可以手动选择希望实例化的根节点。 + +```typescript +engine.resourceManager + .load({ url: "glTF's URL", type: AssetType.GLTF }) + .then((glTF: GLTFResource) => { + // 选择根节点数组中下标为 1 的模型对象,默认下标为 0 + const root = glTF.instantiateSceneRoot(1); + // 将模型对象添加到场景中 + scene.addRootEntity(root); + }); +``` + +### 2. 播放动画 + +若模型携带了动画信息,可以从根节点上获取 [Animator](/apis/core/#Animator) 组件,然后选择播放任意动画片段。 + +```typescript +engine.resourceManager + .load({ url: "glTF's URL", type: AssetType.GLTF }) + .then((glTF: GLTFResource) => { + // 获取 glTF 模型实例化的模型对象 + const root = glTF.instantiateSceneRoot(); + // 将模型对象添加到场景中 + scene.addRootEntity(root); + // 获取 glTF 资产的动画信息 + const { animations } = glTF; + // 获取模型对象挂载的动画组件 + const animation = root.getComponent(Animator); + // 播放第一个动画 + animation.playAnimationClip(animations[0].name); + }); +``` + +### 3. 多材质切换 + +glTF [多材质插件](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_variants) 可以用来切换材质。 + +```typescript +engine.resourceManager + .load({ url: "glTF's URL", type: AssetType.GLTF }) + .then((glTF: GLTFResource) => { + // 获取 glTF 模型实例化的模型对象 + const root = glTF.instantiateSceneRoot(); + // 将模型对象添加到场景中 + scene.addRootEntity(root); + // 获取插件信息 + const { extensionsData } = glTF; + // 根据插件信息切换材质 + const variants: IGLTFExtensionVariants = extensionsData?.variants; + if (variants) { + const extensionData = extensionsData; + const replaceVariant = variants[0]; + const { renderer, material } = replaceVariant; + renderer.setMaterial(material); + } + }); +``` diff --git a/docs/graphics/particle/renderer-animation-module.md b/docs/graphics/particle/renderer-animation-module.md new file mode 100644 index 000000000..be0f00d09 --- /dev/null +++ b/docs/graphics/particle/renderer-animation-module.md @@ -0,0 +1,21 @@ +--- +order: 6 +title: 纹理表格动画模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[`TextureSheetAnimationModule`](${api}core/TextureSheetAnimationModule) 继承自 `ParticleGeneratorModule`,用于控制粒子系统的纹理表动画。 + +avatar + +## 属性 + +| 属性 | 释义 | +| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| [startFrame](${api}core/TextureSheetAnimationModule#startFrame) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示纹理表的起始帧 | +| [frameOverTime](${api}core/TextureSheetAnimationModule#frameOverTime) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示纹理表的帧随时间变化的曲线 | +| [type](${api}core/TextureSheetAnimationModule#type) | `TextureSheetAnimationType` 枚举,表示纹理表动画的类型 | +| [cycleCount](${api}core/TextureSheetAnimationModule#cycleCount) | `number` 类型,表示纹理表动画的周期计数 | +| [tiling](${api}core/TextureSheetAnimationModule#tiling) | `Vector2` 对象,表示纹理表的平铺。可以通过 `get` 和 `set` 方法访问和修改 | diff --git a/docs/graphics/particle/renderer-color-module.md b/docs/graphics/particle/renderer-color-module.md new file mode 100644 index 000000000..e1b1773c5 --- /dev/null +++ b/docs/graphics/particle/renderer-color-module.md @@ -0,0 +1,23 @@ +--- +order: 7 +title: 颜色模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[`ColorOverLifetimeModule`](${api}core/ColorOverLifetimeModule) 继承自 `ParticleGeneratorModule`,用于处理粒子系统的生命周期内的颜色变化。 + +avatar + +## 属性 + +| 属性 | 释义 | +| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| [color](${api}core/ColorOverLifetimeModule#color) | [ParticleCompositeGradient](${api}core/ParticleCompositeGradient) 对象,表示粒子在其生命周期内的颜色渐变 | + +## 渐变编辑 + +对于 [ParticleCompositeGradient](${api}core/ParticleCompositeGradient) 对象,编辑器内置了渐变编辑器。渐变条上方代表颜色 key,下方代表 alpha 值 key。每个 key 在渐变条的位置代表其时间。双击现有 key 可以新建 key,长按 key 并向下拖动可以删除 key。 + +avatar avatar diff --git a/docs/graphics/particle/renderer-emission-module.md b/docs/graphics/particle/renderer-emission-module.md new file mode 100644 index 000000000..b969e5ed6 --- /dev/null +++ b/docs/graphics/particle/renderer-emission-module.md @@ -0,0 +1,40 @@ +--- +order: 2 +title: 发射器模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[EmissionModule](${api}core/EmissionModule) 是 `ParticleGeneratorModule` 的发射模块。该模块用于处理粒子系统的发射行为,包括粒子发射速率、发射形状以及爆破(burst)行为等。 + +avatar + +## 属性 + +| 属性 | 释义 | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [rateOverTime](${api}core/EmissionModule#rateOverTime) | 这是一个 [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子的发射速率。默认值为 `10` | +| [rateOverDistance](${api}core/EmissionModule#rateOverDistance) | 这是一个 [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子的距离发射速率。默认值为 `0` | +| [shape](${api}core/EmissionModule#shape) | 这是一个 `BaseShape` 对象,表示发射器的形状 | + +## 方法 + +| 方法 | 释义 | +| --------------------------------------------------------------------------------- | ------------------------ | +| [addBurst(burst: Burst)](${api}core/EmissionModule#addBurst) | 添加一个爆破行为 | +| [removeBurst(burst: Burst)](${api}core/EmissionModule#removeBurst) | 移除一个爆破行为 | +| [removeBurstByIndex(index: number)](${api}core/EmissionModule#removeBurstByIndex) | 通过索引移除一个爆破行为 | +| [clearBurst()](${api}core/EmissionModule#clearBurst) | 清除所有的爆破行为 | + +## 形状 + +目前引擎内置了以下发射器形状,选中粒子组件时提供对应形状的辅助显示。 + +| 发射器形状类型 | 释义 | +| ------------------------------------------------------------ | ------------------------------------ | +| [BoxShape](${api}core/EmissionModule#BoxShape) | `BaseShape` 对象,发射器形状为立方体 | +| [CircleShape](${api}core/EmissionModule#CircleShape) | `BaseShape` 对象,发射器形状为圆圈 | +| [ConeShape](${api}core/EmissionModule#ConeShape) | `BaseShape` 对象,发射器形状为类圆锥 | +| [HemisphereShape](${api}core/EmissionModule#HemisphereShape) | `BaseShape` 对象,发射器形状为半球 | +| [SphereShape](${api}core/EmissionModule#SphereShape) | `BaseShape` 对象,发射器形状为球体 | diff --git a/docs/graphics/particle/renderer-main-module.md b/docs/graphics/particle/renderer-main-module.md new file mode 100644 index 000000000..60f48dd02 --- /dev/null +++ b/docs/graphics/particle/renderer-main-module.md @@ -0,0 +1,39 @@ +--- +order: 1 +title: 主模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[MainModule](${api}core/MainModule) 是 `ParticleGeneratorModule` 的主模块,包含了最基本的粒子生成参数。这些属性大多用于控制新创建的粒子的初始状态。 + +avatar + +## 属性 + +| 属性 | 释义 | +| -------------------------------------------------------- | ------------------------------------------------------- | +| [duration](${api}core/MainModule#duration) | 粒子生成器的持续时间(单位:秒) | +| [isLoop](${api}core/MainModule#isLoop) | 指定粒子生成器是否循环 | +| [startDelay](${api}core/MainModule#startDelay) | 粒子发射的开始延迟(单位:秒) | +| [startLifetime](${api}core/MainModule#startLifetime) | 粒子发射时的初始生命周期 | +| [startSpeed](${api}core/MainModule#startSpeed) | 粒子生成器首次生成粒子时的初始速度 | +| [startSize3D](${api}core/MainModule#startSize3D) | 是否以每个轴的粒子大小分别指定 | +| [startSize](${api}core/MainModule#startSize) | 粒子生成器首次生成粒子时的初始大小 | +| [startSizeX](${api}core/MainModule#startSizeX) | 粒子生成器首次生成粒子时沿 x 轴的初始大小 | +| [startSizeY](${api}core/MainModule#startSizeY) | 粒子生成器首次生成粒子时沿 y 轴的初始大小 | +| [startSizeZ](${api}core/MainModule#startSizeZ) | 粒子生成器首次生成粒子时沿 z 轴的初始大小 | +| [startRotation3D](${api}core/MainModule#startRotation3D) | 是否启用 3D 粒子旋转 | +| [startRotation](${api}core/MainModule#startRotation) | 粒子生成器首次生成粒子时的初始旋转 | +| [startRotationX](${api}core/MainModule#startRotationX) | 粒子发射时沿 x 轴的初始旋转 | +| [startRotationY](${api}core/MainModule#startRotationY) | 粒子发射时沿 y 轴的初始旋转 | +| [startRotationZ](${api}core/MainModule#startRotationZ) | 粒子发射时沿 z 轴的初始旋转 | +| [flipRotation](${api}core/MainModule#flipRotation) | 使部分粒子以相反方向旋转 | +| [startColor](${api}core/MainModule#startColor) | 粒子的初始颜色模式 | +| [gravityModifier](${api}core/MainModule#gravityModifier) | 此粒子生成器应用于由 Physics.gravity 定义的重力的比例 | +| [simulationSpace](${api}core/MainModule#simulationSpace) | 选择模拟粒子的空间,它可以是世界空间或本地空间 | +| [simulationSpeed](${api}core/MainModule#simulationSpeed) | 覆盖粒子生成器的默认播放速度 | +| [scalingMode](${api}core/MainModule#scalingMode) | 控制粒子生成器如何将其 Transform 组件应用到它发射的粒子 | +| [playOnEnabled](${api}core/MainModule#playOnEnabled) | 如果设置为 true,粒子生成器将在启动时自动开始播放 | +| [maxParticles](${api}core/MainModule#maxParticles) | 最大粒子数 | diff --git a/docs/graphics/particle/renderer-rotation-module.md b/docs/graphics/particle/renderer-rotation-module.md new file mode 100644 index 000000000..303fb687e --- /dev/null +++ b/docs/graphics/particle/renderer-rotation-module.md @@ -0,0 +1,20 @@ +--- +order: 4 +title: 生命周期旋转模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[`RotationOverLifetimeModule`](${api}core/RotationOverLifetimeModule) 继承自 `ParticleGeneratorModule`,用于控制粒子系统的生命周期内的旋转变化。 + +avatar + +## 属性 + +| 属性 | 释义 | +| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| [separateAxes](${api}core/RotationOverLifetimeModule#separateAxes) | `boolean` 类型,表示是否在每个轴上分别进行旋转。如果禁用,将只使用 z 轴 | +| [rotationX](${api}core/RotationOverLifetimeModule#rotationX) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 x 轴旋转 | +| [rotationY](${api}core/RotationOverLifetimeModule#rotationY) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 y 轴旋转 | +| [rotationZ](${api}core/RotationOverLifetimeModule#rotationZ) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 z 轴旋转 | diff --git a/docs/graphics/particle/renderer-size-module.md b/docs/graphics/particle/renderer-size-module.md new file mode 100644 index 000000000..f6119381f --- /dev/null +++ b/docs/graphics/particle/renderer-size-module.md @@ -0,0 +1,41 @@ +--- +order: 3 +title: 生命周期尺寸模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +[`SizeOverLifetimeModule`](${api}core/SizeOverLifetimeModule) 是 `ParticleGeneratorModule` 的子类,用于处理粒子系统的生命周期内的大小变化。 + +avatar + +## 属性 + +| 属性 | 释义 | +| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | +| [separateAxes](${api}core/SizeOverLifetimeModule#separateAxes) | 布尔值,指定每个轴的大小是否独立变化 | +| [sizeX](${api}core/SizeOverLifetimeModule#sizeX) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示 x 轴方向上粒子的大小变化曲线 | +| [sizeY](${api}core/SizeOverLifetimeModule#sizeY) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示 y 轴方向上粒子的大小变化曲线 | +| [sizeZ](${api}core/SizeOverLifetimeModule#sizeZ) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示 z 轴方向上粒子的大小变化曲线 | +| [size](${api}core/SizeOverLifetimeModule#size) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,获取或设置粒子的大小变化曲线 | + +## 折线编辑 + +针对[ ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,在编辑器内置了折线编辑器,可视化调整曲线。 + +avatar + +或者在代码中: + +```ts +sizeOverLifetime.enabled = true; +sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + +const curve = sizeOverLifetime.size.curve; +const keys = curve.keys; +keys[0].value = 0.153; +keys[1].value = 1.0; +curve.addKey(0.057, 0.37); +curve.addKey(0.728, 0.958); +``` diff --git a/docs/graphics/particle/renderer-velocity-module.md b/docs/graphics/particle/renderer-velocity-module.md new file mode 100644 index 000000000..69e6a2a33 --- /dev/null +++ b/docs/graphics/particle/renderer-velocity-module.md @@ -0,0 +1,22 @@ +--- +order: 5 +title: 生命周期速度模块 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +### 生命周期速度模块 + +[`VelocityOverLifetimeModule`](${api}core/VelocityOverLifetimeModule) 继承自 `ParticleGeneratorModule`,用于控制粒子系统的生命周期内的速度变化。 + +avatar + +## 属性 + +| 属性 | 释义 | +| ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| [space](${api}core/VelocityOverLifetimeModule#velocityZ) | 选择速度变化的空间,可以是世界空间或本地空间 | +| [velocityX](${api}core/VelocityOverLifetimeModule#velocityX) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 x 轴旋转 | +| [velocityY](${api}core/VelocityOverLifetimeModule#velocityY) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 y 轴旋转 | +| [velocityZ](${api}core/VelocityOverLifetimeModule#velocityZ) | [ParticleCompositeCurve](${api}core/ParticleCompositeCurve) 对象,表示粒子在其生命周期内的 z 轴旋转 | diff --git a/docs/graphics/particle/renderer.md b/docs/graphics/particle/renderer.md new file mode 100644 index 000000000..8cb48887a --- /dev/null +++ b/docs/graphics/particle/renderer.md @@ -0,0 +1,95 @@ +--- +order: 0 +title: 粒子渲染器 +type: 图形 +group: 粒子 +label: Graphics/Particle +--- + +Galacean Engine 的粒子渲染器 [ParticleRenderer](${api}core/ParticleRenderer) 是常用的渲染组件,具备丰富的属性,通过调节各个属性值达到绚丽多彩的粒子效果。 + +![avatar](https://mdn.alipayobjects.com/huamei_qbugvr/afts/img/A*CObVSaCKF_4AAAAAAAAAAAAADtKFAQ/original) + +## 粒子组件 + +粒子组件可以通过层级树面板上方的快捷方式,或检查器面板的添加组件挂载于场景中已激活的 Entity 上。 + +![avatar](https://mdn.alipayobjects.com/huamei_qbugvr/afts/img/A*fD8iTZUbiI4AAAAAAAAAAAAADtKFAQ/original) + +添加完毕后,可以在检查器面板查看粒子属性。视图窗口的左下角的粒子面板可以控制粒子效果的在视图窗口的播放。 + +![avatar](https://mdn.alipayobjects.com/huamei_qbugvr/afts/img/A*rwF_RLlHNt0AAAAAAAAAAAAADtKFAQ/original) + +您也可以在脚本中挂载粒子组件。 + +```ts +// 创建实体 +const entity = root.createChild("particleEntity"); +// 创建粒子组件 +let particleRenderer = particleEntity.addComponent(ParticleRenderer); +``` + +## 渲染材质 + +[ParticleMaterial](${api}core/ParticleMaterial) 是粒子的默认材质。 + +编辑器中通过 添加材质 - 选择粒子材质 创建。编辑完成后回到粒子观察器面板中选择该材质进行使用。 + +avatar + +或者在脚本中: + +```ts +// 添加粒子材质 +const material = new ParticleMaterial(engine); +particleRenderer.setMaterial(material); +``` + +| 属性 | 释义 | +| ---------------------------------------------------- | -------- | +| [baseColor](${api}core/ParticleMaterial#baseColor) | 基础颜色 | +| [baseTexture](${api}core/ParticleMaterial#baseColor) | 基础纹理 | + +## 播放控制 + +选中带有粒子组件的实体时出现的粒子面板允许您控制粒子效果在视图窗口的播放。 + +需要注意的是,在该面板上对粒子播放的调整,仅为视图窗口的预览服务,并不改变该粒子组件的属性。如果需要改变粒子的播放相关属性,需要在观察器面板调整。 + +avatar + +| 预览播放选项 | 释义 | +| --------------- | ------------------------------------------------------------------ | +| 重播(Restart) | 停止当前的粒子效果播放,并立即从头开始播放 | +| 停止(Stop) | 停止粒子效果的播放,并重置回初识状态 | +| 暂停(Pause) | 暂停选中实体及其子节点上的粒子效果 | +| 播放(Play) | 开始播放选中实体及其子节点上的粒子效果 | +| 速度(Speed) | 调整当前播放速度 | +| 预览(Preview) | 选择播放选中实体及其子节点上的粒子效果,或者播放场景中所有粒子效果 | + +或者在代码中, + +```ts +// 播放 +particleRenderer.generator.play(); +// 停止 +particleRenderer.generator.stop(); +// 调整播放速度 +particleRenderer.generator.main.simulationSpeed = 2; +``` + +## 粒子生成器 + +`ParticleRenderer` 的 [generator](${api}core/ParticleGenerator) 属性主要负责粒子的生成和播放功能,生成粒子相关的功能由多个模块组成,分别是主模块、发射器模块、生命周期尺寸模块、生命周期颜色模块、生命周期速度模块、生命周期旋转模块、纹理表格动画模块。在编辑器粒子观察器面板可以直观看到各个模块及分选项。 + +## 其他参数 + +avatar + +| 属性 | 释义 | +| ---------------------------------------------------------- | ---------------------------------------------------------------- | +| [velocityScale](${api}core/ParticleRenderer#velocityScale) | 指定粒子根据其速度伸展的程度 | +| [lengthScale](${api}core/ParticleRenderer#lengthScale) | 定义粒子在其运动方向上伸展的程度,定义为粒子的长度与其宽度的比例 | +| [pivot](${api}core/ParticleRenderer#pivot) | 粒子的枢轴 | +| [renderMode](${api}core/ParticleRenderer#renderMode) | 粒子的渲染模式 | +| [mesh](${api}core/ParticleRenderer#mesh) | 粒子的网格,当 `renderMode` 为 `Mesh` 时有效 | diff --git a/docs/graphics/renderer/meshRenderer.md b/docs/graphics/renderer/meshRenderer.md new file mode 100644 index 000000000..3ed5adcaf --- /dev/null +++ b/docs/graphics/renderer/meshRenderer.md @@ -0,0 +1,60 @@ +--- +order: 1 +title: 网格渲染器 +type: 图形 +group: 渲染器 +label: Graphics/Renderer +--- + +[MeshRenderer](/apis/core/#MeshRenderer) 是网格渲染组件,当一个实体挂载了网格渲染组件,只需设置它的 `mesh` 与 `material`即可渲染物体。 + + + +## 使用 + +在编辑器 **[层级面板](/docs/interface-hierarchy)** 中,你可以快速创建一个挂载了长方体网格渲染器的节点( **层级面板** -> **右键** -> **3D Object** -> **Cuboid** )。 + +image-20231007153753006 + +当然,也可以为场景中已有的节点挂载上网格渲染器,并设置任意[网格](/docs/graphics-mesh)与[材质](/docs/graphics-material)。( **选中节点** -> **[检查器面板](/docs/interface-inspector)** -> **Add Component** -> **Mesh Renderer** )。 + +image-20231007153753006 + +对应在脚本中使用如下所示: + +```typescript +const cubeEntity = rootEntity.createChild("cube"); +const cube = cubeEntity.addComponent(MeshRenderer); +cube.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); +cube.setMaterial(new BlinnPhongMaterial(engine)); +``` + +## 属性 + +在编辑器中,可以很方便地设置网格渲染器的属性。 + +image-20231007153753006 + +| 设置 | 解释 | +| :--------------- | :------------------------------------------------- | +| `material` | 待渲染物体的[材质](/docs/graphics-material)信息 | +| `mesh` | 待渲染物体的[网格](/docs/graphics-mesh)信息 | +| `receiveShadows` | 是否接收阴影 | +| `castShadows` | 是否投射阴影 | +| `priority` | 渲染器的渲染优先级,值越小渲染优先级越高,默认为 0 | + +相比于基础的[渲染器](/docs/graphics-renderer),网格渲染器还可设置是否支持顶点色(顶点色数据包含在网格的顶点信息中)。 + +| 属性 | 解释 | +| :------------------ | :------------- | +| `enableVertexColor` | 是否支持顶点色 | + +```typescript +const meshRenderer = entity.getComponent(MeshRenderer); +// 启用顶点色 +meshRenderer.enableVertexColor = true; +``` + +## 方法 + +网格渲染器并**没有新增**其他方法,但需要注意的是,很多情况下网格渲染器的网格内包含**若干子网格**,若希望每个子网格都对应**不同的材质**,可以在设置的时候就指定相应的**网格索引**,否则默认使用相同的材质。 diff --git a/docs/graphics/renderer/order.md b/docs/graphics/renderer/order.md new file mode 100644 index 000000000..cd099a411 --- /dev/null +++ b/docs/graphics/renderer/order.md @@ -0,0 +1,63 @@ +--- +order: 3 +title: 渲染排序 +type: 图形 +group: 渲染器 +label: Graphics/Renderer +--- + +渲染器的渲染顺序会影响渲染的**性能**和**准确性**,在 Galacean 中,对于每个相机,组件会按照统一的**判定规则**放置在对应的**渲染队列**中。 + +## 渲染队列 + +Galacean 共划分了三个渲染队列,按照渲染顺序依次为: + +- 非透明渲染队列(**Opaque**) +- 透明裁剪渲染队列(**AlphaTest**) +- 透明渲染队列(**Transparent**) + +而渲染器分配到哪个队列由渲染器材质**是否透明**与**透明裁剪的阈值**共同决定。 + +```mermaid +flowchart TD + A[渲染数据进入队列] --> B{是否透明} + B -->|是| C[透明渲染队列] + B -->|否| D{透明裁剪的阈值是否大于零} + D -->|是| E[透明裁剪渲染队列] + D -->|否| F[非透明渲染队列] +``` + +## 判定规则 + +Galacean 中对渲染顺序的判定规则如下: + +```mermaid +flowchart TD + A[渲染数据排序] --> B{渲染器优先级} + B -->|不相等| C[返回比较结果] + B -->|相等| D{是否来自相同渲染器} + D -->|不相同| E{渲染器组件包围盒与相机的距离} + D -->|相同| F[根据材质优先级返回比较结果] + E -->|不相等| G[返回比较结果] + E -->|想等| H[根据 ID 返回比较结果] +``` + +### 渲染器优先级 + +引擎为渲染器提供了 `priority` 属性用于修改渲染队列中的渲染顺序,默认值为 0 ,**priority 越小(可以为负数),渲染的优先级越高**。 + +### 材质优先级 + +引擎为材质提供了 `priority` 属性用于修改来自同一渲染器不同渲染数据在渲染队列中的渲染顺序,默认值为 0 ,**priority 越小(可以为负数),渲染的优先级越高**。 + +### 渲染器组件包围盒到相机的距离 + +渲染器组件包围盒到相机距离的计算方式取决于[相机](/docs/graphics-camera)的类型。在正交相机中,是渲染器包围盒中心点与摄像机沿着摄像机视图方向的距离,在透视相机中,是渲染器包围盒中心点与摄像机位置的直接距离。 + +到相机距离示意图 + +> 需要注意的是,不同渲染队列中,距离对渲染顺序的影响规则是不同的,在非透明渲染队列和透明裁剪渲染队列中中,渲染的顺序都是**由近到远**,而在透明渲染队列中,渲染的顺序则为**由远到近**。 + +### 稳定性 + +目前不同渲染器在`渲染器优先级`与`渲染器组件包围盒到相机的距离`都相同的情况下,Galacean 通过 **`renderer.instanceId`** 保证渲染排序的稳定性,但**相同渲染器**中无法保证渲染顺序的稳定。 diff --git a/docs/graphics/renderer/renderer.md b/docs/graphics/renderer/renderer.md new file mode 100644 index 000000000..416ec5f4f --- /dev/null +++ b/docs/graphics/renderer/renderer.md @@ -0,0 +1,61 @@ +--- +order: 0 +title: 渲染器总览 +type: 图形 +group: 渲染器 +label: Graphics/Renderer +--- + +渲染器是负责显示图形的[**组件**](/docs/core-component),他会依据不同的数据源展示对应的渲染效果。通过在节点上挂载渲染器,并设置对应的渲染数据,可以展现出各种复杂的三维场景。 + +## 渲染器类型 + +在 Galacean 中,内置了以下几种渲染器: + +- [网格渲染器](graphics-renderer-meshRenderer): 通过设置 `mesh` 与 `material` 即可渲染物体。 +- [蒙皮网格渲染器](graphics-renderer-skinnedMeshRenderer): 基于[网格渲染器](graphics-renderer-meshRenderer),额外包含了`骨骼动画`与 `Blend Shape` 的能力,使得物体的动画效果更加自然。 +- [精灵渲染器](/docs/graphics-2d-spriteRenderer): 通过设置 `sprite` 与 `material` (默认内置精灵材质),可以在场景中展示 2D 图像。 +- [精灵遮罩渲染器](/docs/graphics-2d-spriteMask): 用于对精灵渲染器实现遮罩效果。 +- [文字渲染器](/docs/graphics-2d-text): 在场景中显示文本 +- [粒子渲染器](/docs/graphics-particle-renderer): 在场景中展示粒子效果。 + +通过[渲染排序](/docs/graphics-renderer-order)可以更深入地了解各种渲染器在引擎内的渲染顺序。 + +## 属性 + +`Renderer` 在 Galacean 中是所有渲染器的基类,他包含了如下属性: + +| 属性 | 解释 | +| :--------------- | :------------------------------------------------- | +| `receiveShadows` | 是否接收阴影 | +| `castShadows` | 是否投射阴影 | +| `priority` | 渲染器的渲染优先级,值越小渲染优先级越高,默认为 0 | +| `shaderData` | 渲染依赖的数据,包含一些常量和宏开关 | +| `materialCount` | 渲染器包含的材质总数 | +| `bounds` | 渲染器世界包围盒 | +| `isCulled` | 渲染器在当前帧是否渲染 | + +您可以从任意派生自 `Renderer` 的渲染器内获取到这些属性。 + +```typescript +const renderer = cubeEntity.getComponent(Renderer); +renderer.castShadows = true; +renderer.receiveShadows = true; +renderer.priority = 1; +console.log("shaderData", renderer.shaderData); +console.log("materialCount", renderer.materialCount); +console.log("bounds", renderer.bounds); +console.log("isCulled", renderer.isCulled); +``` + +## 方法 + +`Renderer` 渲染器基类主要提供设置与获取材质相关的方法,需要注意的是,一个渲染器内可能包含多个材质,因此下列方法更像是在**操作材质数组的增删改查**。 + +| 方法 | 解释 | +| :--------------------- | :----------------------- | +| `setMaterial` | 设置数组中某个材质 | +| `getMaterial` | 获取数组中某个材质 | +| `getMaterials` | 获取材质数组 | +| `getInstanceMaterial` | 获取数组中某个材质的副本 | +| `getInstanceMaterials` | 获取材质数组的副本 | diff --git a/docs/graphics/renderer/skinnedMeshRenderer.md b/docs/graphics/renderer/skinnedMeshRenderer.md new file mode 100644 index 000000000..d77636f74 --- /dev/null +++ b/docs/graphics/renderer/skinnedMeshRenderer.md @@ -0,0 +1,30 @@ +--- +order: 2 +title: 蒙皮网格渲染器 +type: 图形 +group: 渲染器 +label: Graphics/Renderer +--- + +蒙皮网格渲染器继承于[网格渲染器](/docs/graphics-renderer-meshRenderer),额外封装了`骨骼动画`与 `Blend Shape` 的能力,使得渲染物体的动画效果更自然逼真。 + +## 属性 + +蒙皮网格渲染器的属性基本上与`骨骼动画`和 `Blend Shape` 脱不了关系。 + +| 设置 | 解释 | +| :------------------ | :----------------------------- | +| `localBounds` | 蒙皮网格渲染器的局部包围盒 | +| `bones` | 蒙皮网格渲染器的所有骨骼节点 | +| `rootBone` | 蒙皮网格渲染器对应的根骨骼节点 | +| `blendShapeWeights` | BlendShapes 的混合权重 | + +美术工作流导出的模型中一般已经预先设置好了所有骨骼和 BlendShape 信息,开发者只需要结合[动画系统](/docs/animation-overview)播放指定的动画片段即可。 + +## 骨骼动画 + + + +## BlendShape + + diff --git a/docs/graphics/shader/blinnPhong.md b/docs/graphics/shader/blinnPhong.md new file mode 100644 index 000000000..2bb30d079 --- /dev/null +++ b/docs/graphics/shader/blinnPhong.md @@ -0,0 +1,32 @@ +--- +order: 3 +title: Blinn Phong +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +[BlinnPhongMaterial](/apis/core/#BlinnPhongMaterial) 虽然不是基于物理渲染,但是其高效的渲染算法和基本齐全的光学部分,流传至今仍可以适用很多的场景。 + + + +## 编辑器使用 + +blinn + +## 参数介绍 + +| 参数 | 应用 | +| :---------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +| [baseColor](/apis/core/#BlinnPhongMaterial-baseColor) | 基础颜色。 **基础颜色 \* 基础纹理 = 最后的基础颜色。** | +| [baseTexture](/apis/core/#BlinnPhongMaterial-baseTexture) | 基础纹理。搭配基础颜色使用,是个相乘的关系。 | +| [specularColor](/apis/core/#BlinnPhongMaterial-specularColor) | 镜面反射颜色。**镜面反射颜色 \* 镜面反射纹理 = 最后的镜面反射颜色。** | +| [specularTexture](/apis/core/#BlinnPhongMaterial-specularTexture) | 镜面反射纹理。搭配镜面反射颜色使用,是个相乘的关系。 | +| [normalTexture](/apis/core/#BlinnPhongMaterial-normalTexture) | 法线纹理。可以设置法线纹理 ,在视觉上造成一种凹凸感,还可以通过法线强度来控制凹凸程度。 | +| [normalIntensity ](/apis/core/#BlinnPhongMaterial-normalIntensity) | 法线强度。法线强度,用来控制凹凸程度。 | +| [emissiveColor](/apis/core/#BlinnPhongMaterial-emissiveColor) | 自发光颜色。**自发光颜色 \* 自发光纹理 = 最后的自发光颜色。即使没有光照也能渲染出颜色。** | +| [emissiveTexture](/apis/core/#BlinnPhongMaterial-emissiveTexture) | 自发光纹理。搭配自发光颜色使用,是个相乘的关系。 | +| [shininess](/apis/core/#BlinnPhongMaterial-shininess) | 镜面反射系数。值越大镜面反射效果越聚拢。 | +| [tilingOffset](/apis/core/#BlinnPhongMaterial-tilingOffset) | 纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移,参考 [案例](${examples}tiling-offset) | + +如果需要通过脚本使用材质,可以前往[材质的使用教程](/docs/graphics-material-script)。 diff --git a/docs/graphics/shader/custom.md b/docs/graphics/shader/custom.md new file mode 100644 index 000000000..70023fe60 --- /dev/null +++ b/docs/graphics/shader/custom.md @@ -0,0 +1,311 @@ +--- +order: 4 +title: 自定义着色器 +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +业务中可能有一些特殊的渲染需求,例如水流特效,这时候就需要通过**自定义着色器** (Shader)去实现。 + + + +## 创建着色器 + +[Shader 类](/apis/core/#Shader) 封装了顶点着色器、片元着色器、着色器预编译、平台精度、平台差异性。他的创建和使用非常方便,用户只需要关注 shader 算法本身,而不用纠结于使用什么精度,亦或是使用 GLSL 哪个版本的写法。下面是一个简单的 demo: + +```javascript +import { Material, Shader, Color } from "@galacean/engine"; + +//-- Shader 代码 +const vertexSource = ` + uniform mat4 renderer_MVPMat; + + attribute vec3 POSITION; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + } + `; + +const fragmentSource = ` + uniform vec4 u_color; + + void main() { + gl_FragColor = u_color; + } + `; + +// 创建自定义 shader(整个 runtime 只需要创建一次) +Shader.create("demo", vertexSource, fragmentSource); + +// 创建材质 +const material = new Material(engine, Shader.find("demo")); +``` + +`Shader.create()`用来将 shader 添加到引擎的缓存池子中,因此整个 runtime 只需要创建一次,接下来就可以通过 [Shader.find(name)](/apis/core/#Shader-find) 来反复使用. + +> 注:引擎已经预先 create 了 blinn-phong、pbr、shadow-map、shadow、skybox、framebuffer-picker-color、trail。用户可以直接使用这些内置 shader,并且不能重名创建。 + +在上述的案例中,因为我们没有上传 `u_color` 变量,所以片元输出还是黑色的(uniform 默认值),接下来我们来介绍下引擎内置的 shader 变量以及如何上传自定义变量。 + +## 内置 shader 变量 + +在上面,我们给 material 赋予了 shader,这个时候程序已经可以开始渲染了。 + +> 需要注意的是,shader 代码中有两种变量,一种是**逐顶点**的 `attribute` 变量,另一种是**逐 shader** 的 `uniform` 变量。(在 GLSL300 后,统一为 in 变量) + +引擎已经自动上传了一些常用变量,用户可以直接在 shader 代码中使用,如顶点数据和 mvp 数据,下面是引擎默认上传的变量。 + +### 顶点输入 + +| 逐顶点数据 | attribute name | 数据类型 | +| :------------- | :------------- | :------- | +| 顶点 | POSITION | vec3 | +| 法线 | NORMAL | vec3 | +| 切线 | TANGENT | vec4 | +| 顶点颜色 | COLOR_0 | vec4 | +| 骨骼索引 | JOINTS_0 | vec4 | +| 骨骼权重 | WEIGHTS_0 | vec4 | +| 第一套纹理坐标 | TEXCOORD_0 | vec2 | +| 第二套纹理坐标 | TEXCOORD_1 | vec2 | + +### 属性 + +#### 渲染器 + +| 名字 | 类型 | 解释 | +| :----------------- | :--- | ------------------ | +| renderer_LocalMat | mat4 | 模型本地坐标系矩阵 | +| renderer_ModelMat | mat4 | 模型世界坐标系矩阵 | +| renderer_MVMat | mat4 | 模型视口矩阵 | +| renderer_MVPMat | mat4 | 模型视口投影矩阵 | +| renderer_NormalMat | mat4 | 法线矩阵 | + +#### 相机 + +| 名字 | 类型 | 解释 | +| :----------------------- | :-------- | ------------------------------------------------------------------ | +| camera_ViewMat | mat4 | 视口矩阵 | +| camera_ProjMat | mat4 | 投影矩阵 | +| camera_VPMat | mat4 | 视口投影矩阵 | +| camera_ViewInvMat | mat4 | 视口逆矩阵 | +| camera_Position | vec3 | 相机位置 | +| camera_DepthTexture | sampler2D | 深度信息纹理 | +| camera_DepthBufferParams | Vec4 | 相机深度缓冲参数:(x: 1.0 - far / near, y: far / near, z: 0, w: 0) | + +#### 时间 + +| 名字 | 类型 | 解释 | +| :---------------- | :--- | :-------------------------------------------------------- | +| scene_ElapsedTime | vec4 | 引擎启动后经过的总时间:(x: t, y: sin(t), z:cos(t), w: 0) | +| scene_DeltaTime | vec4 | 距离上一帧的间隔时间:(x: dt, y: 0, z:0, w: 0) | + +#### 雾 + +| 名字 | 类型 | 解释 | +| :-------------- | :--- | :-------------------------------------------------------------------------------------------- | +| scene_FogColor | vec4 | 雾的颜色 | +| scene_FogParams | vec4 | 雾的参数:(x: -1/(end-start), y: end/(end-start), z: density / ln(2), w: density / sqr(ln(2)) | + +## 上传着色器数据 + +> attribute 逐顶点数据的上传请参考 [网格渲染器](/docs/graphics-mesh-modelMesh),这里不再赘述。 + +除了内置的变量,我们可以在着色器中上传任何自定义名字的变量,我们唯一要做的就是根据着色器数据类型,使用正确的接口。上传的接口全部保存在 [ShaderData](/apis/core/#ShaderData) 中,而 shaderData 实例对象又分别保存在引擎的四大类 [Scene](/apis/core/#Scene)、[Camera](/apis/core/#Camera)、[Renderer](/apis/core/#Renderer)、[Material](/apis/core/#Material) 中,我们只需要分别往这些 shaderData 中调用接口,上传变量,引擎便会在底层自动帮我们组装这些数据,并进行判重等性能的优化。 + +![](https://mdn.alipayobjects.com/huamei_jvf0dp/afts/img/A*ijQMQJM_Vy0AAAAAAAAAAAAADleLAQ/original) + +### 着色器数据分开的好处 + +着色器数据分别保存在引擎的四大类 [Scene](/apis/core/#Scene)、[Camera](/apis/core/#Camera)、[Renderer](/apis/core/#Renderer)、[Material](/apis/core/#Material) 中,这样做的好处之一就是底层可以根据上传时机上传某一块 uniform,提升性能;另外,将材质无关的着色器数据剥离出来,可以实现共享材质,比如两个 renderer ,共享了一个材质,虽然都要操控同一个 shader,但是因为这一部分 shader 数据的上传来源于两个 renderer 的 shaderData,所以是不会影响彼此的渲染结果的。 + +如: + +```typescript +const renderer1ShaderData = renderer1.shaderData; +const renderer2ShaderData = renderer2.shaderData; +const materialShaderData = material.shaderData; + +materialShaderData.setColor("material_color", new Color(1, 0, 0, 1)); +renderer1ShaderData.setFloat("u_progross", 0.5); +renderer2ShaderData.setFloat("u_progross", 0.8); +``` + +### 调用接口 + +着色器数据的类型和分别调用的 API 如下: + +| shader 类型 | ShaderData API | +| :----------------------------------------------------------------------------------------- | :---------------------------------- | +| `bool` 、 `int` | setInt( value: number ) | +| `float` | setFloat( value: number )` | +| `bvec2`、`ivec2`、`vec2` | setVector2( value:Vector2 ) | +| `bvec3`、`ivec3`、`vec3` | setVector3( value:Vector3 ) | +| `bvec4`、`ivec4`、`vec4` | setVector4( value:Vector4 ) | +| `mat4` | setMatrix( value:Matrix ) | +| `float[]` 、`vec2[]` 、`vec3[]`、 `vec4[]` 、`mat4[]` | setFloatArray( value:Float32Array ) | +| `bool[]`、 `int[]` 、`bvec2[]`、 `bvec3[]` 、`bvec4[]`、 `ivec2[]`、 `ivec3[]` 、`ivec4[]` | setIntArray( value:Int32Array ) | +| `sampler2D` 、 `samplerCube` | setTexture( value:Texture ) | +| `sampler2D[]` 、 `samplerCube[]` | setTextureArray( value:Texture[] ) | + +代码演示如下: + +```glsl +// shader + +uniform float u_float; +uniform int u_int; +uniform bool u_bool; +uniform vec2 u_vec2; +uniform vec3 u_vec3; +uniform vec4 u_vec4; +uniform mat4 u_matrix; +uniform int u_intArray[10]; +uniform float u_floatArray[10]; +uniform sampler2D u_sampler2D; +uniform samplerCube u_samplerCube; +uniform sampler2D u_samplerArray[2]; + +// GLSL 300: +// in float u_float; +// ... +``` + +```typescript +// shaderData 可以分别保存在 scene 、camera 、renderer、 material 中。 +const shaderData = material.shaderData; + +shaderData.setFloat("u_float", 1.5); +shaderData.setInt("u_int", 1); +shaderData.setInt("u_bool", 1); +shaderData.setVector2("u_vec2", new Vector2(1, 1)); +shaderData.setVector3("u_vec3", new Vector3(1, 1, 1)); +shaderData.setVector4("u_vec4", new Vector4(1, 1, 1, 1)); +shaderData.setMatrix("u_matrix", new Matrix()); +shaderData.setIntArray("u_intArray", new Int32Array(10)); +shaderData.setFloatArray("u_floatArray", new Float32Array(10)); +shaderData.setTexture("u_sampler2D", texture2D); +shaderData.setTexture("u_samplerCube", textureCube); +shaderData.setTextureArray("u_samplerArray", [texture2D, textureCube]); +``` + +> **注**:为了性能考虑,引擎暂不支持 结构体数组上传、数组单个元素上传。 + +### 宏开关 + +除了 uniform 变量之外,引擎将 shader 中的[宏定义](https://www.wikiwand.com/en/OpenGL_Shading_Language)也视为一种变量,因为宏定义的开启/关闭 将生成不同的着色器变种,也会影响渲染结果。 + +如 shader 中有这些宏相关的操作: + +```glsl +#ifdef DISCARD + discard; +#endif + +#ifdef LIGHT_COUNT + uniform vec4 u_color[ LIGHT_COUNT ]; +#endif +``` + +也是通过 [ShaderData](/apis/core/#Shader-enableMacro) 来操控宏变量: + +```typescript +// 开启宏开关 +shaderData.enableMacro("DISCARD"); +// 关闭宏开关 +shaderData.disableMacro("DISCARD"); + +// 开启变量宏 +shaderData.enableMacro("LIGHT_COUNT", "3"); + +// 切换变量宏。这里底层会自动 disable 上一个宏,即 “LIGHT_COUNT 3” +shaderData.enableMacro("LIGHT_COUNT", "2"); + +// 关闭变量宏 +shaderData.disableMacro("LIGHT_COUNT"); +``` + +## 封装自定义材质 + +这部分的内容是结合上文所有内容,给用户一个简单的封装示例,希望对您有所帮助: + +```typescript +import { + Material, + Shader, + Color, + Texture2D, + BlendFactor, + RenderQueueType, +} from "@galacean/engine"; + +//-- Shader 代码 +const vertexSource = ` + uniform mat4 renderer_MVPMat; + + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + varying vec2 v_uv; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_uv = TEXCOORD_0; + } + `; + +const fragmentSource = ` + uniform vec4 u_color; + varying vec2 v_uv; + + #ifdef TEXTURE + uniform sampler2D u_texture; + #endif + + void main() { + vec4 color = u_color; + + #ifdef TEXTURE + color *= texture2D(u_texture, v_uv); + #endif + + gl_FragColor = color; + + } + `; + +Shader.create("demo", vertexSource, fragmentSource); + +export class CustomMaterial extends Material { + set texture(value: Texture2D) { + if (value) { + this.shaderData.enableMacro("TEXTURE"); + this.shaderData.setTexture("u_texture", value); + } else { + this.shaderData.disableMacro("TEXTURE"); + } + } + + set color(val: Color) { + this.shaderData.setColor("u_color", val); + } + + // make it transparent + set transparent() { + const target = this.renderState.blendState.targetBlendState; + const depthState = this.renderState.depthState; + + target.enabled = true; + target.sourceColorBlendFactor = target.sourceAlphaBlendFactor = + BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = target.destinationAlphaBlendFactor = + BlendFactor.OneMinusSourceAlpha; + depthState.writeEnabled = false; + this.renderQueueType = RenderQueueType.Transparent; + } + + constructor(engine: Engine) { + super(engine, Shader.find("demo")); + } +} +``` diff --git a/docs/graphics/shader/lab.md b/docs/graphics/shader/lab.md new file mode 100644 index 000000000..ef0441eab --- /dev/null +++ b/docs/graphics/shader/lab.md @@ -0,0 +1,484 @@ +--- +order: 5 +title: Shader Lab +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +`ShaderLab` 是一个针对 Galacean 引擎打造的 Shader 包装语言,它允许开发人员使用熟悉的 [GLSL](https://www.khronos.org/files/opengles_shading_language.pdf) 语法编写自定义 Shader,同时提供了额外的高级抽象和管理特性以增强开发效率。通过 ShaderLab,开发者能够更便捷地定义材质属性、渲染配置和其他效果。尽管 ShaderLab 为着色器的编写引入了便利性,但它并不取代 GLSL,而是与之兼容。开发者可以在 ShaderLab 框架内编写原生 GLSL 代码块,享受两者的结合优势。ShaderLab 使用流程如下: + +```mermaid +flowchart LR + 创建着色器 --> 编辑shaderlab --> 调试shaderlab +``` + +以下是一个简单的 ShaderLab 使用示例,其中包含了两个 Shader。`normal` Shader 定义了一个只实现 MVP 转换的顶点着色器,并且通过 Uniform 变量指定了像素颜色的片元着色器。另外,`lines` Shader 是一个使用 ShaderLab 进行改造的 [shadertoy](https://www.shadertoy.com/view/DtXfDr) 示例。 + + + +## 创建着色器 + +#### 在编辑器中创建 + +编辑器中可以添加 3 种 ShaderLab 模板: 自定义、`PBR`、和 着色器片段 + + + +其中 **自定义** 和 **`PBR`** 是使用 ShaderLab 语法进行编写的着色器模板,**着色器片段** 则是为了方便代码段复用,ShaderLab 中可以如下使用 `include` 宏进行代码段引用,后续编译过程中会被自动扩展替换。使用方式详见语法标准模块。 + +#### 在脚本中创建 + +当前`ShaderLab`尚未集成到引擎 core 核心包中,需要在引擎初始化时传入新建的`ShaderLab`对象,否则引擎无法解析使用`ShaderLab`语法编写的 Shader。 + +1. `ShaderLab` 初始化 + +```ts +import { ShaderLab } from '@galacean/engine-shaderlab'; + +const shaderLab = new ShaderLab(); +// 使用ShaderLab初始化Engine +const engine = await WebGLEngine.create({ canvas: 'canvas', shaderLab }); +``` + +2. 创建 Shader + +```glsl +// 直接使用ShaderLab创建Shader +const shader = Shader.create(galaceanShaderCode); +``` + +## `ShaderLab`编写 + +### 在编辑器中编辑着色器 + +双击我们在上一步创建的着色器资产即可跳转到代码编辑页 + +> 未来版本会推出 Galacean VSCode 插件,该插件会为`ShaderLab`提供语法检测和自动补全功能以及代码同步功能,敬请期待 + + + +### 语法标准 + +`ShaderLab`语法骨架如下,每个模块语法和使用会在下文详细展开。 + +```glsl +Shader "ShaderName" { + ... + SubShader "SubShaderName" { + ... + Pass "PassName" { + ... + } + ... + } + ... +} +``` + +#### Shader + +```glsl +Shader "ShaderName" { + ... + // 全局变量区:变量声明,结构体声明,渲染状态声明,材质属性定义 + ... + SubShader "SubShaderName" { + ... + } + ... +} +``` + +ShaderLab 中的`Shader`是传统渲染管线中着色器程序和其他引擎渲染设置相关信息的集合封装,它允许在同一个`Shader`对象中定义多个着色器程序,并告诉 Galacean 在渲染过程中如何选择使用它们。`Shader` 对象具有嵌套的结构,包含 `SubShader` 和 `Pass` 子结构。 + +#### 材质属性定义 + +```glsl +// Uniform +EditorProperties +{ + material_BaseColor("Offset unit scale", Color) = (1,1,1,1); + ... + + Header("Emissive") + { + material_EmissiveColor("Emissive color", Color) = (1,1,1,1); + ... + } + ... +} + +// 宏 +EditorMacros +{ + [On] UV_OFFSET("UV Offset", Range(1,100)) = 10; + ... +} +``` + +此模块用于定义绑定该 Shader 的材质在编辑器 Inspector 面板中的 UI 展示。ShaderLab 材质属性对宏属性和其它 Uniform 属性使用`EditorProperties`和`EditorMacros`进行分开声明,其声明格式为: + +1. Uniform 属性 + + ```glsl + EditorProperties { + propertyName("label in Inspector", type) [= defaultValue]; + ... + [ Header("blockName") { + propertyName("label in Inspector", type) [= defaultValue]; + ... + } ] + } + ``` + + > 可以使用嵌套`Header`块对材质属性进行层级分类。 + + 支持的类型有 + + | Type | Example | + | :-: | :-- | + | Bool | propertyName("Property Description", Boolean) = true; | + | Int | propertyName("Property Description", Int) = 1;
propertyName("Property Description", Range(0,8)) = 1 | + | Float | propertyName("Property Description", FLoat) = 0.5;
propertyName("Property Description", Range(0.0, 1.0)) = 0.5; | + | Texture2D | propertyName("Property Description", Texture2D); | + | TextureCube | propertyName("Property Description", TextureCube); | + | Color | propertyName("Property Description", Color) = (0.25, 0.5, 0.5, 1); | + | Vector2 | propertyName("Property Description", Vector2) = (0.25, 0.5); | + | Vector3 | propertyName("Property Description", Vector3) = (0.25, 0.5, 0.5); | + | Vector4 | propertyName("Property Description", Vector4) = (0.25, 0.5, 0.5, 1.0); | + +2. 宏属性 + + ```glsl + EditorMacros { + [\[Off/On\]] propertyName("label in Inspector"[, type]) [= defaultValue]; + ... + [ Header("blockName") { + [\[Off/On\]] propertyName("label in Inspector"[, type]) [= defaultValue]; + ... + } ] + } + ``` + + 均包含开启和禁用功能,初始化通过 `[On/Off]` 指令指定,其类型包含 + + | Type | Example | + | :-: | :-- | + | 无(开关宏) | macroName("Macro Description"); | + | Bool | macroName("Macro Description", Boolean) = true; | + | Int | macroName("Macro Description", Int) = 1;
macroName("Macro Description", Range(0,8)) = 1; | + | Float | macroName("Macro Description", FLoat) = 0.5;
macroName("Macro Description", Range(0.0, 1.0)) = 0.5; | + | Color | macroName("Macro Description", Color) = (0.25, 0.5, 0.5, 1); | + | Vector2 | macroName("Macro Description", Vector2) = (0.25, 0.5); | + | Vector3 | macroName("Macro Description", Vector3) = (0.25, 0.5, 0.5); | + | Vector4 | macroName("Macro Description", Vector4) = (0.25, 0.5, 0.5, 1.0); | + +> 注意,当前版本 ShaderLab 材质属性模块只是定义了绑定该 Shader 的材质在编辑器中的 Inspector UI 面板,并不会替你在`ShaderPass`中声明对应的全局变量,如果`ShaderPass`代码中引用了该变量需在全局变量模块(见下文)中明确声明补充。 + +#### 全局变量 + +可以在 ShaderLab 中声明 4 类全局变量:渲染状态(RenderState),结构体,函数,以及单变量。 + +- 渲染状态 + + 包含混合状态(BlendState),深度状态(DepthState),模板状态(StencilState),光栅化状态(RasterState) + + - BlendState + + ```glsl + BlendState { + Enabled[n]: bool; + ColorBlendOperation[n]: BlendOperation; + AlphaBlendOperation[n]: BlendOperation; + SourceColorBlendFactor[n]: BlendFactor; + SourceAlphaBlendFactor[n]: BlendFactor; + DestinationColorBlendFactor[n]: BlendFactor; + DestinationAlphaBlendFactor[n]: BlendFactor; + ColorWriteMask[n]: float // 0xffffffff + BlendColor: vec4; + AlphaToCoverage: bool; + } + ``` + + [n] 可省略,在使用 MRT 的情况下, [n] 为指定某个 MRT 渲染状态,省略为设置所有 MRT 状态,BlendOperation 和 BlendFactor 枚举等同引擎 API + + - DepthState + + ```glsl + DepthState { + Enabled: bool; + WriteEnabled: bool; + CompareFunction: CompareFunction; + } + ``` + + CompareFunction 枚举等同引擎 API + + - StencilState + + ```glsl + StencilState { + Enabled: bool; + ReferenceValue: int; + Mask: float; // 0xffffffff + WriteMask: float; // 0xffffffff + CompareFunctionFront: CompareFunction; + CompareFunctionBack: CompareFunction; + PassOperationFront: StencilOperation; + PassOperationBack: StencilOperation; + FailOperationFront: StencilOperation; + FailOperationBack: StencilOperation; + ZFailOperationFront: StencilOperation; + ZFailOperationBack: StencilOperation; + } + ``` + + CompareFunction 和 StencilOperation 举等同引擎 API + + - RasterState + + ```glsl + RasterState { + CullMode: CullMode; + DepthBias: float; + SlopeScaledDepthBias: float; + } + ``` + + CullMode 举等同引擎 API + + 在`ShaderLab`中设置`BlendState`示例: + + ```glsl + Shader "Demo" { + ... + BlendState customBlendState + { + Enabled = true; + // 常量复制方式 + SourceColorBlendFactor = BlendFactor.SourceColor; + // 变量赋值方式 + DestinationColorBlendFactor = material_DstBlend; + } + ... + Pass "0" { + ... + BlendState = customBlendState; + ... + } + } + ``` + + 上述案例中对于 BlendState 属性赋值展示了 2 种方式: *常量赋值*和*变量赋值*方式: + + - 常量赋值指赋值语句右端为指定的对应引擎枚举变量,譬如:BlendFactor.SourceColor + - 变量赋值指赋值语句右端为任一变量名,变量具体值由用户通过脚本方式在运行时通过 ShaderData.setInt("material_DstBlend", BlendFactor.SourceColor) API 进行指定 + +- 结构体、函数 + + 等同 glsl 中的语法 + +- 单变量 + + ```glsl + [lowp/mediump/highp] variableType variableName; + ``` + +与其他编程语言类似,ShaderLab 中的全局变量也有作用域和同名覆盖原则。简单来说,ShaderLab 中的全局变量的作用范围仅限于其声明的 SubShader 或 Pass 模块内部,而同名覆盖原则指的是如果存在与 Pass 内部同名的全局变量,则 Pass 内的全局变量会覆盖 SubShader 内的同名全局变量。 + +#### SubShader + +```glsl +SubShader "SubShaderName" { + ... + // 全局变量区:变量声明,结构体声明,渲染状态声明 + ... + Tags {ReplaceTag = "opaque"} + + UsePass "ShaderName/SubShaderName/PassName" + + Pass "PassName" { + ... + } +} +``` + +一个`Shader`对象可以包含多个,但至少一个`SubShader`。它表示一组渲染管线的具体实现,定义了一种渲染效果的多个实现步骤(Pass),当前`SubShader`可以通过自定义 Tag,如`ReplaceTag`,搭配 [`Camera.setReplacementShader`](/apis/core/#Camera) 指定可能需要替换的着色器程序。 + +- `UsePass` 指令 + + 如果一个 `SubShader` 包含多个 `Pass`,可以通过 `UsePass` 指令复用其他 `Pass` 对象,比如引擎内置的 PBR Pass: `UsePass "pbr/Default/Forward"` + + | 内置 Shader | Pass 路径 | + | :-------------: | :-----------------------------: | + | PBR | pbr/Default/Forward | + | Unlit | unlit/Default/Forward | + | Skybox | skybox/Default/Forward | + | Particle-shader | particle-shader/Default/Forward | + | SpriteMask | SpriteMask/Default/Forward | + | Sprite | Sprite/Default/Forward | + +#### Pass + +```glsl +Pass "PassName" { + Tag {PipelineStage = "ShadowCaster"} + + ... + // 全局变量区:公共变量声明,结构体声明,函数声明 + ... + + // 渲染管线和渲染状态设置 + + // 指定顶点着色器和片元着色器 强调glsl语言 + VertexShader = vert; + + // 指定渲染队列 + RenderQueueType = RenderQueueType.Transparent; +} +``` + +`Pass` 是 `Shader` 对象的基本元素。简单的着色器对象可能只包含一个 `Pass`,但更复杂的着色器可以包含多个 `Pass`。 它定义了渲染管线特定阶段执行的操作,例如在 GPU 上运行的着色器程序,渲染状态,以及渲染管线相关设置。 + +- 渲染状态指定 + + 可以通过以下两种方式指定 + + 1. 显示赋值 + + ``` + BlendState = blendState; + ``` + + 2. Pass 全局变量域中声明指定 + + ``` + BlendState blendState { + 渲染状态属性 = 属性值; + } + ``` + +- uniform 变量指定 + + 直接声明成全局变量 + + ```glsl + mediump vec4 u_color; + float material_AlphaCutoff; + mat4 renderer_ModelMat; + vec3 u_lightDir; + ``` + +- attribute 变量声明 + + 通过定义顶点着色器函数入参结构体指定 + + ```glsl + struct a2v { + vec4 POSITION; + } + + v2f vert(a2v o) { + ... + } + ``` + +- varying 变量声明 + + 通过定义顶点着色器出参结构体和片元着色器入参结构体指定 + + ```glsl + struct v2f { + vec3 color; + } + + v2f vert(a2v o) { + ... + } + void frag(v2f i) { + ... + } + ``` + +- 顶点、片元着色器指定 + + 通过`VertexShader`和`FragmentShader`指定显示指定着色器入口函数 + + ``` + VertexShader = vert; + FragmentShader = frag; + ``` + +- 渲染队列设置 + + 通过`RenderQueueType`指令指定,`RenderQueueType`等同与引擎 API。 + + ``` + RenderQueueType = RenderQueueType.Transparent; + ``` + +#### `include` 宏 + +为了方便代码段复用,ShaderLab 中可以如下使用 `include` 宏进行代码段引用,后续编译过程中会被自动扩展替换。 + +```glsl +#include "{includeKey}" +``` + +为了能使代码段可以通过 `include` 宏进行引用,我们有 2 种方式进行代码段声明: + +1. 编辑器中创建 着色器 / 着色器片段 + +创建的代码段 `includeKey` 为该文件在工程中的文件路径,比如 `/Root/Effect.glsl` + +2. 脚本中显示注册代码段 + +```ts +import { ShaderFactory } from '@galacean/engine'; + +const commonSource = `// shader chunk`; +ShaderFactory.registerInclude('includeKey', commonSource); +``` + +#### 当前不支持的 GLSL 语法格式 + +1. 浮点数小数点前后的 0 不能省略 + + - ❌ `float n = 1. + .9;` + - ✅ `float n = 1.0 + 0.9;` + +2. 变量赋值语句中当赋值为函数调用返回值的属性时,需要用括弧包含函数调用 + + - ❌ `float a3 = texture2D(u_texture, (p.xy * 0.4 + um) * u_water_scale).x;` + - ✅ `float a3 = (texture2D(u_texture, (p.xy * 0.4 + um) * u_water_scale)).x;` + +3. if / for 判断语句后如果只有一行代码,"{}"不能省略 + + - ❌ + ``` + if(dis < EPS || dis > MAX_DIS) + break; + ``` + - ✅ + ``` + if(dis < EPS || dis > MAX_DIS) { + break; + } + ``` + +## 材质绑定着色器 + +有了使用`ShaderLab`编写的自定义着色器资产后,我们可以通过将着色器绑定到新建的材质实现用户自定义材质。 + + + +- `ShaderLab`反射材质属性 + +如果我们在`ShaderLab`中编写了`材质属性定义`模块,模块中定义的属性会暴露在绑定该 Shader 的材质资产 Inspector 面板中 + + + +## 一个利用多 Pass 技术实现平面阴影的示例 + + diff --git a/docs/graphics/shader/pbr.md b/docs/graphics/shader/pbr.md new file mode 100644 index 000000000..833425521 --- /dev/null +++ b/docs/graphics/shader/pbr.md @@ -0,0 +1,72 @@ +--- +order: 1 +title: PBR +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +PBR 全称是 **Physically Based Rendering**,中文意思是**基于物理的渲染**,最早由迪士尼在 2012 年提出,后来被游戏界广泛使用。跟传统的 **Blinn-Phong** 等渲染方法相比,PBR 遵循能量守恒,符合物理规则,美术们只需要调整几个简单的参数,即使在复杂的场景中也能保证正确的渲染效果。PBR 遵循能量守恒,是基于物理的渲染,并且引入了 [IBL](/docs/graphics-light-ambient) 模拟全局光照,通过金属度、粗糙度等参数,更加方便地调节渲染效果。 + + + +## 编辑器使用 + +根据真实世界中光线与材质的交互,绝缘体(即当金属度为 0 时)材质也能反射大约 4% 纯色光线,从而渲染出周边环境,如下模型金属度为 0 但是还能隐约看到反射的周边环境: + +image-20231007153753006 + +我们调节材质的金属度,可以发现,金属度越大,周围的环境越清晰,并且开始从白色纯色变成彩色。这是因为电介质(即金属度为 1 时)材质会将光线 100% 全部反射出物体表面,即反射出彩色的周边环境: + +metal + +除此之外,还有很多通用属性可以配置,比如各向异性,粗糙度、环境遮蔽、自发射光、透明度等等: + +material-anisotropy + +other + +## 参数介绍 + +| 参数 | 应用 | +| :-- | :-- | +| [metallic](/apis/core/#PBRMaterial-metallic) | 金属度。模拟材质的金属程度,金属值越大,镜面反射越强,即能反射更多周边环境。 | +| [roughness](/apis/core/#PBRMaterial-roughness) | 粗糙度。模拟材质的粗糙程度,粗糙度越大,微表面越不平坦,镜面反射越模糊。 | +| [roughnessMetallicTexture](/apis/core/#PBRMaterial-roughnessMetallicTexture) | 金属粗糙度纹理。搭配金属粗糙度使用,是相乘的关系。 | +| [baseColor](/apis/core/#PBRBaseMaterial-baseColor) | 基础颜色。**基础颜色** \* **基础颜色纹理** = **最后的基础颜色**。基础颜色是物体的反照率值,与传统的漫反射颜色不同,它会同时贡献镜面反射和漫反射的颜色,我们可以通过上面提到过的金属度、粗糙度,来控制贡献比。 | +| [emissiveColor](/apis/core/#PBRBaseMaterial-emissiveColor) | 自发光颜色。使得即使没有光照也能渲染出颜色。 | +| [baseTexture](/apis/core/#PBRBaseMaterial-baseTexture) | 基础颜色纹理。搭配基础颜色使用,是个相乘的关系。 | +| [normalTexture](/apis/core/#PBRBaseMaterial-normalTexture) | 法线纹理。可以设置法线纹理 ,在视觉上造成一种凹凸感,还可以通过法线强度来控制凹凸程度。 | +| [emissiveTexture](/apis/core/#PBRBaseMaterial-emissiveTexture) | 自发射光纹理。我们可以设置自发光纹理和自发光颜色([emissiveFactor](/apis/core/#PBRBaseMaterial-emissiveTexture))达到自发光的效果,即使没有光照也能渲染出颜色。 | +| [occlusionTexture](/apis/core/#PBRBaseMaterial-occlusionTexture) | 阴影遮蔽纹理。我们可以设置阴影遮蔽纹理来提升物体的阴影细节。 | +| [tilingOffset](/apis/core/#PBRBaseMaterial-tilingOffset) | 纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移,参考 [案例](${examples}tiling-offset) | +| [clearCoat](/apis/core/#PBRBaseMaterial-clearCoat) | 透明涂层的强度,默认为 0,既不开启透明涂层效果,参考 [案例](${examples}pbr-clearcoat) 。 | +| [clearCoatTexture](/apis/core/#PBRBaseMaterial-clearCoatTexture) | 透明涂层强度纹理,和 clearCoat 是相乘的关系。 | +| [clearCoatRoughness](/apis/core/#PBRBaseMaterial-clearCoatRoughness) | 透明涂层的粗糙度。 | +| [clearCoatRoughnessTexture](/apis/core/#PBRBaseMaterial-clearCoatRoughnessTexture) | 透明涂层粗糙度纹理,和 clearCoatRoughness 是相乘的关系。 | +| [clearCoatNormalTexture](/apis/core/#PBRBaseMaterial-clearCoatNormalTexture) | 透明涂层法线纹理,如果没有设置则会共用原材质的法线。 | + +除了以上通用参数,PBR 提供了 **金属-粗糙度** 和 **高光-光泽度** 两种工作流,分别对应 [PBRMaterial](/apis/core/#PBRMaterial) 和 [PBRSpecularMaterial](/apis/core/#PBRSpecularMaterial)。 + +### PBRMaterial + +| 参数 | 应用 | +| :-- | :-- | +| [metallic](/apis/core/#PBRMaterial-metallic) | 金属度。模拟材质的金属程度,金属值越大,镜面反射越强,即能反射更多周边环境。 | +| [roughness](/apis/core/#PBRMaterial-roughness) | 粗糙度。模拟材质的粗糙程度,粗糙度越大,微表面越不平坦,镜面反射越模糊。 | +| [roughnessMetallicTexture](/apis/core/#PBRMaterial-roughnessMetallicTexture) | 金属粗糙度纹理。搭配金属粗糙度使用,是相乘的关系。 | +| [anisotropy](/apis/core/#PBRMaterial-anisotropy) | 各向异性强度。默认为 0,关闭各项异性计算。参考 [案例](${examples}pbr-anisotropy) 。 | +| [anisotropyRotation](/apis/core/#PBRMaterial-anisotropyRotation) | 各向异性旋转角度。沿切线、副切线空间旋转相应角度。 | +| [anisotropyTexture](/apis/core/#PBRMaterial-anisotropyTexture) | 各向异性纹理。RG 通道保存着各向异性方向,会和 anisotropyRotation 计算结果相乘;B 通道保存着各向异性强度,会和 anisotropy 相乘。 | + +### PBRSpecularMaterial + +| 参数 | 应用 | +| :-- | :-- | +| [specularColor](/apis/core/#PBRMaterial-specularColor) | 高光度。不同于金属粗糙度工作流的根据金属度和基础颜色计算镜面反射,而是直接使用高光度来表示镜面反射颜色。(注,只有关闭金属粗糙工作流才生效) 。| +| [glossiness](/apis/core/#PBRMaterial-glossiness) | 光泽度。模拟光滑程度,与粗糙度相反。(注,只有关闭金属粗糙工作流才生效)。 | +| [specularGlossinessTexture](/apis/core/#PBRMaterial-specularGlossinessTexture) | 高光光泽度纹理。搭配高光光泽度使用,是相乘的关系。 | + +> **注**:PBR 必须开启[环境光](/docs/graphics-light-ambient) + +如果需要通过脚本使用材质,可以前往[材质的使用教程](/docs/graphics-material-script)。 diff --git a/docs/graphics/shader/shader.md b/docs/graphics/shader/shader.md new file mode 100644 index 000000000..28630b48e --- /dev/null +++ b/docs/graphics/shader/shader.md @@ -0,0 +1,65 @@ +--- +order: 0 +title: 着色器总览 +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +在[材质教程](/docs/graphics-material-composition) 中提到,着色器可以编写顶点、片元代码来决定渲染管线输出到屏幕上像素的颜色。 + +image-20240206153815596 + +本节包含以下相关信息: + +- 内置着色器 + - [PBR](/docs/graphics-shader-pbr) + - [Unlit](/docs/graphics-shader-unlit) + - [Blinn Phong](/docs/graphics-shader-blinnPhong) +- [自定义着色器](/docs/graphics-shader-custom) +- [Shader Lab](/docs/graphics-shader-lab) + + +```glsl +const float PI = 3.1415926535897932384626433832795; + +uniform vec3 lightDirection; +uniform vec3 lightColour; +uniform vec2 lightBias; +uniform mat4 projectionViewMatrix; + +vec3 calcSpecularLighting(vec3 toCamVector, vec3 toLightVector, vec3 normal){ + vec3 reflectedLightDirection = reflect(-toLightVector, normal); + float specularFactor = dot(reflectedLightDirection , toCamVector); + specularFactor = max(specularFactor,0.0); + specularFactor = pow(specularFactor, shineDamper); + return specularFactor * specularReflectivity * lightColour; +} + +void main(void){ + + vec3 currentVertex = vec3(in_position.x, height, in_position.y); + vec3 vertex1 = currentVertex + vec3(in_indicators.x, 0.0, in_indicators.y); + vec3 vertex2 = currentVertex + vec3(in_indicators.z, 0.0, in_indicators.w); +} +``` + +## 内置着色器 + +| 类型 | 描述 | +| :-- | :-- | +| [Unlit ](/docs/graphics-material-Unlit) | Unlit 材质适用于烘焙好的模型渲染,她只需要设置一张基本纹理或者颜色,即可展现离线渲染得到的高质量渲染结果,但是缺点是无法实时展现光影交互,因为 Unlit 由纹理决定渲染,不受任何光照影响,可参考 [烘焙教程](/docs/graphics-bake-blender) 和 [导出 Unlit 教程](/docs/graphics-material-Unlit) | +| [Blinn Phong ](/docs/graphics-material-BlinnPhong) | Blinn Phong 材质适用于那些对真实感没有那么高要求的场景,虽然没有遵循物理,但是其高效的渲染算法和基本齐全的光学部分,可以适用很多的场景。 | +| [PBR ](/docs/graphics-material-PBR) | PBR 材质适合需要真实感渲染的应用场景,因为 PBR 是基于物理的渲染,遵循能量守恒,开发者通过调整金属度、粗糙度、灯光等参数,能够保证渲染效果都是物理正确的。 | + +以下属性在内置着色器中可以直接使用。 + +image-20240206173751409 + +| 参数 | 应用 | +| :-- | :-- | +| [isTransparent](/apis/core/#BaseMaterial-isTransparent) | 是否透明。可以设置材质是否透明。如果设置为透明,可以通过 [BlendMode](/apis/core/#BaseMaterial-blendMode) 来设置颜色混合模式。 | +| [alphaCutoff](/apis/core/#BaseMaterial-alphaCutoff) | 透明度裁剪值。可以设置裁剪值,在着色器中,透明度小于此数值的片元将会被裁减,参考 [案例](${examples}blend-mode) | +| [renderFace](/apis/core/#BaseMaterial-renderFace) | 渲染面。可以决定渲染正面、背面、双面。 | +| [blendMode](/apis/core/#BaseMaterial-blendMode) | 颜色混合模式。当设置材质为透明后,可以设置此枚举来决定颜色混合模式,参考 [案例](${examples}blend-mode) | +| [tilingOffset](/apis/core/#BlinnPhongMaterial-tilingOffset) | 纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移,参考 [案例](${examples}tiling-offset) | diff --git a/docs/graphics/shader/unlit.md b/docs/graphics/shader/unlit.md new file mode 100644 index 000000000..56bc79ad7 --- /dev/null +++ b/docs/graphics/shader/unlit.md @@ -0,0 +1,69 @@ +--- +order: 2 +title: Unlit +type: 着色器 +group: 网格 +label: Graphics/Shader +--- + +在一些简单的场景中,可能不希望计算光照,引擎提供了 [UnlitMaterial](/apis/core/#UnlitMaterial),使用了最精简的 shader 代码,只需要提供颜色或者纹理即可渲染。Unlit 材质适用于烘焙好的模型渲染,它只需要设置一张基本纹理或者颜色,即可展现离线渲染得到的高质量渲染结果,但是缺点是无法实时展现光影交互,因为 Unlit 由纹理决定渲染,不受任何光照影响。 + + + +## 编辑器使用 + +unlit + +## 参数介绍 + +| 参数 | 应用 | +| :-- | :-- | +| [baseColor](/apis/core/#UnlitMaterial-baseColor) | 基础颜色。**基础颜色 \* 基础颜色纹理 = 最后的颜色。** | +| [baseTexture](/apis/core/#UnlitMaterial-baseTexture) | 基础纹理。搭配基础颜色使用,是个相乘的关系。 | +| [tilingOffset](/apis/core/#UnlitMaterial-tilingOffset) | 纹理坐标的缩放与偏移。是一个 Vector4 数据,分别控制纹理坐标在 uv 方向上的缩放和偏移,参考 [案例](${examples}tiling-offset) | + +如果需要通过脚本使用材质,可以前往[材质的使用教程](/docs/graphics-material-script)。 + +## Blender 导出 Unlit 材质 + +如[烘焙教程](/docs/graphics-bake-blender)介绍,如果我们已经制作完了烘焙贴图,希望有一种**便捷材质**,颜色只由烘焙纹理影响,不用添加灯光,不用调试法线,也不用调试金属粗糙度等高阶属性,那么你可以试试 Galacean 的 [UnlitMaterial](/apis/core/#UnlitMaterial), glTF 有专门的[KHR_materials_unlit ](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit)插件,Galacean 会解析插件,生成 Unlit 材质。 + +image.png + +测试模型:[TREX.zip](https://www.yuque.com/attachments/yuque/0/2021/zip/381718/1623651429048-7f6a3610-d5cb-4a73-97f5-0d37d0c63b2c.zip?_lake_card=%7B%22src%22%3A%22https%3A%2F%2Fwww.yuque.com%2Fattachments%2Fyuque%2F0%2F2021%2Fzip%2F381718%2F1623651429048-7f6a3610-d5cb-4a73-97f5-0d37d0c63b2c.zip%22%2C%22name%22%3A%22TREX.zip%22%2C%22size%22%3A499161%2C%22type%22%3A%22application%2Fx-zip-compressed%22%2C%22ext%22%3A%22zip%22%2C%22status%22%3A%22done%22%2C%22taskId%22%3A%22u458bcbec-d647-4328-8036-3d5eb12860f%22%2C%22taskType%22%3A%22upload%22%2C%22id%22%3A%22ua8a5baad%22%2C%22card%22%3A%22file%22%7D) + +接下来介绍如何利用 Blender 软件导出有 unlit 插件的 glTF 文件 。 + +1. 导入模型 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/e5dbfb61-5c0c-4ca5-8c7f-bde353d4c211/1623651809057-138f49cf-6fe7-4f54-8161-c7e157ec85fd-20210614150752343.png) + +2. 修改 Shader + +默认的 shader 类型为 BSDF ,我们需要将材质属性栏 surface 里面的 shader 类型修改为 **Background**。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/abf1e279-1f78-4d21-8c1f-d58d7f74992c/1623652169374-7f39e5f0-6639-4795-8565-b8f0b09420ed-20210614150804567.png) + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/c8c51e5f-c7c6-44a3-87e2-dc649e13fddb/1623652230768-69cd6f7e-175d-4f9f-9042-b3629d422b8e.png) + +3. 添加烘焙纹理 + +添加烘焙好的纹理,将 Color 和 Shader 连接在一起 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/50c69e7b-c099-4a2d-b546-8a55ff4f9309/1623652264008-7ae4c13c-6430-44b0-995e-2c23c9f117a7-20210614150846797.png) + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/6ed13e19-a9e5-4454-a0d5-ad27b3cabe14/1623652368637-6dda44be-4cde-4f65-a72f-d39b5d3f60ce.png) + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/e9a99c9c-f661-4666-86bc-d8e91030c0f7/1623652380351-501dd929-7f96-4578-b49a-11724a0782a7.png) + +4. 导出 glTF + +若预览正常,导出 glTF 。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/4b6b5f8f-ebd2-46af-85c7-9a26b5f66a2e/1623652403568-450291a8-1a0b-4cf4-8e71-c183a05632b0-20210614150902221.png) + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/1fe38185-399e-4f56-bff4-c39ba4ae3a2a/1623652462007-85b065a3-69fa-4d80-9dfd-834ef66da12a.png) + +将刚才导出来的 glTF 文件拖入编辑器或者 [glTF 预览器](https://galacean.antgroup.com/#/gltf-viewer),若材质类型为 **UnlitMaterial**,说明已经导出了 glTF 的 [KHR_materials_unlit](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_unlit) 插件,且 Galacean 已经解析成 Unlit 材质。 + +![image.png](https://gw.alipayobjects.com/zos/OasisHub/fbb6ba43-f7d7-4757-a1d3-590083d30573/1623652636074-d8bb8437-f885-43fd-8957-8e14ae9fd8c0-20210614150914493.png) diff --git a/docs/graphics/texture/2d.md b/docs/graphics/texture/2d.md new file mode 100644 index 000000000..b370ab8fa --- /dev/null +++ b/docs/graphics/texture/2d.md @@ -0,0 +1,114 @@ +--- +order: 1 +title: 2D 纹理 +type: 图形 +group: 纹理 +label: Graphics/Texture +--- + +2D 纹理([Texture2D](/apis/core/#Texture2D))是最常用的美术资源,使用二维 UV 坐标进行采样。 + +## 创建 + +在编辑器中可以方便地导入一张 2D 纹理,按照路径 **[资产面板](/docs/assets-interface)** -> **右键上传** -> **选择 Texture2D** -> **选择对应贴图** -> **2D 纹理资产创建完毕** 操作即可。 + +image.png + +同样的,在脚本中可以通过 [ResourceManager](/apis/core/#ResourceManager) 加载图片获取对应的 2D 纹理: + +```typescript +const textureResource = { + type: AssetType.Texture2D, + url: `图片url`, +}; + +engine.resourceManager + .load(textureResource, cubeTextureResource) + .then((resource) => { + // 引擎支持的2D纹理 + const texture = resource; + // 接下来可以将纹理应用到材质上或者进行其他操作 + }); +``` + +## 方法 + +| 方法 | 解释 | +| :------------- | :--------------------- | +| setImageSource | 设置纹理的图像数据源头 | +| setPixelBuffer | 修改纹理对象的图像数据 | +| getPixelBuffer | 获取纹理对象的图像数据 | + +### setImageSource + +前面提到过,图片、canvas 画布、视频等跟图像相关的数据源都可以用来当作纹理。比如视频就可以通过 [setImageSource](/apis/core/#Texture2D-setImageSource) 接口上传到纹理: + +```typescript +// 拿到视频标签,即 HTMLVideoElement +const video = document.getElementsByTagName("video")[0]; + +// 加载到纹理 +texture.setImageSource(video); +``` + +> `setImageSource` 只能同步那一帧的数据,但是视频每一帧都在变化,如果需要纹理同步变化,则要在脚本 onUpdate 钩子里面执行 + +对于视频这类需要频繁更新纹理内容的使用场景,创建纹理的时候需要关闭 mipmap 并设置纹理使用方式为 Dynamic,以获得更好的性能,示例代码如下: + + + +### setPixelBuffer + +纹理底层其实对应着每个像素的颜色值,即 RGBA 通道,我们可以手动填写这些颜色通道的颜色数值,然后通过 [setPixelBuffer](/apis/core/#Texture2D-setPixelBuffer) 接口传到纹理中: + +```typescript +const texture = new Texture2D(engine, 1, 1); +// 将该像素设置为红色,即 R 通道为 255。 +const data = new Uint8Array([255, 0, 0, 255]); +texture.setPixelBuffer(data); +``` + +### getPixelBuffer + +同样的,我们可以读取这些颜色通道的颜色数据: + +```typescript +const texture = new Texture2D(engine, width, height); +// 对纹理做了一系列处理 +// ··· +// 用来保存颜色信息的数组,它的大小和要读取的数据量相等 +const data = new Uint8Array(width * height * 4); +texture.getPixelBuffer(0, 0, width, height, 0, data); +``` + +## 使用 + +将纹理赋予材质球的相应属性,可以开启不同的渲染功能,如添加基础颜色纹理,可以决定模型的基本色调。在编辑器中,只需在对应属性选择相应纹理即可。 + +image.png + +对应的,在脚本中,可以这样设置: + +```typescript +const material = new PBRMaterial(engine); +const texture = 生成纹理(); // 上文所示,不再赘述 + +material.baseTexture = texture; +``` + +## 色彩膨胀 + +image.png + +为了解决带透明像素图片在 Alpha 值突变处出现黑边的问题,编辑器内置了色彩膨胀功能。该功能是通过将图片中所有透明像素的 RGB 值改写为与其最临近非完全透明像素的 RGB 值,达到去除图片黑边的效果。 + +| 选项 | 解释 | +| :--------------- | :---------------------------------------------- | +| 范围 Alpha Range | 阈值,透明像素 Alpha 值小于此阈值, RGB 值被修改 | +| 大小 Alpha Value | 透明像素填充后的 Alpha 值 | + +## 导出配置 + +image.png + +[项目发布](/docs/assets-build)文档中详细说明了纹理导出时的**全局配置**,若此处勾选 Overwrite 选项时,此资产导出时将遵循**自定义配置**将非**全局配置**。 diff --git a/docs/graphics/texture/compression.md b/docs/graphics/texture/compression.md new file mode 100644 index 000000000..2978aaa6e --- /dev/null +++ b/docs/graphics/texture/compression.md @@ -0,0 +1,44 @@ +--- +order: 4 +title: 纹理压缩 +type: 图形 +group: 纹理 +label: Graphics/Texture +--- + +**[KTX2](https://www.khronos.org/ktx/)**(Khronos Texture Container version 2.0) 是 Khronos 推出最新的纹理压缩方案,Galacean 自 1.1 版本开始已经支持。KTX2 会根据设备平台支持运行时转码到对应格式的压缩纹理(BC/PVRTC/ETC/ASTC)。 + +## 使用 + +在引擎中,直接使用 `resourceManager` 加载即可: + +```typescript +engine.resourceManager.load("xxx.ktx2"); +// 或 +engine.resourceManager.load({ + type: AssetType.KTX2, + url: "xxx.ktx2", +}).then(tex=>{ + material.baseTexture = tex; +}) +``` + + + +glTF 中使用 ktx2 需要包含 [KHR_texture_basisu](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_texture_basisu/README.md) 扩展。 + +KTX2 的生成可以使用: + +- toktx +- basisu +- 编辑器打包,可以参考『[项目发布](/docs/assets-build)』文档。 + +## 兼容性 + +KTX2 转码使用到了 WebAssembly 技术,需要使用 Chrome 57+,和 iOS 11.3+(11.0 ~ 11.2.以下的 WebAssembly 存在 [bug](https://bugs.webkit.org/show_bug.cgi?id=181781)) + +iOS 16 以下系统,在通过 worker 加载必要的 KTX2 解析 wasm 文件时会概率发生无返回的情况,尤其是在 wasm 首次加载时概率较大。可以通过 iOS 不走 worker 来绕过去: + +```typescript +WebGLEngine.create({ canvas: "canvas", ktx2Loader: { workerCount: 0 } }); +``` diff --git a/docs/graphics/texture/cube.md b/docs/graphics/texture/cube.md new file mode 100644 index 000000000..84c577aca --- /dev/null +++ b/docs/graphics/texture/cube.md @@ -0,0 +1,53 @@ +--- +order: 2 +title: 立方纹理 +type: 图形 +group: 纹理 +label: Graphics/Texture +--- + +立方纹理([TextureCube](/apis/core/#TextureCube))和 2D 纹理的区别是它有 6 个面,即用 6 张 2D 纹理组成了一个立方纹理。 + +![image.png](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*Omw8Qo0WzfYAAAAAAAAAAAAAARQnAQ) + +![image.png](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*r-XPSaUTEnEAAAAAAAAAAAAAARQnAQ) + +立方纹理和 2D 纹理的底层采样方式略有不同,纹理使用二维坐标进行采样,而立方纹理使用三维坐标,即 _方向向量_ 进行采样,如使用一个橘黄色的方向向量来从立方纹理上采样一个纹理值会像是这样: + +![image.png](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*X752S5pQSB0AAAAAAAAAAAAAARQnAQ) + +正因为这种采样特性,所以立方纹理可以用来实现天空盒、环境反射等特效。 + +## 创建 + +> 可以在 [Poly Haven](https://polyhaven.com/) 或 [BimAnt HDRI](http://hdri.bimant.com/) 下载免费的 HDR 贴图 + +在准备好 HDR 后,依照路径 **[资产面板](/docs/assets-interface)** -> **右键上传** -> **选择 TextureCube(.hdr)** -> **选择对应 HDR 贴图** -> **立方纹理资产创建完毕** 操作即可。 + +![image.png](https://mdn.alipayobjects.com/huamei_yo47yq/afts/img/A*Oi3FSLEEaYgAAAAAAAAAAAAADhuCAQ/original) + +同样的,在脚本中可以通过加载六张对应顺序的纹理也能得到相应的立方纹理。 + +```typescript +const cubeTextureResource = { + type: AssetType.TextureCube, + urls: [ + "px - right 图片 url", + "nx - left 图片 url", + "py - top 图片 url", + "ny - bottom 图片 url", + "pz - front 图片 url", + "nz - back 图片 url", + ], +}; + +engine.resourceManager.load(cubeTextureResource).then((resource) => { + // 引擎支持的立方纹理 + const cubeTexture = resource; + // 接下来可以将纹理应用到材质上或者进行其他操作 +}); +``` + +## 使用 + +立方纹理主要在天空盒中使用,详情可参考[天空背景](/docs/graphics-background-sky) diff --git a/docs/graphics/texture/rtt.md b/docs/graphics/texture/rtt.md new file mode 100644 index 000000000..6e7049e3e --- /dev/null +++ b/docs/graphics/texture/rtt.md @@ -0,0 +1,54 @@ +--- +order: 3 +title: 离屏渲染纹理 +type: 图形 +group: 纹理 +label: Graphics/Texture +--- + +离屏渲染纹理,顾名思义,该纹理可以通过离屏渲染得到。底层使用了 [FBO](https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/framebufferTexture2D) 技术,将渲染操作不再输出到屏幕上,而是输出到纹理上,用户通过该纹理,可以用来实现后处理特效、折射、反射、动态环境贴图等一些艺术创作。 + +引擎提供了 [RenderTarget](/apis/core/#RenderTarget) 类来进行离屏渲染,并获取相应的离屏渲染纹理,目前引擎支持生成以下离屏渲染纹理: + +| 类型 | 应用 | +| :-- | :-- | +| 颜色纹理([Texture](/apis/core/#Texture)) | 可以传入单张颜色纹理、多张颜色纹理 (MRT)、颜色立方纹理 | +| 深度纹理([Texture](/apis/core/#Texture)) | 可以传入深度纹理、深度立方纹理 | +| 纹理组合 | 颜色纹理 + 深度纹理、颜色立方体纹理 + 深度立方体纹理、多张颜色纹理 + 深度纹理 | + +## 使用 + +这里举例使用脚本 `onBeginRender` 钩子,在每一帧渲染前,先将屏幕`物体A`渲染到`离屏纹理`上,然后将离屏纹理当作`物体B`的基础纹理,将物体 B 渲染到`屏幕`上。假设 `物体A` 的 layer 为 `Layer0`,`物体B` 的 layer 为 `Layer1`; + +``` +class switchRTScript extends Script { + renderColorTexture = new Texture2D(engine, 1024, 1024); + renderTarget = new RenderTarget( + engine, + 1024, + 1024, + this.renderColorTexture + ); + + constructor(entity: Entity) { + super(entity); + // 将离屏纹理当作物体B的基础纹理 + materialB.baseTexture = this.renderColorTexture; + } + + onBeginRender(camera: Camera) { + // 渲染物体A + camera.renderTarget = this.renderTarget; + camera.cullingMask = Layer.Layer0; + camera.render(); + + // 还原 RT,接下来渲染物体B到屏幕。 + camera.renderTarget = null; + camera.cullingMask = Layer.Layer1; + } + } + + cameraEntity.addComponent(switchRTScript); +``` + + diff --git a/docs/graphics/texture/texture.md b/docs/graphics/texture/texture.md new file mode 100644 index 000000000..19313a468 --- /dev/null +++ b/docs/graphics/texture/texture.md @@ -0,0 +1,155 @@ +--- +order: 0 +title: 纹理总览 +type: 图形 +group: 纹理 +label: Graphics/Texture +--- + +纹理([Texture](/apis/core/#Texture)), 是在 3D 渲染中最常用到的资源。我们在给模型着色时,需要给每个片元设置一个颜色值,这个色值除了直接手动设置,我们还可以选择从纹理中读取纹素来进行着色,来达到更加丰富的美术效果。 + +> 值得注意的是,图片、canvas 画布、原始数据、视频等都可以用来当作纹理,Galacean 引擎目前支持所有 WebGL 标准的纹理。 + +我们会发现,引擎中大量的问题都来源于不同空间之间的映射(例如 MVP 变换),纹理也是如此,开发者不仅需要了解图片空间到纹理空间的映射关系,也需要了解纹素到像素的映射规则。 + +本文将主要介绍: + +- 纹理的类型,纹理空间和通用属性 +- [2D 纹理](/docs/graphics-texture-2d) +- [立方纹理](/docs/graphics-texture-cube) +- [通过纹理播放视频](/docs/graphics-texture-2d) +- [设置天空纹理](/docs/graphics-background-sky) +- [离屏渲染纹理](/docs/graphics-texture-rtt) +- 使用[压缩纹理](/docs/graphics-texture-compression) + +## 纹理类型 + +| 类型 | 描述 | +| :--------------------------------------- | :----------------------------------------------------------------- | +| [2D 纹理](/docs/graphics-texture-2d) | 最常用的美术资源,使用二维 UV 坐标进行采样 | +| [立方纹理](/docs/graphics-texture-cube) | 6 张 2D 纹理组成了一个立方纹理,可以用来实现天空盒、环境反射等特效 | +| 2D 纹理数组 | 只占用一个纹理单元,非常适合用来实现需要切换纹理图集的需求 | + +## 纹理空间 + +纹理空间是由纹理形状决定的, 2D 纹理对应需要使用二维空间向量进行纹理采样,相应地,立方纹理需要使用三维空间向量进行纹理采样。 + +
+
+ Texture 2D +
Texture 2D
+
+
+ Texture Cube +
Texture Cube
+
+
+ +## 通用属性 + +虽然纹理类型多样,但他们都有一些相似的基本属性与设置: + +| 属性 | 值 | +| :-------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 循环模式 U([wrapModeU](/apis/core/#Texture-wrapModeU)) | 截取模式([Clamp](/apis/core/#TextureWrapMode-Clamp))、 重复模式([Repeat](/apis/core/#TextureWrapMode-Repeat))、镜像重复模式([Mirror](/apis/core/#TextureWrapMode-Mirror)) | +| 循环模式 V([wrapModeV](/apis/core/#Texture-wrapModeV)) | 截取模式([Clamp](/apis/core/#TextureWrapMode-Clamp))、重复模式([Repeat](/apis/core/#TextureWrapMode-Repeat))、 镜像重复模式([Mirror](/apis/core/#TextureWrapMode-Mirror)) | +| 过滤模式([filterMode](/apis/core/#Texture-filterMode)) | 点过滤([Point](/apis/core/#TextureFilterMode-Point))、双线性过滤([Bilinear](/apis/core/#TextureFilterMode-Bilinear))、 三线性过滤([Trilinear](/apis/core/#TextureFilterMode-Trilinear)) | +| 各向异性过滤等级([anisoLevel](/apis/core/#Texture-anisoLevel)) | 1 ~ 16 ,具体要看设备支持情况 | + +### 循环模式 + +纹理采样的范围为`[0,1]`,那么当纹理 UV 坐标超出这个范围时,我们可以通过设置循环模式来控制如何进行超出部分的采样。 + +| 采样循环模式 | 解释 | +| :----------- | :---------------------------- | +| Clamp | 超出范围采样边缘纹素 | +| Repeat | 超出范围从 [0,1] 开始重新采样 | +| Mirror | 超出范围从 [1,0] 开始镜像采样 | + + + +### 过滤模式 + +一般来说,纹素和屏幕像素不会刚好对应,我们可以通过设置过滤模式来控制放大(Mag)和缩小(Min)模式下分别的过滤模式。 + +| 采样过滤模式 | 解释 | +| :----------- | :----------------------------------------------------- | +| Point | 使用距离采样点最近的纹素 | +| Bilinear | 使用距离最近的 2\*2 纹素矩阵的平均值 | +| Trilinear | 在双线性过滤的基础上,对 mipmap 层级也进行了平均值过滤 | + + + +### 各向异性过滤等级 + +各向异性过滤技术可以使纹理在倾斜角度下观看会更加清晰。如下图,纹理的尽头随着各向异性过滤等级的增加会愈加清晰。但请慎重使用,数值越大,GPU 的计算量就会越大。 + + + +## 通用设置 + +| 设置 | 值 | +| :--------------- | :------------------------ | +| mipmap | 多级纹理渐变(默认开启) | +| flipY | 翻转 Y 轴(默认关闭) | +| premultiplyAlpha | 预乘透明通道(默认关闭) | +| format | 纹理格式(默认 R8G8B8A8) | + +### mipmap + +**引擎默认开启 [mipmap](/apis/core/#Texture-generateMipmaps)**(多级纹理渐变),mipmap 用来解决从低分辨率屏幕中采样高分辨率纹理时的精度和性能问题,即能在合适的距离时选取不同分辨率的纹理,如下图: + +![image.png](https://gw.alipayobjects.com/mdn/rms_d27172/afts/img/A*mTBvTJ7Czt4AAAAAAAAAAAAAARQnAQ) + +需要注意的是,WebGL2.0 支持**任意分辨率**的纹理,会根据 [mipmap](http://download.nvidia.com/developer/Papers/2005/NP2_Mipmapping/NP2_Mipmap_Creation.pdf) 算法进行一层层的 mip,但是如果您的环境是在 WebGL1.0 环境,那么请务必上传**2 次幂纹理**,如 1024 \* 512 这种分辨率的纹理,否则 Galacean 会检测到环境不可使用 mipmap,自动降级关闭 mipmap 功能,在视觉上带来一些意外情况。 + +如果需要改变 mipmap 的默认行为,可以通过脚本来实现,参数详见 [API](/apis/core/#Texture2D-constructor): + +```typescript +const texture = new Texture2D( + engine, + width, + height, + TextureFormat.R8G8B8A8, + false +); // 第 5 个参数 +``` + +立方纹理脚本写法,详见 [API](/apis/core/#TextureCube-constructor): + +```typescript +const cubeTexture = new TextureCube( + engine, + size, + TextureFormat.R8G8B8A8, + false +); // 第 4 个参数 +``` + + + +### flipY + +flipY 用来控制纹理是否翻转 Y 轴,即上下颠倒,**引擎和编辑器默认关闭**,如果需要改变 flipY 的默认行为,可以通过 [setImageSource](/apis/core/#Texture2D-setImageSource) 方法来实现: + +```typescript +const texture = new Texture2D(engine, width, height); +texture.setImageSource(img, 0, true); // 第 3 个参数 +``` + +### premultiplyAlpha + +premultiplyAlpha 用来控制纹理是否预乘 alpha(透明) 通道,**引擎和编辑器默认关闭**,如果需要改变 premultiplyAlpha 的默认行为,可以通过 [setImageSource](/apis/core/#Texture2D-setImageSource) 方法来实现: + +```typescript +const texture = new Texture2D(engine, width, height); +texture.setImageSource(img, 0, undefined, true); // 第 4 个参数 +``` + +### format + +引擎默认使用 `TextureFormat.R8G8B8A8` 作为纹理格式, 即红蓝绿、透明通道分别使用 1 个字节,每个通道允许保存 【0 ~ 255】 大小的颜色值。引擎支持配置不同的纹理格式,具体可以参考 [TextureFormat](/apis/core/#TextureFormat)。比如我们不需要使用透明通道,即 A 通道,那么我们可以使用 `TextureFormat.R8G8B8`: + +```typescript +const texture = new Texture2D(engine, width, height, TextureFormat.R8G8B8); +``` diff --git a/docs/input/framebuffer-picker.md b/docs/input/framebuffer-picker.md new file mode 100644 index 000000000..916db9e52 --- /dev/null +++ b/docs/input/framebuffer-picker.md @@ -0,0 +1,43 @@ +--- +order: 4 +title: 帧缓冲拾取 +type: 交互 +label: Interact +--- + +在三维应用中时常需要拾取场景中的物体,[射线包围盒](/docs/physics-manager#使用射线检测)是一种常用的方法,在 CPU 中进行拾取,**性能较好,但是精度较差**,因为包围盒比较简单,不能拾取复杂的模型。 + +当拾取频率不高时,可以考虑使用**像素级精度**的 `FramebufferPicker` 组件;当拾取频率过高时,需要开发者评估好性能开销是否适合业务场景,因为该组件底层会进行 CPU-GPU 通信,即调用 `gl.readPixels` 。 + + + +## 创建帧缓冲拾取 + +```typescript +import { FramebufferPicker } from "@galacean/engine-toolkit-framebuffer-picker"; + +const framebufferPicker = rootEntity.addComponent(FramebufferPicker); +framebufferPicker.camera = camera; +``` + +## 注册拾取事件 + +```typescript +class ClickScript extends Script { + onUpdate(): void { + const inputManager = this.engine.inputManager; + if (inputManager.isPointerDown(PointerButton.Primary)) { + const pointerPosition = inputManager.pointerPosition; + framebufferPicker.pick(pointerPosition.x, pointerPosition.y).then((renderElement) => { + if (renderElement) { + // ... + } else { + // ... + } + }); + } + } +} + +cameraEntity.addComponent(ClickScript); +``` diff --git a/docs/input/keyboard.md b/docs/input/keyboard.md new file mode 100644 index 000000000..f4de7224b --- /dev/null +++ b/docs/input/keyboard.md @@ -0,0 +1,66 @@ +--- +order: 2 +title: 键盘 +type: 交互 +label: Interact +--- + +Galacean 支持开发者随时查询当前的键盘交互实况,且调用接口十分简单。 + +## 方法 + +| 方法名称 | 方法释义 | +| ------------------------------------------------------ | -------------------------- | +| [isKeyHeldDown](/apis/core/#InputManager-isKeyHeldDown) | 返回这个按键是否被持续按住 | +| [isKeyDown](/apis/core/#InputManager-isKeyDown) | 返回当前帧是否按下过此按键 | +| [isKeyUp](/apis/core/#InputManager-isKeyUp) | 返回当前帧是否抬起过此按键 | + +## 快速上手 + +下方枚举了检测按键状态的简单示例。 + +```typescript +class KeyScript extends Script { + onUpdate() { + const { inputManager } = this.engine; + if (inputManager.isKeyHeldDown(Keys.Space)) { + // 现在还按着空格键 + } + if (inputManager.isKeyDown(Keys.Space)) { + // 这帧按下过空格键 + } + if (inputManager.isKeyUp(Keys.Space)) { + // 这帧抬起过空格键 + } + } +} +``` + +## 实战 + +这次就用空格键来控制愤怒的小鸟吧。 + + + +## 状态字典 + +| 按键状态 | isKeyHeldDown | isKeyDown | isKeyUp | +| -------------------------- | ------------- | --------- | ------- | +| 该键从上帧开始就一直按着 | true | false | false | +| 该键当前帧按下后就没有松开 | true | true | false | +| 该键在当前帧松开后又按下 | true | true | true | +| 该键在当前帧按下后又松开 | false | true | true | +| 该键在当前帧被抬起 | false | false | true | +| 该键没按下且没交互 | false | false | false | +| 不会出现这种情况 | true | false | true | +| 不会出现这种情况 | false | true | false | + +## Keys + +Galacean 所枚举的键盘 Keys 与实体键盘一一对应,参考 W3C 标准,且兼容各种不同硬件的特制按键。 + +Keys 枚举:https://github.com/galacean/engine/blob/main/packages/core/src/input/enums/Keys.ts + +W3C 标准:https://www.w3.org/TR/2017/CR-uievents-code-20170601/ + +键盘输入设计思路:https://github.com/galacean/engine/wiki/Keyboard-Input-design diff --git a/docs/input/pointer.md b/docs/input/pointer.md new file mode 100644 index 000000000..f0c0ea60c --- /dev/null +++ b/docs/input/pointer.md @@ -0,0 +1,137 @@ +--- +order: 1 +title: 触控 +type: 交互 +label: Interact +--- + +Galacean 的触控是基于 [PointerEvent](https://www.w3.org/TR/pointerevents3/) 实现的,它抹平了 [MouseEvent](https://developer.mozilla.org/zh-CN/docs/Web/API/MouseEvent) 与 [TouchEvent](https://developer.mozilla.org/zh-CN/docs/Web/API/TouchEvent) 的差异,使得触控在概念和接口上都得到了统一。 + +## Pointer + +在 Galacean 中,无论是 PC 上的鼠标,移动端的触控笔或是指头,当他在触控范围内发生对应行为时( **Down**, **Move**, etc),都会被实例化为 [Pointer](/apis/core/#Pointer),您可以在 [InputManager](/apis/core/#InputManager) 里获取到当前活动着的所有触控点。 + +image.png + +> 需要注意的是,每个触控点都是相互独立的,它们响应对应的事件并回调相应的钩子函数。 + +### 生命周期 + +每个触控点都会在 **PointerDown** 或者 **PointerMove** 中开启自己的一生,在 **PointerLeave** 或者 **PointerCancel** 后黯然离场,在 Galacean 中,您可以通过 `Pointer.phase` 获取这个触控点的即时状态。 + +```mermaid +timeline + title Phase of pointer + …… + Frame 100 : 「PointerDown」 + : pointer.phase = PointerPhase.Down + Frame 101 : 「PointerMove」 + : pointer.phase = PointerPhase.Move + Frame 102 : 「Nothing」 + : pointer.phase = PointerPhase.Stationary + Frame 103 : 「PointerUp」 + : pointer.phase = PointerPhase.Up + Frame 104 : 「PointerLeave」 + : pointer.phase = PointerPhase.Leave + …… +``` + + + +### 触控按键 + +参照 [W3C 标准](https://www.w3.org/TR/uievents/#dom-mouseevent-button) 与[微软相关文档](https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.mousebutton?view=windowsdesktop-6.0),Galacean 对触控按键的定义如下: + +| 枚举 | 解释 | +| :---------------------------------------------- | :--------------------------------------------------------------- | +| [None](/apis/core/#PointerButton-None) | 无触控按键按下 | +| [Primary](/apis/core/#PointerButton-Primary) | 设备的主按键,通常为左键(鼠标)或单按键设备上的唯一按键(指头) | +| [Secondary](/apis/core/#PointerButton-Secondary) | 设备的次级按键,通常为右键(鼠标) | +| [Auxiliary](/apis/core/#PointerButton-Auxiliary) | 设备的辅助按键,通常为滚轮(鼠标) | +| [XButton1](/apis/core/#PointerButton-XButton1) | 设备的拓展按键,通常为撤销按键(鼠标) | +| [XButton2](/apis/core/#PointerButton-XButton2) | 设备的拓展按键,通常为恢复按键(鼠标) | +| [XButton3](/apis/core/#PointerButton-XButton3) | 拓展按键 | +| [XButton4](/apis/core/#PointerButton-XButton4) | 拓展按键 | +| …… | …… | + +结合触控按键可以方便地检测触控点在本帧触发的行为: + + + +### 触控回调 + +只需要为添加了 Collider 组件的 Entity 增加触控回调,就可以实现与渲染物体交互的能力。触控回调已经整合到引擎的[脚本组件生命周期](/docs/script#组件生命周期函数)中,用户可以很方便地添加以下事件,同时钩子函数中会携带触发此回调的 Pointer 实例。 + +| 接口 | 触发时机与频率 | +| :------------------------------------------------- | :------------------------------------------------------------------------- | +| [onPointerEnter](/apis/core/#Script-onPointerEnter) | 当触控点进入 Entity 的碰撞体范围时触发一次 | +| [onPointerExit](/apis/core/#Script-onPointerExit) | 当触控点离开 Entity 的碰撞体范围时触发一次 | +| [onPointerDown](/apis/core/#Script-onPointerDown) | 当触控点在 Entity 的碰撞体范围内按下时触发一次 | +| [onPointerUp](/apis/core/#Script-onPointerUp) | 当触控点在 Entity 的碰撞体范围内松开时触发一次 | +| [onPointerClick](/apis/core/#Script-onPointerClick) | 当触控点在 Entity 的碰撞体范围内按下并松开,在松开时触发一次 | +| [onPointerDrag](/apis/core/#Script-onPointerDrag) | 当触控点在 Entity 的碰撞体范围内按下时**持续**触发,直至触控点解除按下状态 | + +> ⚠️ 触控回调**依赖物理引擎**,使用此功能前请确保物理引擎已初始化完毕。 + +如下示例: + +- 最左边的立方体添加了对 Enter 与 Exit 的响应,当鼠标移动到上方和鼠标移出时便会触发它颜色的改变。 +- 中间的立方体添加了对 Drag 的响应,你可以用鼠标拖拽这个立方体在空间内任意移动。 +- 最右边的立方体添加了对 Click 的响应(先 down 后 up ),当鼠标点击时会触发它颜色的改变。 + + + +### 射线检测 + +触控回调是基于射线检测实现的,若要自定义射线检测也十分简单,只需按照如下步骤即可。 + +```mermaid +flowchart LR + 添加碰撞体组件 --> 获取触控点 --> 通过画布坐标获取射线 --> 射线检测 +``` + +添加碰撞体组件可参考[物理相关文档](/docs/physics-collider),实现检测部分的代码逻辑如下: + +```typescript +// 假设当前有一个活动的触控点 +const pointer = inputManager.pointers[0]; +// 通过触控点得到由相机发射的射线 +const ray = camera.screenPointToRay(pointer.position, new Ray()); +// 射线与场景的碰撞体进行碰撞检测 +const hitResult = new HitResult(); +if (scene.physics.raycast(ray, 100, hitResult)) { + console.log("Hit entity", hitResult.entity); +} +``` + +通过下方示例可以更直观地理解此过程,示例中为主相机添加了辅助线,侧视相机可以完整观察到主相机射线检测到碰撞体的过程。 + + + +## 兼容性 + +截止 2024 年 2 月,不同平台对 PointerEvent 的兼容性已经达到了 [96.35%](https://caniuse.com/?search=PointerEvent) 。 + +设计思路可参考:https://github.com/galacean/engine/wiki/Input-system-design. + +> ⚠️ 若遇到平台的兼容性问题,可以在 https://github.com/galacean/polyfill-pointer-event 提 issue 。 + +## QA + +### 触控在 PC 端正常,但在移动端异常 + +在移动端,触控会触发 HTML 元素的默认行为,一旦触发默认行为,触控就会从元素上被移除(PointerCancel),可以通过设置监听源的 `touchAction` 解决,若触控的监听源为默认画布: + +```typescript +(engine.canvas._webCanvas as HTMLCanvasElement).style.touchAction = "none"; +``` + +### 右键操作失效,弹出菜单栏 + +这是由于右键触发系统默行为导致的,可以加入下列代码阻止: + +```typescript +document.oncontextmenu = (e) => { + e.preventDefault(); +}; +``` diff --git a/docs/input/wheel.md b/docs/input/wheel.md new file mode 100644 index 000000000..eab8748aa --- /dev/null +++ b/docs/input/wheel.md @@ -0,0 +1,16 @@ +--- +order: 3 +title: 滚轮 +type: 交互 +label: Interact +--- + +Galacean 的滚轮输入是基于 [WheelEvent](https://www.w3.org/TR/uievents/#interface-wheelevent) 实现的。 + +## 使用 + +image.png + +可以依此实现用滚轮控制相机距离的示例。 + + diff --git a/docs/interface/hierarchy.md b/docs/interface/hierarchy.md new file mode 100644 index 000000000..b58fe6eeb --- /dev/null +++ b/docs/interface/hierarchy.md @@ -0,0 +1,113 @@ +--- +order: 2 +title: 层级面板 +type: 基础知识 +group: 界面 +label: Basics/Interface +--- + +层级面板位于编辑器的最左侧,它以树状结构显示当前场景中的所有节点,场景节点是所有其他节点的父节点,包括相机、灯光、网格等等。 + +Hierarchy Panel + +在层级面板,您可以: + +- 添加,删除或克隆某个节点 +- 复制节点的路径信息 +- 通过拖拽调整节点的层级 +- 模糊搜索场景中的节点 +- 临时隐藏某个节点 + +## 节点的新增,删除与拷贝 + +### 新增节点 + +> 您既可以添加空节点,也可以快速添加挂载相应功能组件的节点,如挂载相机组件的节点,挂载光源组件节点,以及挂载 3D/2D 基础渲染组件的节点。 + +您可以按照 **点击添加按钮** -> **选择要添加的节点** 的步骤新增节点,需要注意的是,若您此时正选中了某个节点,那么添加的节点将会成为**选中节点的子节点**,否则将默认为场景的子节点: + +
+ add button +
+
通过添加按钮新增节点
+ +您也可以按照 **右键某个节点** -> **选择要添加的节点** 的步骤为该节点新增子节点: + +
+ right click +
+
右键新增节点
+ +添加完毕后,您可以在 **[检查器面板](/docs/interface-inspector)** 中对新节点的属性进行编辑。 + +### 删除节点 + +> 删除节点会删除节点及其所有的子节点。所以在删除节点时,你需要注意所删除的节点是否会影响场景中其他节点。 + +您可以按照 **选中待删节点** -> **点击删除按钮** 的步骤删除节点: + +
+ del button +
+
通过删除按钮移除节点
+ +您也可以按照 **右键某个节点** -> **Delete** 的步骤移除该节点: + +
+ del button +
+
右键移除节点
+ +此外,您还可以在选中后通过快捷键直接删除节点。 + +### 拷贝节点 + +> 拷贝节点会拷贝选中节点及其所有的子节点,本质上是在调用引擎的[克隆](/docs/core-clone)能力。 + +你可以在选中某节点后,通过 `Duplicated` 在同层级下快速克隆该节点。 + +
+ del button +
+
Duplicated 克隆节点
+ +也可以分别选择 `copy` 与 `paste` ,从而实现跨层级拷贝。 + +
+ del button +
+
Copy Paste 克隆节点
+ +此外,您还可以通过快捷键 `⌘` + `D` 快速复制选中的节点。 + +## 节点排序 + +为了更好的组织节点,你可以通过拖拽的方式来排序节点。选中一个节点后,可以通过鼠标左键拖拽来改变节点在层级树中的位置。 + +
+ del button +
+
拖拽排序
+ +## 节点搜索 + +层级面板上方有一个搜索框,用户可以输入节点的名称来搜索场景中的节点。搜索框支持模糊搜索,你可以输入节点名称的部分字符来查找节点。 + +## 节点隐藏 + +每个实体节点右侧都有一个眼睛按钮,点击可以切换节点在场景中的显示/隐藏状态。 + +> 需要注意的是, 此处对节点显示状态的调整仅是工作区的修改, 而非在 **[检查器面板](/docs/interface-inspector)** 中的 `isActive` 的属性。 + +## 快捷键 + +以下操作在选中节点后方可生效。 + +| 操作 | 快捷键 | +| :--------------- | :-------------------------------------------------------------------------------------------------------------------------------- | +| `删除节点` | | +| `复制节点` | `⌘` + `D` | +| `选中上一个节点` | 方向键 ⬆️ | +| `选中下一个节点` | 方向键 ⬇️ | +| `展开节点` | 方向键 ➡️ | +| `折叠节点` | 方向键 ⬅️ | diff --git a/docs/interface/inspector.md b/docs/interface/inspector.md new file mode 100644 index 000000000..9939a37b6 --- /dev/null +++ b/docs/interface/inspector.md @@ -0,0 +1,72 @@ +--- +order: 4 +title: 检查器面板 +type: 基础知识 +group: 界面 +label: Basics/Interface +--- + +检查器面板位于编辑器右侧,它会是你在使用编辑器的过程中最常用的面板。基于你当前选择所选择的东西,检查器面板会显示出相对应的属性。你可以使用检查器面板来编辑场景中几乎所有的事物,如场景、实体、组件、资产等等。 + +
+
+ image-20240122144004260 +
场景检查器
+
+
+ image-20240122144102202 +
实体检查器
+
+
+ image-20240122144141450 +
资产检查器
+
+
+ + +## 属性类型 + +检查器面板中的属性可以分成两大类: + +- **基本数值类型**:数字调节、颜色选择、属性切换等 +- **引用类型**:通常是资源,比如材质选择、纹理选择等 + +### 数字调节 + +检查器中提供了很多数字调节的入口。针对不同的属性,数字可调节的范围,每次调整的大小都会不同。最典型的是调整 `Transform` 组件的位置、旋转、缩放属性值。 + +你可以通过拖拽输入框右侧的滑块来快速调整数字的大小。在拖拽时,按住 `⌘`(window 上为 `ctrl`)可以更精确地调整数字的大小(精度为原 step 的 1/10)。 + +image-20240318175444343 + +一些可以调节的属性是以滑动条的形式出现的。你可以拖动滑块来快速调整数字的大小,如灯光的 `Intensity`。同样的,在拖动滑块时,按住 `⌘`(window 上为 `ctrl`)可以更精确的调整数字的大小。 + +image-20240318175518354 + +还有一些数字调节的属性是以输入框和按钮的形式出现的,如阴影的 `Near Plane`。这些属性往往拥有更精确的步进大小(如 0.1, 1, 10)。点击按钮可以直接以步进长度来增加或减小数值。 + +image-20240318175638055 + +### 颜色面板 + +一些属性需要调整颜色,如光照、场景的背景色,亦或者材质的自发光颜色等。想要调整颜色,你需要点击左侧的颜色按钮来唤起颜色选择器。在颜色选择器中,你可以使用 HUE 来选择颜色,调整颜色的透明度;也可以在输入框来调整颜色具体的 RGBA 数值。点击 image-20230926110451443按钮可以在 HSLA,RGBA 和 HEXA 三种模式下进行切换。 + +image-20240318175748734 + +### 资产选择浮窗 + +一些属性需要引用到需要的资产,在这种情况下,你可以点击资产选择器的输入框来唤起资产选择浮窗。不同的属性需要的资产类型不同,但资产选择器已经默认配置好了相应的过滤器,直接选择即可。 + +资产选择浮窗中还提供了一个搜索框,你可以使用它来更精确的找到对应的资产。 + +
+
+ image-20240122143554973 +
Mesh Asset Picker
+
+
+ image-20240122134039213 +
Texture Asset Picker
+
+
+ diff --git a/docs/interface/intro.md b/docs/interface/intro.md new file mode 100644 index 000000000..ad6f6138e --- /dev/null +++ b/docs/interface/intro.md @@ -0,0 +1,35 @@ +--- +order: 0 +title: 界面总览 +type: 基础知识 +group: 界面 +label: Basics/Interface +--- + +## 首页 + +![image-20240318173506046](https://gw.alipayobjects.com/zos/OasisHub/334d8ca3-639f-4cd9-8aaa-93c1da7acdc3/image-20240318173506046.png) + +| 序号 | 区域 | 说明 | +| ---- | ------------ | ------------------------------------------------------ | +| 1 | **创建项目** | 可以新建一个 3D 项目或者 2D 项目 | +| 2 | **项目** | 可以查看所有的项目,双击可以进入项目 | +| 3 | **侧边栏** | 除了项目页,你还可以获取到项目模板,文档和编辑器讨论区 | + +## 场景编辑页 + +![image-20240318173406939](https://gw.alipayobjects.com/zos/OasisHub/f5b3b853-c5d6-4048-a4de-e18dc69339de/image-20240318173406939.png) + +| 序号 | 区域 | 说明 | +| ---- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | 侧边栏 | 包含了编辑器的主菜单,面板切换按钮以及个性化设置 | +| 2 | [层级面板](/docs/interface-hierarchy) | 位于编辑器左侧,在这里会显示整个场景中的所有节点 | +| 3 | [资产面板](/docs/assets-interface) | 位于编辑器底部,其中会显示当前项目所包含的所有资产,如 HDR 贴图、各种纹理文件、脚本、字体文件等 | +| 4 | [检查器面板](/docs/interface-inspector) | 位于编辑器右侧,会根据你的当前选择二显示不同编辑选项 | +| 5 | [主编辑区](/docs/interface-viewport) | 位于编辑器中间,是编辑器的主要操作区域,可以通过鼠标和键盘来编辑场景 | +| 6 | 工具栏 | 位于编辑器顶部,这里提供了一些快速的操作如切换 Gizmo 的模式、切换场景视角,相机配置等 | +| 7 | 相机预览区 | 位于主编辑区域的左下角,在这里可以以当前选中的相机为视角来预览场景 | +| 8 | [动画片段编辑](/docs/animation-clip) | 双击 AnimationClip 资产或点击**面板切换按钮** -> ,在这里你可以对指定的 AnimationClip 资产进行编辑 | +| 9 | [动画控制器编辑器](/docs/animation-animator) | 双击 AnimatorController 资产或点击**面板切换按钮** -> ,在这里你可以对指定的 AnimationClip 资产进行编辑 | + +对于各个面板详细的介绍可以点击上方链接查看。 diff --git a/docs/interface/menu.md b/docs/interface/menu.md new file mode 100644 index 000000000..4a7b1ecfe --- /dev/null +++ b/docs/interface/menu.md @@ -0,0 +1,36 @@ +--- +order: 1 +title: 主菜单 +type: 基础知识 +group: 界面 +banner: https://gw.alipayobjects.com/zos/OasisHub/adbd922a-f764-4e54-a7e8-891ebd18a074/image-20240319101033970.png +label: Basics/Interface +--- + +项目管理包括新建项目、克隆项目、项目设置等操作。 + +image-20240319100341232 + +### 新建/克隆项目 + +选择 **New Project** 项,可以进一步选择新建不同类型的项目。点击 **Fork**,会跳转到新克隆的项目页面,旧的项目仍会保留。 + +### 项目设置 + +点击 **Project Settings** 项,会出现项目设置弹窗,包含项目重命名、引擎版本管理、快照管理等操作。 + +image-20240319100534596 + +#### 基础设置 + +**Basic** 中包含项目的基础信息设置: + +- **Engine Version**:引擎版本升级,以便快速修复某个 bug 或享受新的功能(注意:引擎版本升级操作是不可逆的。为避免损坏项目,升级引擎过程中会自动克隆一个项目)。 +- **Physics Backend**:物理引擎后端,可以选择 *Physics Lite* 或 *PhysX* 两种后端。前者是一个轻量级的物理引擎,后者是基于 [PhysX](https://developer.nvidia.com/physx-sdk) 的高级物理引擎。 +- **Model Import Options**:模型导入选项,包含计算切线、移除灯光的选项。 + +#### 快照管理 + +**Snapshorts** 快照管理功能允许用户保存某个项目的快照到历史记录中,万一项目出现数据丢失等问题,可以通过 **Revet** 快速恢复到之前保存的某个快照。用户可以在菜单中选择 **Add Snapshot** 。点击快照名可以编辑快照名称,以方便下次快速找到。 + +image-20240319101033970 \ No newline at end of file diff --git a/docs/interface/shortcut.md b/docs/interface/shortcut.md new file mode 100644 index 000000000..73bf3388c --- /dev/null +++ b/docs/interface/shortcut.md @@ -0,0 +1,11 @@ +--- +order: 7 +title: 快捷键 +type: 基础知识 +group: 界面 +label: Basics/Interface +--- + +快捷键有助于提升编辑场景的效率,用户可以在主菜单里 `Shortcuts` 中找到鼠标(或触控板)、键盘的视口控制方式,以及全局和各个面板的快捷键。 + +image-20230925164702582 diff --git a/docs/interface/viewport.md b/docs/interface/viewport.md new file mode 100644 index 000000000..773c8e928 --- /dev/null +++ b/docs/interface/viewport.md @@ -0,0 +1,85 @@ +--- +order: 5 +title: 视图区 +type: 基础知识 +group: 界面 +label: Basics/Interface +--- + +## 简介 + +视图窗口是用于选择、定位、更改当前场景中各种类型实体及组件的交互式界面。 + +drag5 + +## 浏览场景 + +浏览场景有两种方式,分别为标准模式和飞行模式。标准模式是绕着中心视点转,飞行模式适合大型场景浏览,即场景相机在三维空间中前后左右上下移动。 + +| 模式 | 操作 | 快捷键 | +| :----------- | :----------- | ---------------------------------------------------------------------- | +| **标准模式** | 环绕轨道 | `alt` + 鼠标左键 | +| | 平移 | `alt` + `command` + 鼠标左键, 或者 按下鼠标滚轮 | +| | 缩放 | `alt` + `control` + 鼠标左键,或者 滚动鼠标滚轮,或者 触控板上双指轻扫 | +| **飞行模式** | 围绕相机观察 | alt + 鼠标右键 | +| | 前进 | 方向键向上,或者 鼠标右键 + `W` | +| | 后退 | 方向键向下,或者 鼠标右键 + `S` | +| | 左平移 | 方向键向左,或者 鼠标右键 + `A` | +| | 右平移 | 方向键向右,或者 鼠标右键 + `D` | +| | 向上移动 | 鼠标右键 + `E` | +| | 向下移动 | 鼠标右键 + `Q` | +| | 改变飞行速度 | 鼠标右键 + 鼠标滚轮 | + +## 工具栏 + +工具栏位于视图窗口中上,鼠标停留会出现每一项的快捷键,或者内容说明。 + +image-20240131181207870 + +| 图标 | 名称 | 解释 | 快捷键 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| image-20240131175906508 | 拖动 | 拖动画面 | | +| image-20240131180117064
image-20240131180217044
image-20240131180256738 | 移动
旋转
缩放 | 对选中实体进行变换操作 | `W`
`E`
`R` | +| image-20240131180403373
image-20240131180513384 | 中心锚点/枢纽锚点 | 切换选中实体的锚点 | | +| image-20240131180709163
image-20240131180731465 | 本地坐标/世界坐标 | 切换选中实体的坐标 | | +| image-20240131181105676 | 聚焦 | 场景相机聚焦到的选中实体 | `F` | +| image-20240131181429677 | 场景相机 | 场景相机菜单包含用于配置场景相机的选项,主要用来解决搭建场景时,裁剪面太远或者太近导致看不到物体的问题。这些调整不会影响场景中带有相机组件的实体的设置。 | | +| image-20240131181144755 | 设置 | 设置菜单包含用于调整视图辅助显示的选项,包括网格、辅助图标(与场景中特定组件相关联的图形,包括相机、直射光、点光源、聚光灯)、 辅助线框 | | +| image-20240131181242445
image-20240131181524219 | 场景相机类型 | 切换透视/正交相机 | | +| image-20240131181459432
image-20240131181607999 | 模式 | 方便在 2D/3D 场景模式间进行点击切换。2D 模式下,导航部件、正交/透视切换关闭,导航中的环绕轨道不再生效。 | | +| image-20240131182235406 | 全屏/复原 | 最大化视图窗口,最小化层级、资产、检查器 | | +| image-20240131182303867 | 截屏 | 对当前场景进行快照。仅显示场景内用户创建实体,辅助显示的一系列工具,如图标、网格、gizmo 不会被计入其中。进行截屏后,该快照会在首页作为该项目缩略图。 | | + +### 辅助元素设置界面 + +image-20240131181825989 + +| 属性 | 内容 | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 网格(Grid) | 视图中的网格是否显示 | +| 3D 图标(3D Icons) | 辅助图标是否根据组件与摄像机的距离进行缩放 | +| 导航 Gimzo | 用于显示场景相机的当前方向,并且可以通过鼠标操作快速修改视角和投影模式(正交/透视)。打开后会在画面右下角显示。
image-20240131184405058 | +| 轮廓显示(Outline) | 选中某个实体时是否显示轮廓,选中的实体轮廓颜色为橙色,子节点轮廓为蓝色 | +| 相机(Camera) | 以锥体显示选中相机组件 | +| 光源(Light) | 显示光源组件 | +| 静态碰撞体(Static Collider) | 显示静态碰撞体形状 | +| 动态碰撞体(Dynamic Collider) | 显示动态碰撞体形状 | +| 发射器形状(Emission Shape) | 显示粒子发射器形状 | + +### 场景相机设置界面 + +image-20240131185805023 + +| 属性 | 内容 | 默认值 | +| :------------------- | :----------------------------------------------------------- | :------------------- | +| 视角(Fov) | 场景相机的视角 | 60 | +| 动态裁剪(Dynamic) | 相对选中实体和场景相机位置,自动计算场景相机的近裁面和远裁面 | 关闭 | +| 近裁面(Near Plane) | 手动调整相对于场景相机的最近点 | 不勾选动态裁剪后开启 | +| 远裁面(Far Plane) | 手动调整相对于场景相机的最远点 | 不勾选动态裁剪后开启 | +| 速度(Speed) | 飞行模式下相机的移动速度 | 10 | + +## 预览 + +选中带有相机组件的实体时,会在视图窗口左下角显示该相机的实时预览。帮助用户实时调整相机、场景位置。预览窗口可以拖动、锁定,并且切换不同设备比例的窗口。 + +image-20240131190013320 diff --git a/docs/performance/scene-standard.md b/docs/performance/scene-standard.md new file mode 100644 index 000000000..a3eb453ca --- /dev/null +++ b/docs/performance/scene-standard.md @@ -0,0 +1,39 @@ +--- +order: 1 +title: 场景规格指南 +type: 性能 +label: Performance +--- + +Galacean Engine 支持主流的 3D 建模软件(C4D、3ds Max、Maya、Blender)等导出 *.fbx* 文件。考虑到运行时的性能和兼容性问题,请美术注意 3D 场景规格: + +### 模型 + +- **三角面和顶点数量:** 建议单个场景模型面数不要超过 **10万面**。在保证视觉效果的前提下尽量减少模型三角面数量和顶点数量,因为两者对 _GPU_ 的性能损耗较大或显存占用均有一定影响,尤其是三角面的渲染性能有极大影响。 +- **模型合并:** 美术需将不可独立移动的模型尽可能合并减少渲染批次,同时注意不要合并场景范围跨度过大的模型导致模型无法裁剪的问题。 + +### 材质 + +- **材质合并:** 尽可能合并材质,材质作为三维引擎的合并根基,一切引擎级渲染批次的合并前提都是使用相同材质,所以要保持材质对象尽可能的少。 +- **材质选择:** + - 材质模型选择需要根据美术风格尽量精简,比如直接把光照合并在漫反射贴图的的卡通风格模型可以直接选择 _unlit_ 材质,而无需使用复杂的 _PBR_ 材质模型。 + - 优先使用非透明材质,因为无论材质的透明混合还是透明裁剪模式相对于非透明材质都比较耗费性能。 + +### 贴图 + +贴图是占用显存资源的大头,贴图尺寸不可能盲目追求质量使用超大尺寸,需要评估实际项目贴图光栅化后的实际显示像素来使用接近的贴图尺寸,否则使用过大尺寸不仅得不到效果收益还浪费显存,尽量使用 2 的 N 次方贴图。在合理贴图尺寸下还可以继续使用 [纹理压缩](/docs/graphics-texture-compression) 优化显存占用。 + +### 节点 + +减少运行时空节点数量,空节点数量会占据一定内存消耗,而且可能会带来 [变换](/docs/core-transform) 计算的潜在消耗,美术方一定要尽量删除空节点和合并碎节点。 + +### 动画 +动画的制作方式上建议使用骨骼蒙皮动画,这是一种在三维引擎里兼顾效果和内存的一种动画技术,但由于骨骼动画的计算开销较大,尤其是在 JS 这种不擅长密集运算的语言下。所以美术在制作骨骼动画时应保证骨骼数量尽可能的少,有助于提升骨骼动画的性能和内存占用。一般控制在 **25** 块以下,可以保证在 IPhone 这种 GPU _uniform_ 数量较少的机型中保证最佳性能。 + + +### UI +减少 UI 的 Alpha 部分浪费,比如 UI 使用近乎全屏但大部分透明的图片绘制会给 GPU 带来巨大的渲染负担,另外美术尽量自行合并 UI 贴图并高度利用贴图空间,因为依靠编辑器的算法合并仍可能产生一些浪费。 + + +### 特效 +特效贴图部分和 UI 同理,**一定要减少贴图透明部分的尺寸的浪费**,另外由于特效通常 OverDraw 非常严重,比如粒子,所以一定要在粒子等特效上尽量减少发射频率。 \ No newline at end of file diff --git a/docs/performance/stats.md b/docs/performance/stats.md new file mode 100644 index 000000000..e86a91cdf --- /dev/null +++ b/docs/performance/stats.md @@ -0,0 +1,20 @@ +--- +order: 2 +title: 统计面板 +type: 性能 +label: Performance +--- + +[@galacean/engine-toolkit-stats](https://www.npmjs.com/package/@galacean/engine-toolkit-stats) 包主要用来显示相机的渲染状态,只需要为相机节点添加 `Stats` 组件: + +```typescript +import { Engine } from "@galacean/engine"; +import { Stats } from "@galacean/engine-toolkit-stats"; + +cameraEntity.addComponent(Camera); +cameraEntity.addComponent(Stats); +``` + +## 示例 + + diff --git a/docs/physics/collider.md b/docs/physics/collider.md new file mode 100644 index 000000000..a826b1749 --- /dev/null +++ b/docs/physics/collider.md @@ -0,0 +1,92 @@ +--- +order: 3 +title: 碰撞器组件 +type: 物理 +label: Physics +--- + +引入物理引擎的最大好处是使得场景中的物体拥有了物理响应。碰撞器在引擎中属于组件。在使用前,我们需要先了解下碰撞器的类型: + +1. [StaticCollider](/apis/core/#StaticCollider):静态碰撞器,主要用于场景中静止的物体; +2. [DynamicCollider](/apis/core/#DynamicCollider):动态碰撞器,用于场景中需要受到脚本控制,或者响应物理反馈的物体。 + +## 编辑器使用 + +### 添加碰撞器组件 + +首先需要考虑的是,碰撞器是静态的还是动态的,然后添加对应的碰撞器组件,静态碰撞器 StaticCollider 或者 动态 DynamicCollider + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*-E4USbdiH6sAAAAAAAAAAAAADsJ_AQ/original) + +### 选择碰撞器的外形 + +事实上,每一种 `Collider` 都是 [ColliderShape](/apis/core/#ColliderShape) 的集合,即每一种 `Collider` 都可以通过组合 `ColliderShape` 设置复合的碰撞器外形。 + +目前支持了四种 `ColliderShape`,但不同的后端物理包支持程度不同,具体如下: + +| 名称 | 解释 | 支持的后端物理包 | +| :--- |:---------|:----------------------------| +| [BoxColliderShape](/apis/core/#BoxColliderShape) | 盒形碰撞外形 | physics-lite, physics-physx | +| [SphereColliderShape](/apis/core/#SphereColliderShape) | 球形碰撞外形 | physics-lite, physics-physx | +| [PlaneColliderShape](/apis/core/#PlaneColliderShape) | 无界平面碰撞外形 | physics-physx | +| [CapsuleColliderShape](/apis/core/#CapsuleColliderShape) | 胶囊碰撞外形 | physics-physx | + +引擎支持复合的碰撞器外形,也就是说,碰撞器本身可以由 BoxColliderShape,SphereColliderShape,CapsuleColliderShape 复合而成。 + +这里特别强调的是 `Collider` 与 `ColliderShape` 的位置关系。每一个 `Collider` 的姿态和其挂载的 `Entity` 是一致的,每一帧两者都会进行同步。而 `ColliderShape` 上则可以通过 `position` 属性设置 **相对于** `Collider` 的偏移。 + +![table](https://mdn.alipayobjects.com/huamei_vvspai/afts/img/A*erlGRKk7dNMAAAAAAAAAAAAADsqFAQ/original) + +在加入碰撞器组件后,不会默认添加碰撞器外形,因此需要点击 Add Item 进行添加,添加后会在视口中看到碰撞器的辅助渲染出现。 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*OUr-SIejEkoAAAAAAAAAAAAADsJ_AQ/original) + +对于每一个碰撞器外形,都可以设计对应的一些大小属性。例如 + +alt text + +但无论那个碰撞器外形,都可以设置 Local Position,即相对于 Entity 坐标的局部偏移 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*p8UcRJ9Q0EIAAAAAAAAAAAAADsJ_AQ/original) + +### 动态碰撞器设置 +和静态碰撞器不同,动态碰撞器会受到物理规律的作用,因此有许多附加的物理属性进行设置 + +alt text + +在修改这些参数后,视口不会发生变化,因为动态碰撞器默认会受到重力的作用,因此需要在 Play 模式下才能进行观察。 + +### 注意 +- 确定的碰撞区域应尽量简单,以提高物理引擎检测的性能 +- 碰撞器的参照坐标系为从属 Entity 的坐标系 +- PlaneColliderShape 表示全平面,因此没有辅助线的显示,一般作为地板使用 + +## 脚本使用 + +物理响应分为两种类型: + +1. 触发器模式:物体不具备刚体外形,但发生接触时可以触发特定的脚本函数。 +2. 碰撞器模式:物理具有刚体外形,发生接触时不仅可以触发脚本函数,还可以根据物理规律改变原先的运动。 + +针对这两种类型,脚本中都提供了对应的函数,并且碰撞器组件也提供了一系列设置自身状态的函数,例如速度,质量等等。 + +### 触发器脚本函数 + +对于触发器模式,首先需要给场景中的 `Entity` 添加 `Collider`;该当这些组件相互接触时,会自动触发脚本组件当中的三个函数: + +1. [onTriggerEnter](/docs/script#$1-ontriggerenter):相互接触时调用 +2. [onTriggerStay](/docs/script#$1-ontriggerstay):接触过程中*循环*调用 +3. [onTriggerExit](/docs/script#$1-ontriggerexit):接触结束时调用 + +可以通过 `ColliderShape` 上的 `isTrigger` 开启触发器模式,但需要特别强调的是,**两个 StaticCollider 之间不会调用触发器事件**,除非其中一个是 `DynamicCollider`。 + + + +### 碰撞器脚本函数 + +对于碰撞器模式,`DynamicCollider` 相互作用时会触发三个碰撞相关的脚本函数: +1. [onCollisionEnter](/docs/script#$1-oncollisionenter):碰撞触发时调用 +2. [onCollisionStay](/docs/script#$1-oncollisionstay):碰撞过程中*循环*调用 +3. [onCollisionExit](/docs/script#$1-oncollisionexit):碰撞结束时调用 + + diff --git a/docs/physics/controller.md b/docs/physics/controller.md new file mode 100644 index 000000000..d47aeea92 --- /dev/null +++ b/docs/physics/controller.md @@ -0,0 +1,49 @@ +--- +order: 4 +title: 角色控制器组件 +type: 物理 +label: Physics +--- + +角色控制器是物理引擎提供的一种非常重要的功能组件,通过角色控制器可以很容易为动画角色的运动增加物理上的表现,例如可以设定参数使得角色无法爬过一定角度的陡坡, +也可以在角色运动的过程中避免角色与其他碰撞器发生碰撞反馈等等。实际上,角色控制器只是碰撞器的一种高级封装,通过碰撞检测来实现各种高级的的角色控制行为。 +也正因此,角色控制器组件的创建与使用,和碰撞器组件非常类似。 +```typescript +const physicsCapsule = new CapsuleColliderShape(); +physicsCapsule.radius = radius; +physicsCapsule.height = height; +const characterController = capsuleEntity.addComponent(CharacterController); +characterController.addShape(physicsCapsule); +``` +和碰撞器组件一样,都是通过构造 `ColliderShape`,并且将其添加到组件中,使得角色控制器获得特定的外形。但这里需要特别强调两点: +1. 角色控制器不支持复合外形,因此只能添加一个 `ColliderShape`。 +2. 角色控制器目前只支持 `CapsuleColliderShape` 和 `BoxColliderShape`,且其中 `CapsuleColliderShape` 最为常用。 + +后续角色控制器的行为通过 `CharacterController` 的各个参数和方法进行控制,其中最重要的是 `move` 函数: +```typescript +class Controller extends Script { + onPhysicsUpdate() { + const fixedTimeStep = this.engine.physicsManager.fixedTimeStep; + const character = this._character; + const flag = character.move(this._displacement, 0.1, fixedTimeStep); + if (flag | ControllerCollisionFlag.Down) { + character.move(new Vector3(0, -0.2, 0), 0.1, fixedTimeStep); + } + this._displacement.setValue(0, 0, 0); + } +} +```` + +可以在 `move` 方法中指定角色位移,并且该方法返回一个枚举类型的复合值,通过该枚举类型 `ControllerCollisionFlag` 可以判断角色控制器是否碰到其他的碰撞器组件: +```typescript +export enum ControllerCollisionFlag { + /** Character is colliding to the sides. */ + Sides = 1, + /** Character has collision above. */ + Up = 2, + /** Character has collision below. */ + Down = 4 +} +``` +由此角色接下来的动画和运动要怎么进行。在下面的例子当中,可以通过键盘控制角色的运动,使其爬上或者跳过特定的障碍物。 + diff --git a/docs/physics/debug.md b/docs/physics/debug.md new file mode 100644 index 000000000..69cb2861e --- /dev/null +++ b/docs/physics/debug.md @@ -0,0 +1,18 @@ +--- +order: 6 +title: 物理调试 +type: 物理 +label: Physics +--- + +物理碰撞器由基础物理外形复合而成,包括了球,盒,胶囊和无限大的平面。在实际应用中,这些碰撞器外形很少与渲染的物体刚好是完全重合的,这为可视化调试带来了很大的困难。 +有两种调试方法: +1. 借助 PhysX Visual Debugger(PVD),是 Nvidia 官方开发的调试工具,但是用这一工具需要自行编译 debug 版本的PhysX,并且借助 WebSocket 串联浏览器和该调试工具。 +具体的使用方法,可以参考 [physx.js](https://github.com/galacean/physX.js) 的Readme种的介绍。 +2. 我们还提供了轻量级的[辅助线工具](https://github.com/galacean/engine-toolkit/tree/main/packages/auxiliary-lines),该工具根据物理组件的配置绘制对应的线框,辅助配置和调试物理组件。 +使用起来也非常容易,只需要在挂载 `WireframeManager` 脚本,然后设置其关联各种物理组件,或者直接关联节点即可: +```typescript +const wireframe = rootEntity.addComponent(WireframeManager); +wireframe.addCollideWireframe(collider); +``` + diff --git a/docs/physics/joint-basic.md b/docs/physics/joint-basic.md new file mode 100644 index 000000000..40c4d7443 --- /dev/null +++ b/docs/physics/joint-basic.md @@ -0,0 +1,43 @@ +--- +order: 5 +title: 基础物理约束组件 +type: 物理 +label: Physics +--- + +物理约束组件是一种非常重要的物理组件,通过约束可以更好控制动态碰撞器组件的运动,为场景添加有趣的交互响应。本文主要介绍最基础的三种物理约束组件: + +1. 固定约束组件 + + ![fixedJoint](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/_images/fixedJoint.png) +2. 弹性约束组件 + + ![springJoint](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/_images/distanceJoint.png) +3. 铰链约束组件 + + ![hingeJoint](https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/_images/revoluteJoint.png) + +所有的物理约束都有两个作用对象,其中代表受到物理约束作用的动态碰撞器(在该节点上挂载物理约束组件),另外一个是约束挂载的位置或者是另外一个动态碰撞器(通过组件配置来设置)。 +因此,这些组件的使用方法类似,以固定约束组件`FixedJoint`为例: + +```typescript +const fixedJoint = currentEntity.addComponent(FixedJoint); +fixedJoint.connectedCollider = prevCollider; +``` + +## 局部坐标与世界坐标 + +理解物理约束组件的使用,其中一个关键点就是理解**局部坐标**和**世界坐标**。所有的物理约束,都可以配置 `connectedCollider` 属性。 +此外,某些物理约束组件还可以通过配置 `connectedAnchor` 属性,设置物理约束挂载的位置。 + +**需要特别注意的是,当 `connectedCollider` 被设置后,`connectedAnchor` 代表的是相对于该碰撞器的局部坐标。`connectedCollider` 为 null 时, +`connectedAnchor` 代表的是世界坐标。** + +## 铰链约束 + +以上三种物理约束中,相对比较复杂的是铰链约束,因为除了需要配置 `connectedCollider` 和 `connectedAnchor` 之外,还需要指定铰链的旋转轴方向和旋转半径。 +可以通过配置 `axis` (默认方向是朝向 x 轴正方向)和 `swingOffset` 指定这两个属性。 +其中 `swingOffset` 也是一个向量,可以理解成从 `connectedAnchor` 和 `connectedCollider` 共同确定的旋转中心出发的偏移,动态碰撞就被挪到该点开始绕着旋转轴旋转。 + +上述物理约束组件的使用,可以参照: + diff --git a/docs/physics/manager.md b/docs/physics/manager.md new file mode 100644 index 000000000..aed57175d --- /dev/null +++ b/docs/physics/manager.md @@ -0,0 +1,85 @@ +--- +order: 2 +title: 物理管理器 +type: 物理 +label: Physics +--- + +物理管理器(PhysicsManager)用于管理场景中所有的物理组件,并且负责与物理后端通信,实现有关物理场景的全局操作,例如更新和射线检测等等。在多场景项目中,每个Scene都有自己的PhysicsManager,Scene之间的物理系统是相互隔离互不影响的。 + +## 物理更新 + +物理场景和渲染场景相互独立,但在程序运行过程中不断同步各自的数据。因此,和脚本一样,同步的时序也非常重要。一般来说,物理场景的更新频率和渲染场景不同,在物理管理器中可以对其进行设置: + +```typescript +/** The fixed time step in seconds at which physics are performed. */ +fixedTimeStep: number = 1 / 60; + +/** The max sum of time step in seconds one frame. */ +maxSumTimeStep: number = 1 / 3; +``` + +每一个渲染帧中,物理引擎都会按照固定时间步长进行更新 `fixedTimeStep`。 + +如果时时间间隔大于 `fixedTimeStep`,则单步模拟的最大时间步长由 `maxSumTimeStep` 确定。此时,如果按照上面列出的默认参数,有可能会发生追帧现象。 +这时候可以通过调节 `maxSumTimeStep` 降低每帧物理模拟的更新次数。 + +如果不满一个 `fixedTimeStep`,则顺延到下一帧再处理。因此,每一个渲染帧,物理场景可能会更新多次,也可能只更新一次,因此对于有关物理组件更新,都需要放在特定的更新函数,`Script` +提供了这一接口: + +```typescript +export class Script extends Component { + /** + * Called before physics calculations, the number of times is related to the physical update frequency. + */ + onPhysicsUpdate(): void { + } +} +``` + +物理场景在更新时,除了调用该函数,还会同步 Collider 和其所挂载的 Entity 的姿态。物理更新的时序如下: + +1. 调用 `onPhysicsUpdate` 中的用户逻辑 +2. `callColliderOnUpdate` 将被修改的 Entity 新姿态同步给物理碰撞器 +3. 更新物理场景 +4. `callColliderOnLateUpdate` 将所有 DynamicCollider 更新后的位置同步给对应的 Entity + +## 使用射线检测 + + + +射线可以理解成 3D 世界中一个点向一个方向发射的一条无终点的线。射线投射在 3D 应用中非常广泛。通过射线投射,可以在用户点击屏幕时,拾取 3D 场景中的物体;也可以在射击游戏中,判断子弹能否射中目标。 + +![image.png](https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*SHM1RI49Bd4AAAAAAAAAAAAAARQnAQ) +(_图片来源于网络_) + +在使用射线投射,首先要在代码中引入 [Ray](/apis/math/#Ray) 模块;然后生成射线,射线可以自定义生成,也可以通过相机([camera](/apis/core/#Camera-viewportPointToRay) +)将屏幕输入转化成射线;最后调用 [PhysicsManager.raycast](/apis/core/#PhysicsManager-raycast) 方法即可检测射线投射命中的碰撞体。代码如下: + +```typescript +// 加载 Raycast 模块 +import {WebGLEngine, HitResult, Ray} from "@galacean/engine"; +import {LitePhysics} from "@galacean/engine-physics-lite"; + +const engine = await WebGLEngine.create({ + canvas: "canvas", + physics: new LitePhysics(), +}); +engine.canvas.resizeByClientSize(); + +// 将屏幕输入转换成Ray +document.getElementById('canvas').addEventListener('click', (e) => { + const ratio = window.devicePixelRatio; + let ray = new Ray(); + camera.screenPointToRay(new Vector2(e.offsetX, e.offsetY).scale(ratio), ray); + const hit = new HitResult(); + result = engine.physicsManager.raycast(ray, Number.MAX_VALUE, Layer.Everything, hit); + if (result) { + console.log(hit.entity.name); + } +}); +``` + +需要特别指出,如果想要对 Entity 启用射线投射,该 Entity 就必须拥有 **Collider** ,否则无法触发。若射线命中的Collider的Shape距离相同,则返回先被添加的Shape(举个例子:有两个Collider相同的Entity完全重合,则会返回先添加Collider,更准确的说是先添加Shape的Entity)。 + +同时,在 Galacean 当中,还提供了 InputManager,该管理器将输入源做了封装,提供了更加易用的逻辑,使用方式可以[参考](/docs/input) . diff --git a/docs/physics/overall.md b/docs/physics/overall.md new file mode 100644 index 000000000..a2a7a8ecd --- /dev/null +++ b/docs/physics/overall.md @@ -0,0 +1,49 @@ +--- +order: 1 +title: 物理总览 +type: 物理 +label: Physics +--- + +物理引擎是游戏引擎中非常重要的组成部分。 业界普遍采用 PhysX 引入相关功能。 但是对于轻量级的场景,PhysX 使得最终的应用体积非常大,超出了这些项目的限制。 Galacean 基于多后端设计。 一方面,它通过 WebAssembly +编译得到 [PhysX.js](https://github.com/galacean/physX.js) ; 另一方面,它也提供了轻量级的物理引擎。 +两者在 [API](https://github.com/galacean/engine/tree/main/packages/design/src/physics) 设计上是一致的。 用户只需要在初始化引擎时选择特定的物理后端。 +可以满足轻量级应用、重量级游戏等各种场景的需求。有关物理组件的总体设计,可以参考 [Wiki](https://github.com/galacean/engine/wiki/Physical-system-design). + +对于需要使用各种物理组件,以及 `InputManager` 等需要 Raycast 拾取的场景,都需要在使用之前初始化物理引擎。目前 Galacean 引擎提供两种内置的物理引擎后端实现: + + - [physics-lite](https://github.com/galacean/engine/tree/main/packages/physics-lite) + - [physics-physx](https://github.com/galacean/engine/tree/main/packages/physics-physx) + +开发者可以在 [主菜单](/docs/interface-menu) 界面打开的 **项目设置** 面板中设置物理后端。 + +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*LO_FRIsaIzIAAAAAAAAAAAAADsJ_AQ/original) +![alt text](https://mdn.alipayobjects.com/huamei_3zduhr/afts/img/A*ZvWdQqEfIKoAAAAAAAAAAAAADsJ_AQ/original) + +若通过脚本初始化引擎,只需要将物理后端对象传入 `Engine` 中即可: + +```typescript +import {LitePhysics} from "@galacean/engine-physics-lite"; + +const engine = await WebGLEngine.create({ + canvas: htmlCanvas, + physics: new LitePhysics(), +}); +``` + +## PhysX 版物理引擎加载与初始化 + +```typescript +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +const engine = await WebGLEngine.create({ + canvas: htmlCanvas, + physics: new PhysXPhysics(), +}); +``` + +## 选择物理后端 +选择物理后端需要考虑到功能,性能和包尺寸这三个因素: +1. 功能:追求完整物理引擎功能以及高性能的物理模拟,推荐选择 PhysX 后端,Lite 后端只支持碰撞检测。 +2. 性能:PhysX 会在不支持 WebAssembly 的平台自动降级为纯 JavaScript 的代码,因此性能也会随之降低。但由于内置了用于场景搜索的数据结构,性能比 Lite 后端还是要更加好。 +3. 包尺寸:选择 PhysX 后端会额外引入接近 2.5mb 的 wasm 文件(纯 JavaScript 版的大小接近),增加包的大小的同时降低应用初始化的速度。 diff --git a/docs/quick-start/core-concept.md b/docs/quick-start/core-concept.md new file mode 100644 index 000000000..fd742055c --- /dev/null +++ b/docs/quick-start/core-concept.md @@ -0,0 +1,205 @@ +--- +order: 1 +title: 核心概念 +type: 基础知识 +group: 快速上手 +label: Basics/GettingStarted +--- + +我们通过一个立方体的例子,来了解一下编辑器和运行时中的核心概念。 + +## 编辑器使用 + +### 创建项目 + +在你登录之后,首先看到的是编辑器的首页,在这个页面中会显示所有你创建的项目。使用右上角的按钮来创建项目,点击后可以选择要创建的项目类型,2D 或 3D。我们选择 3D Project。 + +image-20230921161225962 + +### 创建立方体 + +首先,我们在 **层级面板** 中创建一个新的实体([什么是实体?](https://galacean.antgroup.com/#/docs/latest/cn/entity))。 + +image-20230921161422138 + +我们用鼠标左键选中新建的实体节点,此时右侧的 **[检查器面板](/docs/interface-inspector)** 会显示出当前实体的一些可配置属性。因为我们的实体现在没有绑定任何组件([什么是组件?](https://galacean.antgroup.com/#/docs/latest/cn/entity)),所以我们暂时只能调整实体的坐标信息这类的基础属性。 + +接下来,我们点击 **[检查器面板](/docs/interface-inspector)** 中的 `Add Component` 按钮唤起组件选单,然后选择添加 `Mesh Renderer` 组件(什么是 [Mesh Renderer?](/docs/graphics-renderer-meshRenderer))。 + +image-20230921161622497 + +这样,我们就给当前的实体新增了一个 `Mesh Renderer` 组件。但我们在主编辑区还看不到这个物体。需要为该组件添加 Mesh 和 Material 才行。编辑器会默认为 `Mesh Renderer` 组件添加一个不可编辑的默认材质,我们只需要为组件的 Mesh 属性添加一个 Cuboid Mesh 就可以在场景中看到它了。 + +image-20230921161758541 + +默认的材质比较简单,所以接下来,我们来创建一个自定义的材质。 + +你也可以通过添加实体按钮中的 `3D Object` → `Cuboid` 来快速添加一个立方体模型,它会自动帮你添加一个 `Mesh Renderer` 组件: + + + +### 创建材质 + +首先,我们来上传纹理。我们可以把这些纹理文件直接拖动到 **资产管理面板,** 既可批量上传这些文件。 + +上传后,我们可以在面板中看到这些文件,依次是粗糙度纹理、法线纹理、基础颜色纹理。 + +image-20230921172453377 + +我们首先在 **资产管理面板** 中依次选择 `右键` → `Create` → `Material` 让编辑器会创建出一个默认的 PBR 材质。我们选中这个材质,此时 **[检查器面板](/docs/interface-inspector)** 会显示当前材质的配置选项。默认的材质比较简单,我们可以为这个材质增加一些纹理贴图,如基础纹理、粗糙度纹理、法线贴图。 + +image-20230921173056885 + +接下来,我们把这些贴图配置到材质的对应属性当中。配置后我们再次选择上一步创建的实体节点,将 `Mesh Renderer` 组件的 `Material` 属性修改为我们刚刚创建的自定义材质。一个拥有金属质感的立方体就创建成功了。 + +Untitled + +只不过,立方体现在看上去有点暗,需要把场景中的 [灯光](https://galacean.antgroup.com/#/docs/latest/cn/light) 调亮一点。我们在节点树中选择 `DirectLight` 节点,然后在检查器中调高 `Intensity`(光强度)属性。 + +现在看上去就比较正常了。 +Untitled + +### 创建脚本 + +接下来,我们为这个节点再绑定一个 `Script` 组件([什么是 Script 组件?](https://galacean.antgroup.com/#/docs/latest/cn/script))。 + +1. 我们继续使用上述方式在 **[检查器面板](/docs/interface-inspector)** 中添加 `Script` 组件 +2. 接下来,我们在 **[资产面板](/docs/assets-interface)** 中 `右键` → `Create` → `Script` 创建一个 `Script` 资产 +3. 最后,在 **[检查器面板](/docs/interface-inspector)** 中将刚创建的脚本文件绑定到脚本组件上 + +> ⚠️ 注意,如果你没有把脚本资产绑定到实体的脚本组件上,则脚本不会运行 + +创建脚本后,我们可以 **双击它** 来跳转到代码编辑器页面。 + +image-20230921180953712 + +进入代码编辑器后,我们写一个非常简单的旋转功能: + +```ts +// Script.ts +import { Script } from "@galacean/engine"; + +export default class extends Script { + onUpdate(deltaTime: number) { + this.entity.transform.rotate(1, 1, 1); + } +} +``` + +在写好代码后,保存(`⌘+s`), 右侧预览区就可以实时的看到整个场景的效果。 + +### 导出项目 + +现在,我们已经完成了在编辑器中的基础开发工作,接下来我们来导出这个项目到本地。 + +我们点击左侧工具栏的 **下载** 按钮,会唤起导出界面,我们这里把项目名改为 “box”,然后点击 `Download` 按钮,编辑器就会把项目打包为一个 `box.zip` 文件下载。 + +image-20230921162204014 + +项目打包完成后,我们使用 VsCode 打开 box 项目,运行 `npm install` & `npm run dev` ,可以看到项目已经能够正常运行了。 + +## 脚本使用 + + + +## 引入模块 + +我们开始使用 [TypeScript](https://www.typescriptlang.org/) 编写引擎代码。如果你还不太适应 TypeScript,使用 JavaScript 也一样可以运行,并且同样可以享受到引擎 API 提示(通过使用 [VSCode](https://code.visualstudio.com/) 等 IDE 进行编程)。 + +回到我们的编程,为了实现这样一个功能,需要在我们的工程里引入如下 Galacean 引擎的类: + +```typescript +import { + WebGLEngine, + Camera, + MeshRenderer, + PrimitiveMesh, + BlinnPhongMaterial, + DirectLight, + Script, + Vector3, + Vector4, + Color, +} from "@galacean/engine"; +``` + +我们先来简单认识一下这些类: + +| 类型 | 类名 | 释义 | +| -------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| WebGL 引擎类 | [WebGLEngine](${api}rhi-webgl/WebGLEngine) | WebGL 平台引擎,支持 WebGL1.0 和 WebGL2.0,它能够控制画布的一切行为,包括资源管理、场景管理、执行/暂停/继续、垂直同步等功能。(详见 [引擎](/docs/core-engine) 章节。) | +| 组件类 | [Camera](/apis/core/#Camera) | 相机,是一个图形引擎对 3D 投影的抽象概念,作用好比现实世界中的摄像机或眼睛,如果不加相机,画布将什么都画不出来。(详见 [相机](/docs/graphics-camera) 章节) | +| | [DirectLight](/apis/core/#DirectLight) | 直接光,是光照的一种,光照使场景更有层次感,使用光照,能建立更真实的三维场景。(详见 [光照](/docs/graphics-light) 章节) | +| | [Script](/apis/core/#Script) | 脚本,是衔接引擎能力和游戏逻辑的纽带,可以通过它来扩展引擎的功能,也可以脚本组件提供的生命周期钩子函数中编写自己的游戏逻辑代码。(详见 [脚本](/docs/script) 章节) | +| | [MeshRenderer](/apis/core/#MeshRenderer) | 网格渲染器,使用网格对象(这个例子中就是立方体)作为几何体轮廓的数据源 | +| 几何体和材质类 | [PrimitiveMesh](/apis/core/#PrimitiveMesh) | 基础几何体,提供了创建立方体、球体等网格对象的便捷方法。(详见 [内置几何体](/docs/graphics-model) 章节) | +| | [BlinnPhongMaterial](/apis/core/#BlinnPhongMaterial) | 材质定义了如何渲染这个立方体,BlinnPhong 材质是经典的材质之一。(详见 [材质](/docs/graphics-material) 章节) | +| 数学库相关类 | [Vector3](/apis/math/#Vector3), [Vector4](/apis/math/#Vector4), [Color](/apis/math/#Color) | 这几个类是数学计算的一些基本单元,用来计算立方体的位置、颜色等。(详见 [数学库](/docs/core-math) 章节) | + +## 创建引擎实例 + +创建引擎实例,参数 `canvas` 是 _Canvas_ 元素的 `id`,若 `id` 不同请自行替换。如上文所述,通过 [resizeByClientSize](${api}rhi-webgl/WebCanvas#resizeByClientSize) 方法重设画布高宽。 + +```typescript +const engine = await WebGLEngine.create({ canvas: "canvas" }); +engine.canvas.resizeByClientSize(); +``` + +## 创建场景根节点 + +值得注意的是,一个引擎实例可能包含多个场景实例,如果为了在当前激活的场景中添加一个立方体,需要通过引擎的场景管理器 `engine.sceneManager` 获得当前激活的场景。 + +获得场景后,通过场景的 `createRootEntity` 方法创建一个**根实体**。场景中的根实体是场景树的根节点。 + +```typescript +const scene = engine.sceneManager.activeScene; +const rootEntity = scene.createRootEntity("root"); +``` + +## 创建一个相机实体 + +在 Galacean Engine 中,功能是以组件形式添加到实体上的。首先,我们先创建一个实体用来添加相机组件。 + +创建完成之后,通过实体上自带的变换组件 `transform` 来改变相机的位置和朝向。然后给这个实体添加相机组件 `Camera`。 + +```typescript +let cameraEntity = rootEntity.createChild("camera_entity"); + +cameraEntity.transform.position = new Vector3(0, 5, 10); +cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + +let camera = cameraEntity.addComponent(Camera); +``` + +## 创建光照 + +同样的,光照也是通过组件形式挂载到实体上。创建完实体之后,添加直接光组件 `DirectLight`,设置直接光组件的颜色、强度属性和光照角度来获得合适的光照效果。 + +```typescript +let lightEntity = rootEntity.createChild("light"); + +let directLight = lightEntity.addComponent(DirectLight); +directLight.color = new Color(1.0, 1.0, 1.0); +directLight.intensity = 0.5; + +lightEntity.transform.rotation = new Vector3(45, 45, 45); +``` + +## 创建立方体 + +再创建一个实体用来挂载立方体网格渲染组件。`MeshRenderer` 是网格渲染器组件,通过 `.mesh` 属性设置成 `PrimitiveMesh` 创建的立方体数据,通过 `setMaterial` 方法把立方体的材质设置成 BlinnPhong。 + +```typescript +let cubeEntity = rootEntity.createChild("cube"); +let cube = cubeEntity.addComponent(MeshRenderer); +cube.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); +cube.setMaterial(new BlinnPhongMaterial(engine)); +``` + +## 启动引擎 + +一切都准备好了,让我们用一行代码来启动引擎吧! + +```typescript +engine.run(); +``` diff --git a/docs/quick-start/flappy-bird.md b/docs/quick-start/flappy-bird.md new file mode 100644 index 000000000..5dc084df0 --- /dev/null +++ b/docs/quick-start/flappy-bird.md @@ -0,0 +1,781 @@ +--- +order: 2 +title: 像素小鸟 +type: 基础知识 +group: 快速上手 +label: Basics/GettingStarted +--- + +> 相信大家对 Flappy Bird 都不陌生,本文简单描述下如何用 Galacean 复刻这个 2D 游戏。 +> +> 原游戏链接:[http://flappybird.io/](http://flappybird.io/) + +Flappy Bird 是一个 2D 项目,编辑器首页自带的 2D 模版便是按照此文档一步一步实现的,我们先通过编辑器的 `New Project` 创建一个 `2D Project`。(若遇到问题,可参照**首页**->**模版**->**像素小鸟**) + +image-20231007170002181 + +## 准备资源 + +Flappy Bird 依赖的资源是一堆图片,点击[这里](https://github.com/galacean/galacean.github.io/files/13161928/fb.zip)可以下载图片包到本地。解压之后看到以下图片: + +- 0-9 的分数数字图 +- 游戏背景图 +- 小鸟的动画帧图 +- 草地、管道 +- 游戏重新开始按钮 + +image-20231007170002181 + +### 上传资源 + +回到场景编辑器,点击资源面板上的上传按钮 image-20231007145111353,选择 `Sprite`,此时会唤起操作系统的文件查看器,选中所有 FlappyBird 目录下的图片。上传之后,如下图所示,编辑器为每张图片创建了一个 [Texture](/docs/graphics-texture) 资源和 一个 [Sprite](/docs/graphics-2d-sprite) 资源(为了和 Texture 资源作区分,Sprite 对象带灰色圆角矩形背景)。在接下来的操作中,我们只需要关心 Sprite 资源。 + +image-20231007145451371 + +到这里,我们已经把资源上传完,但是有洁癖的你看到这散乱的资源可能已经按耐不住整理的冲动了。让我们创建一个文件夹,并重命名为 _Sprites_,把刚上传的资源批量选中后拖到 _Sprites_ 目录中。这样做的目的不仅是让资源面板更加整洁,还为我们下一步创建 [Atlas 图集](/docs/graphics-2d-spriteAtlas)资源做好了准备。 + +### 创建图集 + +为了达到更好的运行时性能,我们选择把这些 Sprite 资源打包到一个 Atlas 资源。我们点击 image-20231007152415467 按钮选择 `Sprite Atlas`,创建后选中它,通过 **[检查器面板](/docs/interface-inspector)** 上的 `Add to List` 按钮把所有 Sprite 资源都添加到列表中。 + +image-20231007171348757 + +点击 `Pack and Preview` 按钮可以看到 Atlas 创建成功: + +image-20231007153448666 + +恭喜你,到这里你已经完成了资源上传和管理的操作。接下去我们进行游戏场景搭建的环节。 + +## 搭建场景 + +搭建 2D 场景就像玩拼图一样充满乐趣。首先,我们试着把游戏背景图从资源面板拖动场景中。不要怕拖的位置不准,只要拖到大概的位置,我们后面可以在 **[检查器面板](/docs/interface-inspector)** 中精细调整。 + +![drag1](https://gw.alipayobjects.com/zos/OasisHub/6cabaeea-cc36-4fe1-8bb5-d7ed8a9a49b7/drag1.gif) + +选中层级树面板中的 `Camera` 节点,可以预览场景在各种设备上渲染的样子。 + +> 如果你发现画面太大或太小,可以调整正交相机的 `Orthographic Size` 来实现缩放。 + +image-20231007162550749 + +### 加上小鸟 + +同样,我们把小鸟的 Sprite(`bird3-spr.png`)也拖到场景中。小鸟“飞”的动画是通过序列帧实现的,详见[帧动画](/docs/animation-sprite-sheet)。 + +### 加上管道 + +随着游戏的进行,管道会在画面中重复出现,并且是上下成对出现。这里有个小技巧,可以把上面的管道的 `Scale` 值设成 `-1`,这样就优雅地实现了翻转。 + +image-20231007163240028 + +在游戏过程中,产生管道的高度也是随机的,但是我们手上的资产高度却是固定的。不用急,只需要调整一下`精灵渲染模式`即可,这样可以让我们`无损`拉伸某些资产哦。 + + + +这里有个小技巧,将引用 `sprite` 资产的 `pivot` 属性设置为 `buttom` 就可以避免每次调整高度的时候重新锚定位置了。 + +image-20231007163240028 + +考虑到管道会重复出现,我们在节点树中把一对管道设置成一个 `PipeMother` 的组,并把它放到 `Pipe` 节点下。这样,后面通过在 Pipe 上绑定脚本组件就可以获取 `PipeMother` 以实现管道的复用。 + +image-20231007163400680 + +### 加上草地 + +我们可以通过结合`精灵渲染模式`与 `动画片段编辑` 来实现草地来实现草地在地上平铺且能水平移动的效果。 + +步骤如下: + +1. 在节点树中创建一个节点,命名为 `ground`。 + +2. 在 **[检查器面板](/docs/interface-inspector)** 中通过 `Add Component` 按钮添加 `Sprite Renderer` 组件,并且把 `SpriteRenderer DrawMode Info` 属性设置成 `Tiled`,并将宽度设置为 `8.14` + + image-20231007173243980 + +3. 此时就得到了一个平铺完毕的地面,接下来我们可以通过创建动画片段来让它动起来!详见[动画片段编辑](/docs/animation-clip)。 + + + +### 添加遮罩 + +添加完地面后发现,左右显示好像穿帮了!对于这种情况,只需要为精灵渲染器增加遮罩就好了,详见[精灵遮罩组件](/docs/graphics-2d-spriteMask) + + + +### 加上 GUI + +GUI 包括分数显示和重新开始按钮。我们分数( `0.png`) 和重新开始按钮( `restart.png`) 两个精灵拖到场景中,并放到新建的 `GUI` 节点下。 + +image-20231007180819265 + +至此,界面搭建完毕!观察一下左侧的节点树的完整结构,好的树结构对复杂场景管理来说很重要。 + +> 如果你在上述过程中需要处理精灵之间的遮盖关系,就像 CSS 里的 `z-index` 属性一样,你可以通过 `Sprite Renderer` 的 `priority` 属性来设置,值越大越后渲染,即越能遮住其他精灵。 + +### 增加物理反馈 + +在此项目中,我们需要为**小鸟在触碰到水管或地面**和**鼠标在点击重开按钮**时增加物理反馈,增加物理反馈只需要两步: + +- 添加碰撞体 +- 处理碰撞回调 + +#### 添加碰撞体 + +碰撞体描述了事物的位姿形态,因此在添加碰撞体时应该尽量贴合物体实际显示的大小。关于碰撞器的使用详见[碰撞器](/docs/physics-collider),此处演示为小鸟添加碰撞体。 + + + +#### 处理碰撞回调 + +```typescript +/** + * 挂载在小鸟节点上的脚本组件 + */ +class Bird extends Script { + onTriggerEnter(other: ColliderShape): void { + // 与水管或地面发生了碰撞 + } +} + +/** + * 挂载在重开按钮节点上的脚本组件 + */ +class Restart extends Script { + onPointerClick() { + // 点击了重开按钮 + } +} +``` + +## 编写逻辑 + +正式编写逻辑前,需要对游戏进行全局链路分析: + +- 状态切换与通信方式 +- 不同状态下各个实例对应的表现 + +### 状态切换与通信方式 + +```mermaid +stateDiagram + [*] --> Idle + Idle --> Flying: Tap screen + Flying --> Crash: Collision + Crash --> Result: Landing + Result --> Idle: Click restart + Result --> [*] +``` + +我们枚举了**全局状态**与**切换条件**,可以将它们想象成穿梭在不同实例之间的`信息流`,当小鸟在准备阶段时**按下屏幕**,信息被解析并传递给其他实例对象,此时地面开始播放循环移动动画,水管开始交替出现并消失,`信息流`的传递可以用[事件系统](/docs/script-communication)实现,下面我们简化逻辑,在 `Bird` 中监听屏幕点击事件,一旦点击发生,`Idle` 状态就会切换至 `Flying` ,并且其他实例也会监听到对应状态改变。 + +```typescript +/** + * 全局状态的枚举 + */ +enum EnumState { + Idle, + Flying, + Crash, + Result, +} + +/** + * 对全局状态的控制与分发 + */ +class GameCtrl extends EventDispatcher { + private static _ins: GameCtrl; + static get ins() { + return (this._ins ||= new GameCtrl()); + } + + private _gameState: EnumState = EnumState.Idle; + set gameState(value: EnumState) { + if (this._gameState !== value) { + console.log("GameCtrl:全局状态被改变"); + this._gameState = value; + this.dispatch("State_Change", value); + } + } + + get gameState() { + return this._gameState; + } +} + +/** + * 挂载在小鸟节点上的脚本组件 + */ +class Bird extends Script { + onAwake(): void { + GameCtrl.ins.on("State_Change", (state: EnumState) => { + console.log("Bird:监听到了状态改变"); + }); + } + + onUpdate(deltaTime: number): void { + const { ins } = GameCtrl; + if ( + ins.gameState === EnumState.Idle && + this.engine.inputManager.isPointerDown() + ) { + console.log("Bird:按下屏幕,对局开始"); + ins.gameState = EnumState.Flying; + } + } +} + +/** + * 挂载在地面节点上的脚本组件 + */ +class Ground extends Script { + onAwake(): void { + GameCtrl.ins.on("State_Change", (state: EnumState) => { + console.log("Ground:监听到了状态改变"); + }); + } +} +``` + +同理,依照流程图中各个状态的切换条件,完善其他状态之间的切换(`GameCtrl.ins.gameState = 对应状态`): + +- `Flying` -> `Crash` :小鸟与水管或地面发生碰撞 +- `Crash` -> `Result` :小鸟落地(判断 Y 轴坐标即可) +- `Result` -> `Idle` :点击重新开始按钮 + +### 完善表现 + +我们已经将全局的状态都串联起来,并且保证各个实例都能获取当前状态并监听到状态的改变,接下来只需要让各个实例在不同状态下展示对应的表现即可。 + +```mermaid +timeline + title Game life cycle + Idle : Bird.hang() : Ground.move() : Pipe.reset() : Gui.hide() + Flying : Bird.fly() : Ground.move() : Pipe.move() + Crash : Bird.crash() : Ground.pause() : Pipe.pause() + Result : Gui.show() +``` + +经过拆解可以发现,如果我们简单地将动画分类为待机动画,飞行动画与坠落动画,考虑到待机的时候要播放精灵切换和上下缓动的动画,飞行的时候也需要播放精灵切换与抬头坠落的动画,他们重合的部分不仅会增加动画编辑时的工作量,还需要额外考虑这两个动画衔接时其中的精灵切换动画是否自然,因此我们更进一步,将各个动画状态原子化,拆分其中精灵切换与坐标改变的部分,并分别设置在不同的 `Layer` 中,不同的 `Layer` 相互独立,并可同时播放各自的动画,设置各自的叠加模式与权重,详情可参考[动画组件](/docs/animation-system)。 + +image-20231007180819265 + +让各个 `Layer` 分别控制各自的动画状态,可以逻辑更加清晰。 + +```mermaid +timeline + title Animation Layer + 替换精灵(Layer0) : Alive : Dead + 修改旋转(Layer1) : Hang : Fly : Crash + 修改坐标(Layer2) : Hang : Fly : Crash +``` + +#### 小鸟 + +```typescript +/** + * 挂载在小鸟节点上的脚本组件 + */ +class Bird extends Script { + private _animator: Animator; + + onAwake() { + this._animator = this.entity.getComponent(Animator); + GameCtrl.ins.on("State_Change", (state: EnumState) => { + const animator = this._animator; + switch (state) { + case EnumState.Idle: + this._alive(); + this._hang(); + break; + case EnumState.Flying: + break; + case EnumState.Crash: + this._dead(); + this._crash(); + break; + case EnumState.Result: + break; + } + }); + } + + onUpdate(deltaTime: number): void { + const { ins } = GameCtrl; + if ( + ins.gameState === EnumState.Idle && + this.engine.inputManager.isPointerDown() + ) { + this._fly(); + ins.gameState = EnumState.Flying; + } + } + + onTriggerEnter(other: ColliderShape): void { + GameCtrl.ins.gameState = EnumState.Crash; + } + + private _alive(): void { + // 帧动画-拍动翅膀 + animator.play("alive", 0); + } + + private _dead(): void { + // 停止拍动翅膀 + animator.play("dead", 0); + } + + private _hang(): void { + // 准备阶段 + animator.play("Hang", 1); + animator.play("Hang", 2); + } + + private _fly(): void { + // 向上冲 + animator.play("Fly", 1); + animator.play("Fly", 2); + } + + private _crash(): void { + // 坠落 + animator.play("Crash", 1); + animator.play("Crash", 2); + } +} +``` + +由于动画片段编辑只能编辑绝对的坐标或旋转变化,例如每次飞行的动画,他的旋转变化是绝对的,但坐标却是相对的,因此我们可以在 `StateMachineScript` 中实现,以 `Fly` 动画为例: + + + +然后打开这个脚本,并在其中添加上自由落体的坐标变化: + +```typescript +export default class extends StateMachineScript { + // 小鸟的位置 + private _position: Vector3; + // 起始时间 + private _startTime = 0; + // 起始位置 + private _startY = 0; + // 起始速度 + private _startV = 10; + // 最终匀速速度 + private _maxV = -8; + // 重力加速度 + private _gravity = -35; + // [0, _dividTime] 匀加速;[_dividTime, +∞] 匀速 + private _dividTime = 18 / 35; + + onStateEnter( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + this._startTime = animator.engine.time.elapsedTime; + this._position = animator.entity.transform.position; + this._startY = this._position.y; + } + + onStateUpdate( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + const { engine } = animator; + const { _maxV, _startV, _gravity, _dividTime, _position } = this; + const subTime = engine.time.elapsedTime - this._startTime; + if (subTime <= _dividTime) { + _position.y = + ((_startV + (_startV + subTime * _gravity)) * subTime) / 2 + + this._startY; + } else { + _position.y = + ((_maxV + _startV) * _dividTime) / 2 + + _maxV * (subTime - _dividTime) + + this._startY; + } + } +} +``` + +同理,在小鸟坠落时也需要添加 `Crash` 脚本: + +```typescript +class extends StateMachineScript { + // 是否已经落地 + private _bLanding: boolean = false; + + onStateEnter( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + this._bLanding = false; + } + + onStateUpdate( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + if (this._bLanding) { + return; + } + const { entity, engine } = animator; + const { position } = entity.transform; + // 地面高度 + if (position.y <= -3.1) { + GameCtrl.ins.gameState = EnumState.Result; + this._bLanding = true; + } else { + position.y -= engine.time.deltaTime; + } + } +} +``` + +OK!这个游戏中最复杂的部分已经被我们成功攻克了,此时点击屏幕,小鸟触发飞行动画,同时每帧计算自由落体的位置,触碰到障碍物后,小鸟触发坠落动画,同时每帧计算坠落的位置。 + +#### 水管 + +水管较为复杂,在对局开始时,我们让水管向左移动,当需要生成下个水管时,从池子中获取,当水管移动超出显示区域时,回收水管到池子中。 + +```typescript +/** + * 挂载在水管节点上的脚本组件 + */ +class Pipe extends Script { + // 水管池子 + private _pipePool = []; + // 当前激活的水管 + private _pipes = []; + // 水管母体 + private _pipeMother: Entity; + // 是否停止 + private _isPaused: boolean = true; + + private _inv = 2.87; + private _up = 4.48; + private _down = -3.2; + private _downLimit = -2.12 + 1.08; + private _upLimit = 3.4 - 1.08; + private _pipeHorizontalV = 3; + private _leftDistance = 2; + + onAwake() { + this._pipeMother = this.entity.children[0]; + this._pipeMother.parent = null; + GameCtrl.ins.on("State_Change", (state: EnumState) => { + switch (state) { + case EnumState.Idle: + this._reset(); + break; + case EnumState.Flying: + this._move(); + break; + case EnumState.Crash: + this._pause(); + break; + default: + break; + } + }); + } + + onUpdate(deltaTime: number) { + if (this._isPaused) { + return; + } + const { ins } = GameCtrl; + const moveDistance = this._pipeHorizontalV * deltaTime; + if ((this._leftDistance -= moveDistance) <= 0) { + this._leftDistance = 4; + this._generate(); + } + const { _pipes: pipes } = this; + for (let i = pipes.length - 1; i >= 0; i--) { + const pipe = pipes[i]; + const { position } = pipe.transform; + const posX = position.x - moveDistance; + if (position.x >= 0 && posX < 0) { + ins.score += 1; + } + if (posX <= -4.53) { + pipes.splice(i, 1); + pipe.parent = null; + this._pipePool.push(pipe); + } else { + position.x = posX; + } + } + } + + private _move() { + this._isPaused = false; + } + + private _pause() { + this._isPaused = true; + } + + private _reset() { + const { _pipes: pipes } = this; + for (let i = 0, n = pipes.length; i < n; i++) { + const pipe = pipes[i]; + pipe.parent = null; + this._pipePool.push(pipe); + } + pipes.length = 0; + this._leftDistance = 2; + this._isPaused = true; + } + + private _getOrCreatePipe() { + let pipe: Entity; + if (this._pipePool.length > 0) { + pipe = this._pipePool.pop(); + } else { + pipe = this._pipeMother.clone(); + } + this._pipes.push(pipe); + const center = + Math.random() * (this._upLimit - this._downLimit) + this._downLimit; + + const [upColliderShape, downColliderShape] = ( + pipe.getComponent(StaticCollider).shapes + ); + + const upPipe = pipe.findByName("up_pipe"); + const upRenderer = upPipe.getComponent(SpriteRenderer); + const upHeight = this._up - center - this._inv / 2; + upColliderShape.size.set(1.2, upHeight, 1); + upColliderShape.position.set(0, 4.48 - upHeight / 2, 0); + upRenderer.height = upHeight; + const downPipe = pipe.findByName("down_pipe"); + const downRenderer = downPipe.getComponent(SpriteRenderer); + const downHeight = center - this._down - this._inv / 2; + downColliderShape.size.set(1.2, downHeight, 1); + downColliderShape.position.set(0, downHeight / 2 - 3.2, 0); + downRenderer.height = downHeight; + pipe.transform.position.x = 4.53; + this.entity.addChild(pipe); + } +} +``` + +可以看到,上方的逻辑就是对流程图的代码完善: + +- 当状态切换为 `Idle` 时,`Pipe._reset()` 函数被触发,场上所有的水管都被回收至池中 +- 当状态切换为 `Flying` 时,`Pipe._move()` 函数被触发,水管命运的齿轮开始转动,帧循环中判断是否需要生成新的水管,是否需要回收旧的水管,生成新水管使用了引擎自带的 [clone](/docs/core-clone) 能力,可以完整复刻节点的结构与组件。 +- 当状态切换为 `Crash` 时,`Pipe._pause()` 函数被触发,水管停止移动。 + +#### 地面 + +地面的逻辑相对简单,只需要在 `Flying` 时让地面移动,其余的时间让地面保持静止即可。 + +```typescript +class Ground extends Script { + private _animator: Animator; + onAwake() { + this._animator = this.entity.getComponent(Animator); + GameCtrl.ins.on("State_Change", (state: EnumState) => { + if (state === EnumState.Flying) { + this._move(); + } else { + this._pause(); + } + }); + this._pause(); + } + + private _move() { + this._animator.speed = 1; + } + + private _pause() { + this._animator.speed = 0; + } +} +``` + +#### GUI + +在实现完上述逻辑后,项目基本可以正常运行了,此时我们需要为游戏加上分数显示让他的逻辑更加完整,同样分析分数的改变与传递时机,小鸟通过水管时会触发分数的叠加,对局重开时会触发分数的重置,分数的改变信息会传递给各个实例,参考 `gameState` 依葫芦画瓢,我们在 `GameCtrl` 中添加 `score` 属性。 + +```typescript +class GameCtrl extends EventDispatcher { + private static _ins: GameCtrl; + + static get ins() { + return (this._ins ||= new GameCtrl()); + } + + private _gameState: EnumState = EnumState.Idle; + private _score: number = 0; + set gameState(value: EnumState) { + if (this._gameState !== value) { + this._gameState = value; + if (value === EnumState.Idle) { + // 重开时重置分数 + this._reset(); + } + this.dispatch("State_Change", value); + } + } + + get gameState() { + return this._gameState; + } + + set score(val: number) { + this._score = val; + this.dispatch("Score_Change", val); + } + + get score() { + return this._score; + } + + private _reset() { + this.score = 0; + } +} +``` + +这样一来,分数变化整体的流程也完善了,接下来只需要完善分数的展示逻辑即可: + +- 切换至 `Idle` 状态时,隐藏分数 +- 切换至 `Flying` 状态时,展示分数 + +```typescript +class Score extends Script { + // 数字精灵所在的图集 + private _atlas: SpriteAtlas; + // 提供克隆的数字母体 + private _scoreMother: Entity; + // 当前显示的数字节点数组 + private _scoreEntities: Entity[] = []; + // 当前显示的数字精灵渲染器数组 + private _scoreRenderers: SpriteRenderer[] = []; + // 每个数字之间的间隔(归一化) + private _inv: number = 1.2; + + onAwake() { + const { engine, entity } = this; + const { ins } = GameCtrl; + this._scoreEntities[0] = this._scoreMother = + entity.findByName("scoreMother"); + + this._scoreRenderers[0] = + this._scoreEntities[0].getComponent(SpriteRenderer); + + // 通过相对路径获取精灵图集资产 + engine.resourceManager + .load({ type: AssetType.SpriteAtlas, url: "/Assets/atlas/SpriteAtlas" }) + .then((atlas: SpriteAtlas) => { + this._atlas = atlas; + }); + + ins.on("State_Change", (state: EnumState) => { + switch (state) { + case EnumState.Idle: + this._hide(); + break; + case EnumState.Flying: + this._show(ins.score); + break; + } + }); + + ins.on("Score_Change", (num: number) => { + if (ins.gameState !== EnumState.Idle) { + this._show(num); + } + }); + } + + private _show(num: number): void { + const { + _scoreEntities: entities, + _scoreRenderers: renderers, + _scoreMother: mother, + } = this; + const score = num.toFixed(0); + const needCount = score.length; + const currCount = entities.length; + const n = Math.max(needCount, currCount); + const width = needCount * this._inv; + for (let i = 0; i < n; i++) { + if (i >= needCount) { + entities[i] && (entities[i].isActive = false); + } else { + let entity: Entity; + let renderer: SpriteRenderer; + if (entities[i]) { + entity = entities[i]; + renderer = renderers[i]; + } else { + entity = entities[i] = mother.clone(); + renderer = renderers[i] = entity.getComponent(SpriteRenderer); + this.entity.addChild(entity); + } + entity.isActive = true; + entity.transform.position.x = this._inv * (i + 0.5) - width / 2; + renderer.priority = 10; + renderer.sprite = this._atlas?.getSprite( + "Assets/sprites/" + score[i] + "-spr.png" + ); + } + } + } + + private _hide(): void { + const { _scoreEntities: entities } = this; + for (let i = 0, n = entities.length; i < n; i++) { + entities[i].isActive = false; + } + } +} +``` + +Restart 按钮相对来说比较简单: + +```typescript +class Restart extends Script { + private collider: StaticCollider; + private spriteRenderer: SpriteRenderer; + + onAwake() { + const { entity } = this; + this.collider = entity.getComponent(StaticCollider); + this.spriteRenderer = entity.getComponent(SpriteRenderer); + GameCtrl.ins.on("State_Change", (state: EnumState) => { + switch (state) { + case EnumState.Result: + this.show(); + break; + default: + this.hide(); + break; + } + }); + + this.hide(); + } + + hide() { + this.collider.enabled = this.spriteRenderer.enabled = false; + } + + show() { + this.collider.enabled = this.spriteRenderer.enabled = true; + } + + onPointerClick() { + GameCtrl.ins.gameState = EnumState.Idle; + } +} +``` + +至此,所有的游戏逻辑都已完善,点击预览快试试有没有 Bug 吧!如果对中间某些步骤有疑问,可以通过在编辑器 **首页**->**模版**->**像素小鸟** 对照依照此文档实现的模版,如果对此文档有其他建议,欢迎提出您的想法。 diff --git a/docs/quick-start/overview.md b/docs/quick-start/overview.md new file mode 100644 index 000000000..52f69e66c --- /dev/null +++ b/docs/quick-start/overview.md @@ -0,0 +1,112 @@ +--- +order: 0 +title: 概述 +type: 基础知识 +label: Basics +--- + +**Galacean Engine** 是一套 Web 为先、移动优先、开源共建的实时互动解决方案,采用组件化架构与 [Typescript](https://www.typescriptlang.org/) 编写。它包含了[渲染](/docs/graphics-renderer)、[物理](/docs/physics-overall)、[动画](/docs/animation-system)和[交互](/docs/input)功能,并提供了具备完善工作流的可视化在线编辑器,帮助你在浏览器上创作绚丽的 2D/3D 互动应用。它主要由两部分组成: + +- 编辑器:一个在线 Web 互动创作平台 [Editor](https://galacean.antgroup.com/editor) +- 运行时:一个 Web 为先、移动优先的高性能的互动运行时 [Runtime](https://github.com/galacean/runtime),一系列非核心功能和偏业务逻辑定制功能 [Toolkit](https://github.com/galacean/runtime-toolkit) + +## 编辑器 + +[Galacean Editor](https://antg.antgroup.com/editor) 是一个在线 Web 互动创作平台。它可以帮助你快速的创建、编辑和导出一个互动项目。你可以通过 Galacean Editor 快速上传互动资产,创建和编辑材质、调整灯光、创建实体,从而创造出复杂的场景。 + +使用编辑器创建互动项目的整体流程: + +```mermaid +flowchart LR + 创建项目 --> 创建资产 --> 搭建场景 --> 编写脚本 --> 导出 +``` + +通过编辑器可以让技术与美术同学更好地进行协作,你可以在[编辑器首页](https://galacean.antgroup.com/editor)通过项目模板快速开始第一个项目的开发。 + +## 运行时 + +核心功能由 [Galacean Runtime](https://www.npmjs.com/package/@galacean/runtime) 提供,非核心和偏业务逻辑定制的高级功能由 [Galacean Toolkit](https://github.com/galacean/runtime-toolkit) 提供。你可以通过浏览器在线浏览引擎的各种[示例](https://antg.antgroup.com/#/examples/latest/background)。 + +### 核心包 + +包括以下子包: + +| 功能 | 解释 | API | +| :--------------------------------------------------------------------------------------------- | :--------------------- | -------------------------- | +| [@galacean/engine](https://www.npmjs.com/package/@galacean/engine) | 核心架构逻辑和核心功能 | [API](${api}core) | +| [@galacean/engine-physics-lite](https://www.npmjs.com/package/@galacean/engine-physics-lite) | 轻量级物理引擎 | [API](${api}physics-lite) | +| [@galacean/engine-physics-physx](https://www.npmjs.com/package/@galacean/engine-physics-physx) | 全功能物理引擎 | [API](${api}physics-physx) | +| [@galacean/engine-draco](https://www.npmjs.com/package/@galacean/engine-draco) | Draco 模型压缩 | [API](${api}draco) | + +你可以通过 [NPM](https://docs.npmjs.com/) 的方式进行安装: + +```bash +npm install --save @galacean/engine +``` + +然后在业务中引入使用: + +```typescript +import { WebGLEngine, Camera } from "@galacean/engine"; +``` + +如果你只是想在本地快速完成一个 Demo, 推荐你使用 [create-galacean-app](https://github.com/galacean/create-galacean-app), 它提供了一些常用的框架如 [React](https://reactjs.org/)、[Vue](https://vuejs.org/) 等模板。 + +### 工具包 + +非核心功能和偏业务逻辑定制功能由 galacean-toolkit 包提供(完成功能列表请查看[engine-toolkit](https://github.com/galacean/engine-toolkit/tree/main)): + +| 功能 | 解释 | API | +| :----------------------------------------------------------------------------------------------------------------------- | :----------- | :------------------------------------- | +| [@galacean/engine-toolkit-controls](https://www.npmjs.com/package/@galacean/engine-toolkit-controls) | 控制器 | [Doc](/docs/graphics-camera-control) | +| [@galacean/engine-toolkit-framebuffer-picker](https://www.npmjs.com/package/@galacean/engine-toolkit-framebuffer-picker) | 帧缓冲拾取 | [Doc](/docs/input-framebuffer-picker) | +| [@galacean/engine-toolkit-stats](https://www.npmjs.com/package/@galacean/engine-toolkit-stats) | 引擎统计面板 | [Doc](/docs/performance-stats) | +| ...... | | | + +你可以通过 [NPM](https://docs.npmjs.com/) 的方式进行安装: + +```bash +npm install --save @galacean/engine-toolkit-controls +``` + +然后在业务中引入使用: + +```typescript +import { OrbitControl } from " @galacean/engine-toolkit-controls"; +``` + +> 在同一项目中,请保证引擎核心包的版本一致和工具包的大版本保持一致,以 1.0.x 版本的引擎为例,需要配套使用 1.0.y 版本的工具包。 + +另外还有一些二方生态包,引入和使用方式和引擎工具包相同: + +| 功能 | 解释 | API | +| :------------------------------------------------------------------------------- | :---------- | :------------------------------ | +| [@galacean/engine-spine](https://www.npmjs.com/package/@galacean/engine-spine) | Spine 动画 | [Doc](/docs/graphics-2d-spine) | +| [@galacean/engine-lottie](https://www.npmjs.com/package/@galacean/engine-lottie) | Lottie 动画 | [Doc](/docs/graphics-lottie) | + +### 兼容性 + +可以在支持 WebGL 的环境下运行,到目前为止,所有主流的移动端浏览器与桌面浏览器都支持这一标准。可以在 [CanIUse](https://caniuse.com/?search=webgl) 上检测运行环境的兼容性。 + +此外,**Galacean Runtime** 还支持在[支付宝/淘宝小程序](/docs/assets-build)中运行,同时也有开发者在社区贡献了[微信小程序/游戏的适配方案](https://github.com/deepkolos/platformize)。对于一些需要额外考虑兼容性的功能模块,当前的适配方案如下: + +| 模块 | 兼容考虑 | 具体文档 | +| :------------------------------ | :------------------------------------------------------- | :-------------------------------------------------------------------------------------- | +| [鼠标与触控](/docs/input) | [PointerEvent](https://caniuse.com/?search=PointerEvent) | 兼容请参照 [polyfill-pointer-event](https://github.com/galacean/polyfill-pointer-event) | +| [PhysX](/docs/physics-overall) | [WebAssembly](https://caniuse.com/?search=wasm) | 运行环境需支持 WebAssembly | + +### 版本管理 + +以 `@galacean/engine` 为例,你可以在 [Github](https://github.com/galacean/engine/releases) 或 [NPM](https://www.npmjs.com/package/@galacean/engine?activeTab=versions) 上查看所有可用版本,其中: + +- **alpha**:内部测试版,用于早期功能研发,有里程碑内的新功能但稳定性较差,例如 [1.0.0-alpha.6](https://www.npmjs.com/package/@galacean/engine/v/1.0.0-alpha.6) +- **beta**: 公开测试版,内部测试已基本完毕,稳定性较强,但可能仍有较少的问题与缺陷,例如 [1.0.0-beta.8](https://www.npmjs.com/package/@galacean/engine/v/1.0.0-beta.8) +- **stable**:正式稳定版,经过长期测试和验证,无重大缺陷,可投入生产的推荐版本,例如 [0.9.8](https://www.npmjs.com/package/@galacean/engine/v/0.9.8) + +每个里程碑版本更新迭代时会同步发布[版本升级引导](https://github.com/galacean/engine/wiki/Migration-Guide),其中包含了本次更新的内容以及 BreakChange,可依据此文档进行版本的更新迭代。 + +如果您的项目正在使用旧版本的 Oasis 进行开发,并且希望升级为 Galacean,可以参考 [@crazylxr](https://github.com/crazylxr) 提供的 [galacean-codemod](https://github.com/crazylxr/galacean-codemod) 工具。 + +## 开源共建 + +**Galacean** 渴望与你共建互动引擎,所有的开发流程,包括[规划](https://github.com/galacean/engine/projects?query=is%3Aopen),[里程碑](https://github.com/galacean/engine/milestones),[架构设计](https://github.com/galacean/engine/wiki/Physical-system-design)在内的信息,全部都公开在 GitHub 的项目管理中,你可以通过[创建 issue](https://docs.github.com/zh/issues/tracking-your-work-with-issues/creating-an-issue) 与[提交 PR](https://docs.github.com/zh/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) 参与到引擎的建设当中。如果你有疑问或者需要帮助,可以加入钉钉群或[讨论区](https://github.com/orgs/galacean/discussions)寻求帮助。 diff --git a/docs/script/attributes.md b/docs/script/attributes.md new file mode 100644 index 000000000..ffef3c3c6 --- /dev/null +++ b/docs/script/attributes.md @@ -0,0 +1,71 @@ +--- +order: 4 +title: 脚本参数 +type: 脚本 +label: Script +--- + +脚本参数是脚本系统中一个非常实用的功能。使用此功能,你可以将脚本中的参数暴露给编辑器,从而可以在场景编辑器中进行配置。你可以直接在界面上修改脚本的各项属性,而无需深入代码进行修改。这种直观的编辑方式可以让非专业开发人员也能够便捷的调试脚本中的各种状态。 + +## 基本用法 + +```typescript +import { Script } from '@galacean/engine'; +import { inspect } from "@galacean/editor-decorators"; + +export default class extends Script { + @inspect('Number') + rotate = 1; + + onUpdate(deltaTime: number) { + this.entity.transform.rotate(this.rotate, this.rotate, this.rotate); + } +} +``` + +在上面的代码中,我们借助 `@inspect` 装饰器声明了一个类型为 `Number` 的 `rotate` 属性,并且将其暴露给了编辑器。 + +![属性面板](https://mdn.alipayobjects.com/huamei_fvsq9p/afts/img/A*n22bR7-lZ5QAAAAAAAAAAAAADqiTAQ/original) + +## 参数类型 + +目前支持的参数类型有: + +- `Number`:数字类型 +- `Input`:输入框 +- `Slider`:滑动条 +- `Boolean`:布尔类型 +- `Vector2`:二维向量 +- `Vector3`:三维向量 +- `Vector4`:四维向量 +- `Rect`:矩形 +- `Color`:颜色选择器,支持 RGBA +- `AssetPicker`:资源选择器 +- `Select`:下拉选择器 +- `Textarea`:多行文本输入框 + +## 参数配置 + +`@inspect` 装饰器的第二个参数是一个对象,用于配置所对应类型参数的各项属性。不同的参数类型对应的选项是不同的。比如 `Number` 和 `Slider` 具有 `min` `max` 配置,`Select` 有 `options` 配置。 想要了解更多的可配置属性可以查看 [@galaean/editor-decorators](https://www.npmjs.com/package/@galacean/editor-decorators?activeTab=readme) 。下面以数字选择器为例,介绍一下各项配置的含义。 + +```typescript +import { Script } from '@galacean/engine'; +import { inspect } from "@galacean/editor-decorators"; + +export default class extends Script { + @inspect('Number', { + min: 0, // 最小值 + max: 10, // 最大值 + dragStep: 0.1, // 拖拽步长 + property: 'rotate', // 对应到引擎对象的属性名,默认为装饰器所修饰的属性名 + label: 'Rotate', // 在检查器面板中显示的名称,默认为装饰器所修饰的属性名 + info: 'Rotate speed', // 在检查器面板中显示的描述信息 + }) + rotate = 1; + + onUpdate(deltaTime: number) { + this.entity.transform.rotate(this.rotate, this.rotate, this.rotate); + } +} +``` + diff --git a/docs/script/class.md b/docs/script/class.md new file mode 100644 index 000000000..a342a6aa9 --- /dev/null +++ b/docs/script/class.md @@ -0,0 +1,219 @@ +--- +order: 1 +title: 脚本类 +type: 脚本 +label: Script +--- + +自定义脚本的基类是 [Script](/apis/core/#Script) ,它扩展自 [Component](/docs/core-component),因此组件包含的能力与操作,它不仅支持组件的基础能力: + +- 挂载到 [Entity](/docs/core-entity) 上 +- 方便地获取节点实例,组件实例 +- 遵循组件的禁用销毁规则 +- …… + +除此以外,脚本还提供丰富的生命周期回调函数,只要脚本中重写特定的回调函数,不需要手工调用它们,Galacean 就会在特定的时期自动执行相关脚本。 + +## 脚本生命周期 + +脚本生命周期-zh + +> [onBeginRender](/apis/core/#Script-onBeginRender) 和 [onEndRender](/apis/core/#Script-onEndRender) 这两个生命周期与其他的有些不同。 +> +> **当且仅当实体挂载了相机组件** 也就是添加了相机组件时,他们才会被调用。 + +### [**onAwake**](/apis/core/#Script-onAwake) + +如果脚本添加到的实体的 [isActiveInHierarchy](/apis/core/#Entity-isactiveinhierarchy) 为 `true`,则在脚本初始化时回调函数将被调用,如果[isActiveInHierarchy](/apis/core/#Entity-isActiveInHierarchy) 为 `false`,则在实体被激活,即 [isActive](/apis/core/#Entity-isActive)  被设为 `true`  时被调用。 `onAwake`  只会被调用一次,并且在所有生命周期的最前面,通常我们会在 `onAwake`  中做一些初始化相关的操作: + +```typescript +onAwake() { + this.child = this.entity.getChild(0); + this.child.isActive = false; +} +``` + +### [**onEnable**](/apis/core/#Script-onEnable) + +当脚本的 [enabled](/apis/core/#Component-enabled) 属性从 `false` 变为 `true` 时,或者所在实体的 [isActiveInHierarchy](/apis/core/#Entity-isactiveinhierarchy) 属性从 `false` 变为 `true` 时,会激活 `onEnable` 回调。倘若实体第一次被创建且 [enabled](/apis/core/#Component-enabled) 为 `true`,则会在 `onAwake` 之后,`onStart` 之前被调用。 + +### [**onDisable**](/apis/core/#Script-ondisable) + +当组件的 [enabled](/apis/core/#Component-enabled) 属性从 `true` 变为 `false` 时,或者所在实体的 [isActiveInHierarchy](/apis/core/#Entity-isActiveInHierarchy) 属性从 `true` 变为 `false` 时,会激活 `onDisable` 回调 + +注意:[isActiveInHierarchy](/apis/core/#Entity-isActiveInHierarchy) 的判断方法为实体在层级树中是被激活状态即该实体为激活状态,它的父亲及父亲的父亲直到根实体都为激活状态 [isActiveInHierarchy](/apis/core/#Entity-isActiveInHierarchy) 才为 `true` + +### [**onStart**](/apis/core/#Script-onStart) + +`onStart` 回调函数会在脚本第一次进入帧循环,也就是第一次执行 `onUpdate` 之前触发。`onStart` 通常用于初始化一些需要经常修改的数据,这些数据可能在 `onUpdate` 时会发生改变。 + +```typescript +onStart() { + this.updateCount = 0 +} + +onUpdate() { + this.updateCount++; +} +``` + +需要注意的是,Galacean 在批量执行完 `onStart` 回调后再批量执行 `onUpdate` 回调。这样做的好处是可以在 `onUpdate` 中访问其他实体初始化的值。 + +```typescript +import { TheScript } from './TheScript' +onStart() { + this.otherEntity = Entity.findByName('otherEntity'); + this.otherEntityScript = this.otherEntity.getComponent(TheScript) +} + +onUpdate() { + console.log(this.otherEntityScript.updateCount) +} +``` + +### [**onPhysicsUpdate**](/apis/core/#Script-onPhysicsUpdate) + +`onPhysicsUpdate` 回调函数调用频率与物理引擎更新频率保持一致。每个渲染帧可能会调用多次。 + +### [**onTriggerEnter**](/apis/core/#Script-onTriggerEnter) + +`onTriggerEnter` 回调函数会在触发器相互接触时调用,以处理触发器相遇时的逻辑,例如在触发发生时删除实体。 + +### [**onTriggerStay**](/apis/core/#Script-onTriggerStay) + +`onTriggerStay` 回调函数会在触发器接触过程中**持续**调用,每帧调用一次。 + +### [**onTriggerExit**](/apis/core/#Script-onTriggerExit) + +`onTriggerExit` 回调函数会在两个触发器分离时被调用,即触发关系发生改变,只调用一次。 + +### [**onCollisionEnter**](/apis/core/#Script-onCollisionEnter) + +`onCollisionEnter` 回调函数会在碰撞器碰撞时调用,以处理碰撞体相遇时的逻辑,例如在碰撞发生时删除实体。 + +### [**onCollisionStay**](/apis/core/#Script-onCollisionStay) + +`onCollisionStay` 回调函数会在碰撞器碰撞过程中**持续**调用,每帧调用一次。 + +### [**onCollisionExit**](/apis/core/#Script-onCollisionExit) + +`onCollisionExit` 回调函数会在两个碰撞器分离时被调用,即碰撞关系发生改变,只调用一次。 + +### [**onUpdate**](/apis/core/#Script-onUpdate) + +游戏/动画开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 `onUpdate` 回调中。接收与上一次 `onUpdate` 执行时间差参数, 类型是 `number` + +```typescript +onStart() { + this.rotationY = 0 +} + +onUpdate(deltaTime: number) { + this.entity.transform.rotate(new Vector3(0, this.rotationY++, 0)) +} +``` + +### [**onLateUpdate**](/apis/core/#Script-onLateUpdate) + +`onUpdate` 会在所有动画更新前执行,但如果我们要在动效(如动画、粒子等)更新之后才进行一些额外操作,或者希望在所有组件的 `onUpdate` 都执行完之后才进行其它操作比如相机跟随,那就需要用到 `onLateUpdate` 回调。接收与上一次 `onLateUpdate` 执行时间差参数, 类型是 `number` + +```typescript +onStart() { + this.rotationY = 0 +} + +onUpdate() { + this.entity.transform.rotate(new Vector3(0, this.rotationY++, 0)) +} + +onLateUpdate(deltaTime: number) { + this.rotationY %= 360; +} +``` + +### [**onBeginRender**](/apis/core/#Script-onBeginRender) + +**当且仅当实体挂载了相机组件**,那么相机组件的 [render](/apis/core/#Camera-render) 方法调用之前 `onBeginRender` 回调将被调用。 + +### [**onEndRender**](/apis/core/#Script-onEndRender) + +**当且仅当实体挂载了相机组件**,那么相机组件的 [render](/apis/core/#Camera-render) 方法调用之后 `onEndRender` 回调将被调用。 + +### [**onDestroy**](/apis/core/#Script-onDestroy) + +当组件或者所在实体调用了 [destroy](/apis/core/#Entity-destroy),则会调用 `onDestroy` 回调,并在当帧结束时统一回收组件。 + +### onPointerXXX + +输入系统接口详见[输入交互](/docs/input)。 + +## 实体操作 + +[实体](/docs/core-entity)是脚本的主要操作对象。你可以在编辑器场景检查器里修改节点和组件,也能在脚本中动态修改。脚本能够响应玩家输入,能够修改、创建和销毁实体或组件,从而实现各种各样的游戏逻辑。 + +### 访问实体和组件 + +你可以在脚本的任意生命周期内获得它所绑定的实体,如: + +```typescript +class MyScript extends Script { + onAwake() { + const entity = this.entity; + } +} +``` + +### 获得其它组件 + +当我们需要获取同一节点上的其他组件,这时就要用到 [getComponent](/apis/core/#Entity-getComponent) 这个 API, 它会帮你查找你要的组件。 + +```typescript +onAwake() { + const components = [] + this.entity.getComponents(o3.Model, components); +} +``` + +有些时候可能会有多个同一类型的组件,上面的方法只会返回第一个找到的组件,如果需要找到所有组件可以用 [getComponents](/apis/core/#Entity-getComponents)。 + +### 变换 + +以旋转为例,在 [onUpdate](/apis/core/#Script-onUpdate) 中通过 [setRotation](/apis/core/#Transform-setRotation) 方法来旋转实体: + +```typescript +this.entity.transform.setRotation(0, 5, 0); +``` + +```typescript +onAwake() { + const component = this.entity.getComponent(o3.Model); +} +``` + +### 查找子节点 + +有时候,场景中会有很多个相同类型的对象,像多个粒子动画,多个金币,它们通常都有一个全局的脚本来统一管理。如果用一个一个将它们关联到这个脚本上,那工作就会很繁琐。为了更好地统一管理这些对象,我们可以把它们放到一个统一的父物体下,然后通过父物体来获得所有的子物体: + +如果明确知道子节点在父节点中的 index 可以直接使用 [getChild](/apis/core/#Entity-getChild) : + +```typescript +onAwake() { + this.entity.getChild(0); +} +``` + +如果不清楚子节点的 index,可以使用 [findByName](/apis/core/#Entity-findByName) 通过节点的名字找到它, [findByName](/apis/core/#Entity-findByName) 不仅会查找子节点,还会查找孙子节点 + +```typescript +onAwake() { + this.entity.findByName('model'); +} +``` + +如果有同名的节点可以使用 [findByPath](/apis/core/#Entity-findByPath) 传入路径进行逐级查找,使用此 API 也会一定程度上提高查找效率。 + +```typescript +onAwake() { + this.entity.findByPath('parent/child/grandson'); +} +``` diff --git a/docs/script/communication.md b/docs/script/communication.md new file mode 100644 index 000000000..14b90b345 --- /dev/null +++ b/docs/script/communication.md @@ -0,0 +1,65 @@ +--- +order: 5 +title: 事件通信 +type: 脚本 +label: Script +--- + +使用 Galacean Engine 开发的项目,通常还需要与外部环境进行通信,比如将项目运行时的信息发送到外部,或从外部环境或许某些配置信息。此时,你可以使用 Galacean Engine 的事件系统来实现此类功能。 + +## 添加事件 + +Galacean Engine 提供了 [EventDispatcher](/apis/core/#EventDispatcher) 作为事件类,[Engine](/apis/core/#Engine) 继承自 [EventDispatcher](/apis/core/#EventDispatcher),因此我们直接在代码中使用 `engine` 来作为内外部通信的媒介。 + +**使用 `engine.on` 添加事件** + +```ts +import { Script } from "@galacean/engine"; + +class MyScript extends Script { + onAwake() { + this.engine.on("Trigger", (...args) => { + console.log("Trigger Event is Fired!", args); + }); + } +} +``` + +**使用 `engine.once` 添加事件** + +使用 `engine.once` 添加的事件只会触发一次回调函数。 + +```ts +import { Script } from "@galacean/engine"; + +class MyScript extends Script { + onAwake() { + this.engine.once("TriggerOnce", (...args) => { + console.log("Trigger Event is Fired!", args); + }); + } +} +``` + +保存代码后,我们就可以在事件面板中看到相应的事件。 + +## 触发事件 + +调用 `engine.dispatch` 方法可以派发事件,派发事件会使用 `dispatch` 中配置的参数来执行相应的回调函数。 + +```ts +this.engine.dispatch("Trigger", { eventData: "mydata" }); +``` + +你可以在脚本的任何生命周期中触发事件,当然你也可以使用事件面板来触发事件,或者配置触发事件时所携带的参数。 + +## 移除事件 + +使用 `this.engine.off` 可以移除相关事件。 + +```ts +// Remove the specific function "fun" that listen to "Trigger". +this.engine.off("Trigger", fun); +// Remove all functions that listen to "Trigger". +this.engine.off("Trigger"); +``` diff --git a/docs/script/create.md b/docs/script/create.md new file mode 100644 index 000000000..ed333295a --- /dev/null +++ b/docs/script/create.md @@ -0,0 +1,15 @@ +--- +order: 2 +title: 创建脚本 +type: 脚本 +label: Script +--- + +[脚本组件](/docs/script)是引擎提供给开发者的重要的扩展能力,在 Galacean 编辑器中,脚本也是一种资产。 + +## 在编辑器中使用脚本 + +在编辑器中使用脚本非常方便,只需要创建脚本后,在实体的脚本组件中添加脚本即可。 + + +![Script](https://mdn.alipayobjects.com/huamei_fvsq9p/afts/img/A*Qw0rTbQPyWYAAAAAAAAAAAAADqiTAQ/original) \ No newline at end of file diff --git a/docs/script/edit.md b/docs/script/edit.md new file mode 100644 index 000000000..9033a6ea5 --- /dev/null +++ b/docs/script/edit.md @@ -0,0 +1,78 @@ +--- +order: 3 +title: 编辑脚本 +type: 脚本 +label: Script +--- + +Galacean Editor 提供了一个功能强大的代码编辑器,提供了代码提示,第三方包引入,引擎事件调试,脚本参数调试,项目实时预览等多种能力,可帮助你快速编辑和调试代码。 + +![image-20240318173952160](https://gw.alipayobjects.com/zos/OasisHub/2707ed27-a85a-4915-9db0-1cbed9e5c3dc/image-20240318173952160.png) + +| 序号 | 区域 | 说明 | +| ---- | ------------ | ------------------------------------------------------------ | +| 1 | 文件列表 | 查看项目中的所有脚本文件 | +| 2 | 代码编辑区 | 编辑脚本文件,支持代码高亮,代码提示,代码格式化等功能 | +| 3 | 预览区 | 预览当前脚本的运行效果。保存代码后会实时刷新此区域的渲染状态 | +| 4 | 包依赖管理区 | 添加需要依赖的 [NPM](https://www.npmjs.org/) 三方包,比如 [tween.js](https://www.npmjs.com/package/@tweenjs/tween.js) | +| 5 | 事件调试区 | 代码编辑器会自动检索所有绑定到引擎中的事件并显示在这里,你可以在这里触发事件,也可以配置事件的参数 | +| 5 | 控制台 | 查看代码运行时的日志信息 | + +想要了解更多关于代码编辑器的信息,请查看[代码编辑器](/docs/script-edit)。 + + + +## 代码编辑 + +在场景编辑器中创建脚本资产后,双击该脚本即可打开代码编辑器。Galacean 中的脚本需使用 [Typescript](https://www.typescriptlang.org/) 语言编写,同时新脚本默认基于内置模板创建。另外,Galacean 的代码编辑器基于 Monaco,快捷键与 VSCode 类似。修改脚本后,按 `Ctrl/⌘ + S` 保存,右侧实时预览区展现最新场景效果。 + +> 提示:Galacean 代码编辑器目前支持 `.ts` `.gs` 和 `.glsl` 的文件编辑 + +## 文件预览 + +Code Editor Snapshot + +1. **文件搜索** 可快速搜索项目中的文件 +2. **代码筛选** 文件树是否仅显示代码文件 ( `.ts` `.gs` `.glsl` ) +3. **内置文件** 用来显示哪些文件是不可编辑的内部文件 +4. **展开/隐藏** 可切换文件夹的展开或隐藏 +5. **代码文件** 可编辑的代码文件会显示对应的文件类型的缩略图标 + +## 引入第三方包 + +代码编辑器内置了与项目相对应的引擎,可自动提供引擎 API 的智能提示,从而帮助你快速实现基于引擎的逻辑。但大多数情况下你都需要引入 Galacean 生态包或其他第三方包来增强功能。 + +Code Editor Snapshot + +1. **搜索框** 在搜索框输入包名,按下 回车键,即可快速拉取包的版本列表 +2. **版本选择** 默认情况下使用 `latest` 版本 +3. **导入按钮** 选择好包名和版本,点击导入按钮即可将三方包的类型信息加载到工作区 +4. **包列表** 此处会列出当前项目依赖的所有第三方包 +5. **版本切换** 此处可切换已导入的包的版本,切换后会将新的类型信息加载到工作区 + +> 试一下:在搜索框输入 `@galacean/engine-toolkit`,点击「引入」按钮,然后在代码中使用 `import { OrbitControl } from '@galacean/engine-toolkit` 来引入自由相机组件。 + +## 实时预览区 + +Galacean 的代码编辑器提供实时预览功能。保存代码后,预览区会自动更新,从而让你快速查看代码的执行结果。 + +Code Editor Snapshot + +1. **拖动按钮** 按住来拖动模拟器。将模拟器拖到屏幕右边缘,即可将其固定在右侧面板上。 +2. **统计信息切换** 点击来切换场景统计信息的显示状态 +3. **新窗口打开** 在新窗口中来打开项目预览页面 +4. **脚本参数编辑** 如果当前场景中激活的脚本,拥有可配置的参数,即可打开此面板来实时调整脚本参数。想要了解脚本参数的详细信息,请查看 [脚本系统](/docs/script-attributes) +5. **关闭按钮** 点击来关闭模拟器。关闭后,屏幕右上方提供一个显示按钮,点击即可重新打开模拟器 + +## 事件调试 + +在代码编辑器中,我们提供了一个事件调试面板,从而帮助你快速调试场景中的事件。 + +Code Editor Snapshot + +1. **事件列表** Galacean Editor 会自动收集场景中的所有事件名并显示在这里 +2. **事件参数配置** 你可以点击此按钮来配置触发事件时所携带的参数,参数的配置使用 `JSON` 格式编写 +3. **事件触发按钮** 点击此按钮会触发场景中的对应事件 + +> 注意,脚本组件一定要绑定到某个实体上,才会展示事件列表。 + diff --git a/docs/xr/camera.md b/docs/xr/camera.md new file mode 100644 index 000000000..9dd90d2c7 --- /dev/null +++ b/docs/xr/camera.md @@ -0,0 +1,35 @@ +--- +order: 3 +title: 相机管理器 +type: XR +label: XR +--- + +相机管理器从属于 XRManager 实例,你可以通过 `xrManager.cameraManager` 获取。 + +## 属性 + +| 属性 | 类型 | 解释 | +| :------------- | :----- | :--------------------- | +| fixedFoveation | number | 设置相机的固定视觉焦点 | + +## 方法 + +| 方法 | 解释 | +| :----------- | :------------------------------------------- | +| attachCamera | 将虚拟世界的摄像头与现实世界的摄像头绑定 | +| detachCamera | 解除虚拟世界的摄像头与现实世界的摄像头的绑定 | + +## 更新流程 + +只需将`现实相机`的参数和姿态完全同步给`虚拟相机`,`现实场景`和`虚拟场景`就可以保持**同步**。 + +```mermaid +flowchart TD + A[时间片开始] --> B[获取现实相机数据] + B --> C[将姿态同步给虚拟相机] + C --> D[将 viewport 同步给虚拟相机] + D --> E[将投影矩阵同步给虚拟相机] + E --> F[时间片结束] + F --> A +``` diff --git a/docs/xr/compatibility.md b/docs/xr/compatibility.md new file mode 100644 index 000000000..8d009cc46 --- /dev/null +++ b/docs/xr/compatibility.md @@ -0,0 +1,53 @@ +--- +order: 7 +title: XR 兼容性 +type: XR +label: XR +--- + +XR 系统支持多后端(可参照[XR 总览](/docs/xr-overall)),目前官方仅适配了 WebXR 标准,因此 XR 互动的兼容性也**受限于设备对 WebXR** 的兼容。 + +在使用 XR 能力前,可参考 [CanIUse](https://caniuse.com/?search=webxr) 对运行环境进行评估,下方是当下 WebXR 兼容性的概括。 + +## 设备支持 + +### PC + +- 支持 WebXR 的 PC 端浏览器(本文使用 Mac Chrome) +- PC 端 Chrome 安装 [Immersive Web Emulator](https://chromewebstore.google.com/detail/immersive-web-emulator/cgffilbpcibhmcfbgggfhfolhkfbhmik) 或其他 WebXR 模拟插件 + +### 安卓 + +- 支持 WebXR 的终端与浏览器(本文使用安卓机与安卓机搭载的移动端 Chrome 应用) +- 安卓手机需额外安装 [Google Play Services for AR](https://play.google.com/store/apps/details?id=com.google.ar.core&hl=en_US&pli=1) + +### IOS + +- 苹果手机端 Safari 暂不支持 WebXR +- Apple Vision Pro 支持 WebXR + +### 头显设备 + +视情况而定,可参考头显官网对兼容性的说明,大部分头显中的浏览器(内核为 Chromium 的浏览器)都支持 WebXR + +## 运行时兼容性判断 + +在 runtime 中,您可以通过如下代码判断当前环境是否支持 `AR` 或 `VR`: + +```typescript +// 判断是否支持 AR +xrManager.sessionManager.isSupportedMode(XRSessionMode.AR); +``` + +在添加功能前,您可以通过如下代码判断该功能的兼容性: + +```typescript +// 判断是否支持图片追踪 +xrManager.isSupportedFeature(XRImageTracking); +``` + +## 安卓开启试验功能 + +安卓支持某些试验功能,但是开关时默认关闭的,此时可以通过设置 flags 打开,**安卓打开 Chrome** -> **登陆 chrome://flags** -> **搜索 WebXR** -> **打开 WebXR Incubations** + +image.png diff --git a/docs/xr/features.md b/docs/xr/features.md new file mode 100644 index 000000000..23361ca72 --- /dev/null +++ b/docs/xr/features.md @@ -0,0 +1,120 @@ +--- +order: 6 +title: XR 能力 +type: XR +label: XR +--- + +Galacean XR 目前包含以下能力: + +| 能力 | 解释 | +| :-------------- | :------- | +| Anchor Tracking | 锚点追踪 | +| Plane Tracking | 平面追踪 | +| Image Tracking | 图片追踪 | +| Hit Test | 碰撞检测 | + +## 锚点追踪 + +| 属性 | 解释 | +| :-------------- | :------------------------- | +| trackingAnchors | (只读)获取请求追踪的锚点 | +| trackedAnchors | (只读)获取追踪到的锚点 | + +| 方法 | 解释 | +| :-------------------- | :--------------------- | +| addAnchor | 添加特定锚点 | +| removeAnchor | 移除特定锚点 | +| clearAnchors | 移除所有锚点 | +| addChangedListener | 添加监听锚点变化的函数 | +| removeChangedListener | 移除监听锚点变化的函数 | + +你可以通过如下代码在 XR 空间中添加锚点: + +```typescript +const anchorTracking = xrManager.getFeature(XRAnchorTracking); +const position = new Vector3(); +const rotation = new Quaternion(); +// 添加一个锚点 +const anchor = anchorTracking.addAnchor(position, rotation); +// 移除这个锚点 +anchorTracking.removeAnchor(anchor); +// 监听锚点变化 +anchorTracking.addChangedListener( + ( + added: readonly XRAnchor[], + updated: readonly XRAnchor[], + removed: readonly XRAnchor[] + ) => { + // 此处添加对新增锚点,更新锚点和移除锚点的处理 + } +); +``` + +## 平面追踪 + +| 属性 | 解释 | +| :------------ | :----------------------------------------- | +| detectionMode | (只读)追踪屏幕的类型,水平,竖直或者所有 | +| trackedPlanes | (只读)获取追踪到的平面 | + +| 方法 | 解释 | +| :-------------------- | :--------------------- | +| addChangedListener | 添加监听平面变化的函数 | +| removeChangedListener | 移除监听平面变化的函数 | + +> 需要注意的是,平面追踪在添加功能时就需要指定平面追踪的类型。 + +```typescript +// 在初始化时指定平面追踪的类型为所有 +xrManager.addFeature(XRPlaneTracking, XRPlaneMode.EveryThing); +``` + +我们可以追踪现实平面,并为他们标记透明的网格和坐标系: + + + +## 图片追踪 + +| 属性 | 解释 | +| :------------- | :----------------------------------------------- | +| trackingImages | (只读)请求追踪的图片数组,包含名称,来源与尺寸 | +| trackedImages | (只读)获取追踪到的图片 | + +| 方法 | 解释 | +| :-------------------- | :--------------------- | +| addChangedListener | 添加监听平面变化的函数 | +| removeChangedListener | 移除监听平面变化的函数 | + +> 需要注意的是,图片追踪在添加功能时就需要指定追踪的图片,并且在 WebXR 中,同张图片只会被追踪一次。 + +```typescript +// 在初始化时指定平面追踪的类型为所有 +xrManager.addFeature(XRImageTracking, [refImage]); +``` + +我们可以追踪现实图片,并为他们标记坐标系: + + + +## 碰撞检测 + +| 方法 | 解释 | +| :------------ | :------------------------------------------- | +| hitTest | 通过射线与现实空间的平面进行碰撞检测 | +| screenHitTest | 通过屏幕空间坐标与现实空间的平面进行碰撞检测 | + +```typescript +const pointer = engine.inputManager.pointers[0]; +// 获取平面触控点 +if (pointer) { + const hitTest = xrManager.getFeature(XRHitTest); + const { position } = pointer; + // 通过屏幕空间坐标与现实空间的平面进行碰撞检测 + const result = hitTest.screenHitTest( + position.x, + position.y, + TrackableType.Plane + ); +} +``` diff --git a/docs/xr/input.md b/docs/xr/input.md new file mode 100644 index 000000000..a27591dcf --- /dev/null +++ b/docs/xr/input.md @@ -0,0 +1,57 @@ +--- +order: 5 +title: 交互管理器 +type: XR +label: XR +--- + +交互管理器从属于 XRManager 实例,你可以通过 `xrManager.inputManager` 获取,他管理所有的输入设备,包括但不限于: + +- 手柄 +- 头显 +- 手 +- …… + +## 方法 + +| 方法 | 解释 | +| :--------------------------------- | :--------------------- | +| getTrackedDevice | 通过类型获取某个设备 | +| addTrackedDeviceChangedListener | 添加监听设备变化的函数 | +| removeTrackedDeviceChangedListener | 移除监听设备变化的函数 | + +## 使用 + +通过如下代码可以监听设备的更新信息: + +```typescript +const { inputManager } = xrManager.inputManager; +inputManager.addTrackedDeviceChangedListener( + (added: readonly XRInput[], removed: readonly XRInput[]) => { + // 此处添加对新增设备和移除设备的处理 + } +); +``` + +通过如下代码可以获取左手手柄的姿态: + +```typescript +const controller = inputManager.getTrackedDevice( + XRTrackedInputDevice.LeftController +); +// 手柄的姿态 +controller.gripPose.position; +controller.gripPose.rotation; +controller.gripPose.matrix; +// 是否按下 select 键 +controller.isButtonDown(XRInputButton.Select); +// 是否抬起 select 键 +controller.isButtonUp(XRInputButton.Select); +// 是否一直按着 select 键 +controller.isButtonHeldDown(XRInputButton.Select); +``` + +> `XRInputButton.Select` 对应 WebXR 原生 `XRInputSourceEventType.selectXXX` 事件 +> `XRInputButton.Squeeze` 对应 WebXR 原生 `XRInputSourceEventType.squeezeXXX` 事件 + + diff --git a/docs/xr/manager.md b/docs/xr/manager.md new file mode 100644 index 000000000..a1d9c5a0c --- /dev/null +++ b/docs/xr/manager.md @@ -0,0 +1,46 @@ +--- +order: 2 +title: XR 管理器 +type: XR +label: XR +--- + +XR 管理器从属于 Engine 实例,你可以通过 `engine.xrManager` 获取。它在 XR 中扮演着总控制器的角色,主要管理: + +- 串联 XR 的整体流程 +- [XR 相机](/docs/xr-camera) +- [XR 会话](/docs/xr-session) +- [XR 交互](/docs/xr-input) +- [XR 功能](/docs/xr-features) + +## 属性 + +| 属性 | 类型 | 解释 | +| :----- | :-------------------------- | :------------------------------------------ | +| origin | [Entity](/apis/core/#Entity) | XR 初始化时的原点,它连接虚拟世界与现实世界 | + +> 若你在编辑器中,将 origin 节点放置在 `(1,1,1)` 这个位置,那么可以理解为,当 XR 空间展开时,你在现实世界里坐标的参考原点是 `(1,1,1)` ,他们建立连接的方式是彼此之间有固定的转换公式。 + +## 方法 + +| 方法 | 解释 | +| :----------------- | :--------------- | +| isSupportedFeature | 是否支持某个功能 | +| addFeature | 添加特定 XR 功能 | +| getFeature | 获取特定 XR 功能 | +| getFeatures | 获取所有 XR 功能 | +| enterXR | 进入 XR 会话 | +| exitXR | 退出 XR 会话 | + +## 整体流程 + +依据上方的属性与方法,梳理一下 XR 的整体流程: + +```mermaid +flowchart TD + A[初始化 XR 后端] --> B[设置 XR Origin] + B --> C[连接 XR 相机] + C --> D[添加 XR 能力] + D --> E[进入 XR 会话] + E --> F[退出 XR 会话] +``` \ No newline at end of file diff --git a/docs/xr/overall.md b/docs/xr/overall.md new file mode 100644 index 000000000..0115c47dc --- /dev/null +++ b/docs/xr/overall.md @@ -0,0 +1,22 @@ +--- +order: 0 +title: XR 总览 +type: XR +label: XR +--- + +`XR` 是一个通用术语,用于描述扩展现实 `Extended Reality`的概念,它包含虚拟现实(Virtual Reality,VR)、增强现实(Augmented Reality,AR)、混合现实(Mixed Reality,MR)等。 + +Galacean 对 XR 做了干净灵活的设计: + +- 更加干净:不需 XR 能力时,包体不含任何 XR 逻辑,大小也不会增加分毫 +- 更加灵活:可插拔的功能,让开发更简单 +- 面向未来:多后端设计,后续可适配不同平台不同接口 + +image.png + +在本章节,你可以了解到: + +- [快速开发 XR 互动](/docs/xr-start):XR 工作流与调试 +- [XR 管理器](/docs/xr-manager):管理[相机](/docs/xr-camera),[会话](/docs/xr-session),[交互](/docs/xr-input),[功能](/docs/xr-features)等 +- [XR 兼容性](/docs/xr-compatibility):介绍当前 WebXR 的兼容性 diff --git a/docs/xr/session.md b/docs/xr/session.md new file mode 100644 index 000000000..c1d17707d --- /dev/null +++ b/docs/xr/session.md @@ -0,0 +1,37 @@ +--- +order: 4 +title: 会话管理器 +type: XR +label: XR +--- + +会话管理器从属于 XRManager 实例,你可以通过 `xrManager.sessionManager` 获取。 + +## 属性 + +| 属性 | 类型 | 解释 | +| :----------------- | :------------- | :----------------------- | +| mode | XRSessionMode | (只读)获取当前会话类型 | +| state | XRSessionState | (只读)获取当前会话状态 | +| supportedFrameRate | Float32Array | (只读)获取硬件支持的帧率 | +| frameRate | number | (只读)获取硬件运行的帧率 | + +## 方法 + +| 方法 | 解释 | +| :-------------- | :------------------- | +| isSupportedMode | 获取是否支持会话类型 | +| run | 运行会话 | +| stop | 停止会话 | + +> 在进入 XR 会话后,开发者可以随时运行或停止会话,需要注意的是,这个状态不影响引擎的 `run` 和 `pause` 。 + +```mermaid +flowchart TD + A[enter session] --> B[run] + B --> C[stop] + C --> D[run] + D --> E[stop] + E --> F[……] + F --> G[exit session] +``` diff --git a/docs/xr/start.md b/docs/xr/start.md new file mode 100644 index 000000000..d51b6d3fe --- /dev/null +++ b/docs/xr/start.md @@ -0,0 +1,80 @@ +--- +order: 1 +title: 快速开发 XR 互动 +type: XR +label: XR +--- + +开发 XR 互动的流程如下所示: + +```mermaid +flowchart LR + 创建XR项目 --> 编辑项目 --> 导出 --> 本地构建 --> PC预览 --> XR设备预览 --> 正式发布 +``` + +编辑项目的环节与其他项目无异,本文将以 XR 模版为例,重点叙述 XR 项目的难点,**本地构建**, **PC 预览**与 **XR 设备预览**。 + +## 前置准备 + +由于我们引入的后端为 WebXR ,以 AR 项目为例,需要准备的运行环境与 XR 设备如下: + +- 支持 WebXR 的 PC 端浏览器(本文使用 Mac Chrome) +- 支持 WebXR 的终端与浏览器(本文使用安卓机与安卓机搭载的移动端 Chrome 应用) +- 安卓手机需额外安装 [Google Play Services for AR](https://play.google.com/store/apps/details?id=com.google.ar.core&hl=en_US&pli=1) + +> `Google Play Services for AR` 是由谷歌开发的增强现实平台(ARCore),有些手机自带此 App ,若没有,可在应用商店搜索,下图为小米应用商城的搜索结果。 + +image.png + +### PC 端调试 + +PC 端 Chrome 推荐安装 [Immersive Web Emulator](https://chromewebstore.google.com/detail/immersive-web-emulator/cgffilbpcibhmcfbgggfhfolhkfbhmik),它是由 Meta 开发的可以让你在 Chrome 上便捷调试 WebXR 的工具,如下图所示,我们在用这款工具在 PC 端 Chrome 中模拟 XR 设备进行调试。 + +image.png + +> 上图左侧为 XR 业务面板视图区,右侧为开发者工具。 + +### 手机端调试 + +安卓机器在确认安装 `Google Play Services for AR` 后,可用 Chrome 打开 [AR 示例](https://immersive-web.github.io/webxr-samples/immersive-ar-session.html) 进行测试。 + +## XR 模版 + +在做好以上准备后,可以在**编辑器主页**的**菜单视图**侧依次**点击模版**-> **XR 模版**快速创建 XR 项目。 + +image.png + +## PC 端预览 + +按照如下命令行构建项目,即可在 PC 端调试: + +```bash +npm install +npm run https +``` + +随后在 Chrome 打开相应网址,即可调试 XR 项目。 + +image.png + +> WebXR 仅在安全环境(HTTPS)中可用,因此,构建项目调试时需启用 Https。 + +### 调试 + +如上文所述,在安装 `Immersive Web Emulator` 的前提下,依次 `打开开发者工具(F12)` -> `打开开发者工具(F12)` + +## 手机端预览 + +项目没有发布上线前,我们可以让手机与电脑在同一个局域网下进行测试。 + +image.png + +### 调试 + +请参考[远程调试安卓设备](https://developer.chrome.com/docs/devtools/remote-debugging?hl=zh-cn) + +> 在调试前确保手机开启 **`开发者选项`** ,且允许 **`USB 调试`** + +## 最佳实践 + +由于 XR 调试的困难,我们建议绝大部份的工作和验证在 PC 预览与调试阶段完成,这样可以显著提升开发效率。 diff --git a/examples/AStar.ts b/examples/AStar.ts new file mode 100644 index 000000000..be788e1a5 --- /dev/null +++ b/examples/AStar.ts @@ -0,0 +1,563 @@ +/** + * @title AStar + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*8TbMTLiQO0kAAAAAAAAAAAAADiR2AQ/original + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*1ejuTpeuUEwAAAAAAAAAAAAADiR2AQ/original + */ + +import { + BoxColliderShape, + Camera, + Color, + Entity, + Font, + MeshRenderer, + PrimitiveMesh, + Rect, + Script, + StaticCollider, + TextRenderer, + UnlitMaterial, + Vector2, + WebGLEngine, +} from "@galacean/engine"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import * as dat from "dat.gui"; + +class GripMap { + static StraightCost = 10; + static SlashCost = 14; + + width: number = 0; + height: number = 0; + version: number = 0; + + private _grids: Grid[] = []; + private _pool: Grid[] = []; + private _subArr: number[][] = [ + [0, -1], + [1, 0], + [0, 1], + [-1, 0], + [-1, -1], + [1, -1], + [1, 1], + [-1, 1], + ]; + + reset(width: number, height: number, data?: []) { + this.width = width; + this.height = height; + const { _grids: grids } = this; + const newLength = width * height; + const preLength = grids.length; + if (preLength < newLength) { + for (let i = preLength; i < newLength; i++) { + grids.push(this._createGrid()); + } + } else { + for (let i = newLength; i < preLength; i++) { + this._destroyGrid(grids.pop()); + } + } + // reset + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + grids[i + j * width].reset(i, j); + } + } + } + + random(factor: number) { + const { width, height } = this; + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const gridData = this.getGrid(i, j); + if (gridData) { + gridData.walkAble = Math.random() > factor; + } + } + } + } + + getGrid(x: number, y: number): Grid | null { + if (x >= 0 && y >= 0 && x < this.width && y < this.height) { + return this._grids[x + this.width * y]; + } else { + return null; + } + } + + findPath(start: Vector2, end: Vector2): Grid[] { + const time = window.Date.now(); + const binaryHeap = new BinaryHeap(this._compareFun); + const startGrid = this.getGrid(start.x, start.y); + let endGrid = this.getGrid(end.x, end.y); + let minHCost: number = Infinity; + const version = ++this.version; + let nearestGrid; + let currGrid = startGrid; + currGrid.G = 0; + currGrid.isClose = true; + + while (currGrid !== endGrid) { + currGrid.isInit || this._initNeighbors(currGrid); + const { neighbors } = currGrid; + for (let i = 0; i < 8; i++) { + const neighbor = neighbors[i]; + if (!neighbor || !neighbor.walkAble) { + continue; + } + const grid = neighbor.grid; + if (grid.version !== version) { + grid.isClose = false; + grid.isOpen = false; + grid.version = version; + } else { + if (grid.isClose) { + continue; + } + } + if (grid.isOpen) { + // update cost + const newG = currGrid.G + neighbor.cost; + if (newG < grid.G) { + grid.G = currGrid.G + neighbor.cost; + grid.F = grid.G + grid.H; + grid.parent = currGrid; + } + } else { + grid.G = currGrid.G + neighbor.cost; + grid.H = this._getH(grid, endGrid); + grid.F = grid.G + grid.H; + grid.parent = currGrid; + grid.isOpen = true; + binaryHeap.ins(grid); + } + } + + if (binaryHeap.isEmpty()) { + if (nearestGrid) { + endGrid = nearestGrid; + break; + } else { + return []; + } + } + + while (!binaryHeap.isEmpty()) { + const bestGrid = binaryHeap.pop(); + if (!bestGrid.isClose) { + bestGrid.isOpen = false; + bestGrid.isClose = true; + const distance = bestGrid.H; + if (distance < minHCost) { + nearestGrid = bestGrid; + minHCost = distance; + } + currGrid = bestGrid; + break; + } + } + } + + // reverse path + const resArr = [endGrid]; + currGrid = endGrid; + while (currGrid != startGrid) { + currGrid = currGrid.parent; + resArr.push(currGrid); + } + console.log("Spend time:" + (window.Date.now() - time) + " ms"); + return resArr; + } + + /** + * Add obstacles. + * @param rect - starting position and width and height + */ + addObstacle(rect: Rect): void { + const { x, y, width, height } = rect; + const endX = x + width; + const endY = y + height; + for (let i = x; i < endX; i++) { + for (let j = y; j < endY; j++) { + const grid = this.getGrid(i, j); + if (grid) { + grid.walkAble = false; + } + } + } + } + + private _compareFun(x: Grid, y: Grid): boolean { + return x.F < y.F; + } + + /** + * Manhattan distance + * @param from - start position + * @param to - end position + * @returns estimated cost + */ + private _getH(from: Grid, to: Grid): number { + let dx = from.x - to.x; + let dy = from.y - to.y; + dx = dx >= 0 ? dx : -dx; + dy = dy >= 0 ? dy : -dy; + // straight line distance + let a = dx > dy ? dx - dy : dy - dx; + // slash distance + let b = (dx + dy - a) / 2; + return a * GripMap.StraightCost + b * GripMap.SlashCost; + } + + private _initNeighbors(grid: Grid): void { + const { x, y, neighbors } = grid; + const { _subArr: subArr } = this; + for (let i = 0; i < 4; i++) { + let testNode = this.getGrid(x + subArr[i][0], y + subArr[i][1]); + if (testNode) { + const link = new NeighborLink(testNode, GripMap.StraightCost); + link.walkAble = testNode.walkAble; + neighbors[i] = link; + } else { + neighbors[i] = null; + } + } + for (let i = 4; i < 8; i++) { + const expectX = x + subArr[i][0]; + const expectY = y + subArr[i][1]; + let testNode = this.getGrid(expectX, expectY); + if (testNode) { + const link = new NeighborLink(testNode, GripMap.SlashCost); + if (testNode.walkAble) { + const gridDataA = this.getGrid(expectX, y); + const gridDataB = this.getGrid(x, expectY); + const walkAbleA = gridDataA ? gridDataA.walkAble : false; + const walkAbleB = gridDataB ? gridDataB.walkAble : false; + link.walkAble = walkAbleA || walkAbleB; + } else { + link.walkAble = false; + } + neighbors[i] = link; + } else { + neighbors[i] = null; + } + } + grid.isInit = true; + } + + private _createGrid(): Grid { + if (this._pool.length > 0) { + return this._pool.pop(); + } else { + return new Grid(); + } + } + + private _destroyGrid(grid: Grid): void { + this._grids.push(grid); + } +} + +class Grid { + parent: Grid; + x: number; + y: number; + neighbors: (NeighborLink | null)[] = []; + G: number; + H: number; + F: number; + + walkAble: boolean = false; + isOpen: boolean = false; + isClose: boolean = false; + isInit: boolean = false; + version: number = 0; + + reset(x: number, y: number, walkAble: boolean = true) { + this.x = x; + this.y = y; + this.walkAble = walkAble; + this.G = this.H = this.F = 0; + this.isOpen = this.isClose = this.isInit = false; + this.version = -1; + } +} + +class NeighborLink { + grid: Grid; + cost: number; + walkAble: boolean; + constructor(grid: Grid, cost: number) { + this.grid = grid; + this.cost = cost; + } +} + +/** + * Min heap + */ +class BinaryHeap { + arr: Array = []; + compareFun: Function = function (x: any, y: any): Boolean { + return x < y; + }; + + ins(value: Object): void { + const { arr } = this; + let curI = arr.length; + arr[curI] = value; + let curParentI = curI >> 1; + while (curI > 1 && this.compareFun(this.arr[curI], this.arr[curParentI])) { + let temp = arr[curI]; + arr[curI] = arr[curParentI]; + arr[curParentI] = temp; + curI = curParentI; + curParentI = curI >> 1; + } + } + + isEmpty(): boolean { + return this.arr.length < 2; + } + + pop(): Object { + const { arr } = this; + let min: Object = arr[1]; + arr[1] = arr[arr.length - 1]; + arr.pop(); + const l = arr.length; + let curI = 1; + let leftI = curI << 1; + let rightI = leftI + 1; + let minI: number; + while (leftI < l) { + if (rightI < l) { + minI = this.compareFun(arr[rightI], arr[leftI]) ? rightI : leftI; + } else { + minI = leftI; + } + if (this.compareFun(arr[minI], arr[curI])) { + let temp: Object = arr[curI]; + arr[curI] = arr[minI]; + arr[minI] = temp; + curI = minI; + leftI = curI << 1; + rightI = leftI + 1; + } else { + break; + } + } + return min; + } + + constructor(justMinFun: (a: never, b: never) => boolean) { + justMinFun && (this.compareFun = justMinFun); + this.arr.push(-1); + } +} + +enum FindingPathStep { + SetStart, + SetEnd, + Finish, +} + +class MapViewControl extends Script { + map: GripMap; + private _path: Grid[]; + private _gridEntities: Entity[][] = []; + private _tempColor: Color = new Color(); + private _tempStartVec: Vector2 = new Vector2(); + private _tempEndVec: Vector2 = new Vector2(); + private _step: FindingPathStep = FindingPathStep.SetStart; + + set path(val: Grid[]) { + this._path = val; + } + + reset() { + this._path && (this._path.length = 0); + this.map.reset(50, 50); + this.map.random(0.3); + this.clearView(); + this._step = FindingPathStep.SetStart; + this.engine.dispatch("StateChange", this._step); + } + + clearView() { + const { map, _gridEntities: gridEntities } = this; + const { width, height } = map; + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const gridData = map.getGrid(i, j); + if (!gridData) { + continue; + } + const gridEntity = gridEntities[i][j]; + const gridRenderer = gridEntity.getComponent(MeshRenderer); + const gridMaterial = gridRenderer.getMaterial() as UnlitMaterial; + gridMaterial.baseColor = this._getColor(gridData); + } + } + } + + drawPath() { + const { map, _gridEntities: gridEntities, _path: path } = this; + const { width, height } = map; + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const gridData = map.getGrid(i, j); + if (!gridData) { + continue; + } + const gridEntity = gridEntities[i][j]; + const gridRenderer = gridEntity.getComponent(MeshRenderer); + const gridMaterial = gridRenderer.getMaterial() as UnlitMaterial; + if (path.includes(gridData)) { + gridMaterial.baseColor = new Color(0, 1, 0, 1); + } + } + } + } + + onAwake() { + this.map = new GripMap(); + const { map, entity, engine, _gridEntities: gridEntities } = this; + map.reset(50, 50); + map.random(0.3); + const { width, height } = map; + this._getColor = this._getColor.bind(this); + for (let i = 0; i < width; i++) { + gridEntities[i] ||= new Array(height); + for (let j = 0; j < height; j++) { + const gridData = map.getGrid(i, j); + const gridEntity = (gridEntities[i][j] = entity.createChild( + i + "_" + j + )); + gridEntity.transform.setScale(0.9, 1, 0.9); + const gridRenderer = gridEntity.addComponent(MeshRenderer); + gridRenderer.mesh = PrimitiveMesh.createPlane(engine, 1, 1); + const gridMaterial = new UnlitMaterial(engine); + gridRenderer.setMaterial(gridMaterial); + gridEntity.transform.setPosition(i, 0, j); + if (gridData) { + if (gridData.walkAble) { + gridMaterial.baseColor = new Color(1, 1, 1, 1); + } else { + gridMaterial.baseColor = new Color(1, 0, 0, 1); + } + const collider = gridEntity.addComponent(StaticCollider); + const colliderShape = new BoxColliderShape(); + colliderShape.size.set(1, 1, 1); + collider.addShape(colliderShape); + const gridControl = gridEntity.addComponent(Script); + gridControl.onPointerEnter = () => { + gridMaterial.baseColor = new Color(0, 0, 1, 1); + }; + gridControl.onPointerExit = () => { + gridMaterial.baseColor = this._getColor(gridData); + }; + gridControl.onPointerClick = () => { + const { x, y } = gridData; + switch (this._step) { + case FindingPathStep.SetStart: + this._tempStartVec.set(x, y); + this._path = [this.map.getGrid(x, y) as Grid]; + this._step = FindingPathStep.SetEnd; + break; + case FindingPathStep.SetEnd: + this._tempEndVec.set(x, y); + this._path = map.findPath(this._tempStartVec, this._tempEndVec); + this._step = FindingPathStep.Finish; + this.drawPath(); + break; + case FindingPathStep.Finish: + this._tempStartVec.set(x, y); + this._path = [this.map.getGrid(x, y) as Grid]; + this._step = FindingPathStep.SetEnd; + this.clearView(); + break; + default: + break; + } + engine.dispatch("StateChange", this._step); + }; + } else { + gridMaterial.baseColor = new Color(0, 0, 0, 1); + } + } + } + } + + private _getColor(grid: Grid) { + if (grid) { + if (grid.walkAble) { + if (this._path?.includes(grid)) { + this._tempColor.set(0, 1, 0, 1); + } else { + this._tempColor.set(1, 1, 1, 1); + } + } else { + this._tempColor.set(1, 0, 0, 1); + } + } else { + this._tempColor.set(0, 0, 0, 1); + } + return this._tempColor; + } +} + +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => { + // Create engine object. + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 5); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + camera.orthographicSize = 40; + + const textEntity = rootEntity.createChild("text"); + textEntity.transform.setPosition(0, 30, 0); + textEntity.transform.setScale(5, 5, 5); + const renderer = textEntity.addComponent(TextRenderer); + renderer.color = new Color(1, 1, 1, 1); + renderer.fontSize = 40; + renderer.font = Font.createFromOS(engine, "Arial"); + renderer.text = "Please select the starting point"; + engine.on("StateChange", (state: FindingPathStep) => { + switch (state) { + case FindingPathStep.SetStart: + renderer.text = "Please select the starting point"; + break; + case FindingPathStep.SetEnd: + renderer.text = "Please select the end point"; + break; + case FindingPathStep.Finish: + renderer.text = "Please select the starting point again"; + break; + default: + break; + } + }); + + const mapEntity = rootEntity.createChild("map"); + mapEntity.transform.setRotation(90, 0, 0); + mapEntity.transform.setPosition(-25, 25, 0); + const mapViewControl = mapEntity.addComponent(MapViewControl); + engine.run(); + + const gui = new dat.GUI(); + const guiData = { + reset: () => { + mapViewControl.reset(); + }, + }; + gui.add(guiData, "reset"); +}); diff --git a/examples/CSS-DOM.ts b/examples/CSS-DOM.ts new file mode 100644 index 000000000..7c40c45bf --- /dev/null +++ b/examples/CSS-DOM.ts @@ -0,0 +1,92 @@ +/** + * @title CSS DOM + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*fZ8pR6j51Q0AAAAAAAAAAAAADiR2AQ/original + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*FBjgQJeAQwsAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + Camera, + Entity, + GLTFResource, + Logger, + Script, + Vector3, + WebGLEngine, + WebGLMode, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit"; + +async function main() { + // Create engine + const htmlCanvas = document.getElementById("canvas") as HTMLCanvasElement; + const engine = await WebGLEngine.create({ + canvas: htmlCanvas, + graphicDeviceOptions: { webGLMode: WebGLMode.Auto }, + }); + + engine.canvas.resizeByClientSize(); + + // Create root entity + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.setPosition(0, 1.5, 5); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Load glTF asset + const glTFResource = await engine.resourceManager.load( + "https://gw.alipayobjects.com/os/bmw-prod/8d36415b-5905-461f-9336-68a23d41518e.gltf" + ); + const defaultSceneRoot = glTFResource.defaultSceneRoot; + rootEntity.addChild(defaultSceneRoot); + + // Add dom element + const dom = document.createElement("div"); + dom.innerHTML = "Hello world!!!"; + dom.setAttribute( + "style", + "padding:10px;position:absolute;top:0;left:0;background:white;border-radius:5px" + ); + document.body.appendChild(dom); + + // Add script + const script = defaultSceneRoot.addComponent(LocationTrackingScript); + script.htmlCanvas = htmlCanvas; + script.camera = camera; + script.dom = dom; + + // Run engine + engine.run(); +} + +main(); + +class LocationTrackingScript extends Script { + screenPoint: Vector3 = new Vector3(); + widthRatio: number; + heightRatio: number; + camera: Camera; + htmlCanvas: HTMLCanvasElement; + dom: HTMLDivElement; + + onStart() { + const canvas = this.engine.canvas; + this.widthRatio = canvas.width / this.htmlCanvas.clientWidth; + this.heightRatio = canvas.height / this.htmlCanvas.clientHeight; + } + + onUpdate() { + // Convert world coordinates to screen coordinates + this.camera.worldToScreenPoint( + this.entity.transform.position, + this.screenPoint + ); + const style = this.dom.style; + style.left = `${this.screenPoint.x / this.widthRatio}px`; + style.top = `${this.screenPoint.y / this.heightRatio}px`; + } +} diff --git a/examples/ambient-light.ts b/examples/ambient-light.ts new file mode 100644 index 000000000..5818fba7c --- /dev/null +++ b/examples/ambient-light.ts @@ -0,0 +1,110 @@ +/** + * @title AmbientLight222 + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*7LKBQ4bsMiEAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AmbientLight, + AssetType, + BackgroundMode, + Camera, + DiffuseMode, + DirectLight, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + SkyBoxMaterial, + Vector3, + WebGLEngine, + Logger, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const gui = new dat.GUI(); + + // Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.position = new Vector3(-3, 0, 3); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + // Create sky + const sky = scene.background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + scene.background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + const lightEntity = rootEntity.createChild(); + lightEntity.addComponent(DirectLight).intensity = 0.5; + lightEntity.transform.setPosition(-5, 5, 5); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // material ball + const ball = rootEntity.createChild("ball"); + const ballRender = ball.addComponent(MeshRenderer); + const material = new PBRMaterial(engine); + material.metallic = 0; + material.roughness = 0; + ballRender.mesh = PrimitiveMesh.createSphere(engine, 1, 64); + ballRender.setMaterial(material); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/6470ea5e-094b-4a77-a05f-4945bf81e318.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + openDebug(ambientLight.specularTexture); + engine.run(); + }); + + function openDebug(specularTexture) { + const info = { + diffuseMode: "SphericalHarmonics", + diffuseSolidColor: [0.212 * 255, 0.227 * 255, 0.259 * 255], + specularTexture: true, + }; + + gui + .add(info, "diffuseMode", ["SolidColor", "SphericalHarmonics"]) + .onChange((v) => { + if (v === "SphericalHarmonics") { + scene.ambientLight.diffuseMode = DiffuseMode.SphericalHarmonics; + } else if (v === "SolidColor") { + scene.ambientLight.diffuseMode = DiffuseMode.SolidColor; + } + }); + + gui.addColor(info, "diffuseSolidColor").onChange((v) => { + scene.ambientLight.diffuseSolidColor.set( + v[0] / 255, + v[1] / 255, + v[2] / 255, + 1 + ); + }); + + gui.add(info, "specularTexture").onChange((v) => { + if (v) { + scene.ambientLight.specularTexture = specularTexture; + } else { + scene.ambientLight.specularTexture = null; + } + }); + + // env light debug + gui.add(scene.ambientLight, "specularIntensity", 0, 1); + gui.add(scene.ambientLight, "diffuseIntensity", 0, 1); + } +}); diff --git a/examples/animation-customAnimationClip.ts b/examples/animation-customAnimationClip.ts new file mode 100644 index 000000000..185f4a2d2 --- /dev/null +++ b/examples/animation-customAnimationClip.ts @@ -0,0 +1,235 @@ +/** + * @title Animation CustomAnimationClip + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*6_1JQKX5m0UAAAAAAAAAAAAADiR2AQ/original + */ +import { + AnimationClip, + AnimationColorCurve, + AnimationFloatCurve, + AnimationVector3Curve, + Animator, + AnimatorController, + AnimatorControllerLayer, + AnimatorStateMachine, + Camera, + Color, + DirectLight, + GLTFResource, + Keyframe, + Logger, + SpotLight, + SystemInfo, + Transform, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraWrap = rootEntity.createChild("camera_wrap"); + const cameraEntity = cameraWrap.createChild("camera"); + cameraEntity.transform.position = new Vector3(0, 3, 8); + cameraEntity.transform.rotation = new Vector3(-2, 0, 0); + cameraEntity.addComponent(Camera); + + const lightWrap = rootEntity.createChild("light_wrap"); + + const directLightEntity = lightWrap.createChild("light_node"); + const directLight = directLightEntity.addComponent(DirectLight); + directLight.intensity = 0.6; + directLightEntity.transform.lookAt(new Vector3(0, 0, 1)); + directLightEntity.transform.rotate(new Vector3(0, 90, 0)); + + const spotLightEntity = lightWrap.createChild("spotLight1"); + const spotLightEntity2 = spotLightEntity.clone(); + spotLightEntity2.name = "spotLight2"; + spotLightEntity2.transform.setRotation(-120, 0, 0); + lightWrap.addChild(spotLightEntity2); + + const spotLight = spotLightEntity.addComponent(SpotLight); + spotLight.angle = Math.PI / 60; + spotLightEntity.transform.setPosition(0, 8, 0); + spotLightEntity.transform.setRotation(-60, 0, 0); + const spotLight2 = spotLightEntity2.addComponent(SpotLight); + spotLight2.angle = Math.PI / 60; + spotLightEntity2.transform.setPosition(0, 8, 0); + spotLightEntity2.transform.setRotation(-60, 0, 0); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/ca50859b-d736-4a3e-9fc3-241b0bd2afef.gltf" + ) + .then((gltfResource) => { + const { defaultSceneRoot } = gltfResource; + defaultSceneRoot.transform.setScale(0.05, 0.05, 0.05); + rootEntity.addChild(defaultSceneRoot); + }); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/244228a7-361c-4c63-a790-dd9e19d12e78/data.gltf" + ) + .then((gltfResource) => { + const { defaultSceneRoot, animations = [] } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + + const sceneAnimator = rootEntity.addComponent(Animator); + sceneAnimator.animatorController = new AnimatorController(); + const layer = new AnimatorControllerLayer("base"); + sceneAnimator.animatorController.addLayer(layer); + const stateMachine = (layer.stateMachine = new AnimatorStateMachine()); + const sceneState = stateMachine.addState("sceneAnim"); + const sceneClip = (sceneState.clip = new AnimationClip("sceneAnim")); + + //custom rotate curve + const rotateCurve = new AnimationVector3Curve(); + const key1 = new Keyframe(); + key1.time = 0; + key1.value = new Vector3(0, 0, 0); + const key2 = new Keyframe(); + key2.time = 15; + key2.value = new Vector3(0, 360, 0); + rotateCurve.addKey(key1); + rotateCurve.addKey(key2); + + //custom color curve + const colorCurve = new AnimationColorCurve(); + const key3 = new Keyframe(); + key3.time = 0; + key3.value = new Color(1, 0, 0, 1); + const key4 = new Keyframe(); + key4.time = 5; + key4.value = new Color(0, 1, 0, 1); + const key5 = new Keyframe(); + key5.time = 10; + key5.value = new Color(0, 0, 1, 1); + const key6 = new Keyframe(); + key6.time = 15; + key6.value = new Color(1, 0, 0, 1); + colorCurve.addKey(key3); + colorCurve.addKey(key4); + colorCurve.addKey(key5); + colorCurve.addKey(key6); + + const color2Curve = new AnimationColorCurve(); + const key16 = new Keyframe(); + key16.time = 0; + key16.value = new Color(0, 0, 1, 1); + const key17 = new Keyframe(); + key17.time = 5; + key17.value = new Color(0, 1, 0, 1); + const key18 = new Keyframe(); + key18.time = 10; + key18.value = new Color(1, 0, 0, 1); + const key19 = new Keyframe(); + key19.time = 15; + key19.value = new Color(0, 0, 1, 1); + color2Curve.addKey(key16); + color2Curve.addKey(key17); + color2Curve.addKey(key18); + color2Curve.addKey(key19); + + //custom fov curve + const fovCurve = new AnimationFloatCurve(); + const key7 = new Keyframe(); + key7.time = 0; + key7.value = 45; + const key8 = new Keyframe(); + key8.time = 8; + key8.value = 80; + const key9 = new Keyframe(); + key9.time = 15; + key9.value = 45; + + fovCurve.addKey(key7); + fovCurve.addKey(key8); + fovCurve.addKey(key9); + + //custom spotLight1 rotate curve + const spotLight1RotateCurve = new AnimationVector3Curve(); + const key10 = new Keyframe(); + key10.time = 0; + key10.value = new Vector3(-60, 0, 0); + const key11 = new Keyframe(); + key11.time = 10; + key11.value = new Vector3(-120, 0, 0); + const key12 = new Keyframe(); + key12.time = 15; + key12.value = new Vector3(-60, 0, 0); + spotLight1RotateCurve.addKey(key10); + spotLight1RotateCurve.addKey(key11); + spotLight1RotateCurve.addKey(key12); + + //custom spotLight2 rotate curve + const spotLight2RotateCurve = new AnimationVector3Curve(); + const key13 = new Keyframe(); + key13.time = 0; + key13.value = new Vector3(-120, 0, 0); + const key14 = new Keyframe(); + key14.time = 10; + key14.value = new Vector3(-60, 0, 0); + const key15 = new Keyframe(); + key15.time = 15; + key15.value = new Vector3(-120, 0, 0); + spotLight2RotateCurve.addKey(key13); + spotLight2RotateCurve.addKey(key14); + spotLight2RotateCurve.addKey(key15); + + sceneClip.addCurveBinding( + "/light_wrap/spotLight1", + SpotLight, + "color", + colorCurve + ); + sceneClip.addCurveBinding( + "/light_wrap/spotLight1", + Transform, + "rotation", + spotLight1RotateCurve + ); + sceneClip.addCurveBinding( + "/light_wrap/spotLight2", + Transform, + "rotation", + spotLight2RotateCurve + ); + sceneClip.addCurveBinding( + "/light_wrap/spotLight2", + SpotLight, + "color", + color2Curve + ); + sceneClip.addCurveBinding( + "/light_wrap", + Transform, + "rotation", + rotateCurve + ); + // curve can be reused + sceneClip.addCurveBinding( + "/camera_wrap", + Transform, + "rotation", + rotateCurve + ); + sceneClip.addCurveBinding( + "/camera_wrap/camera", + Camera, + "fieldOfView", + fovCurve + ); + + sceneAnimator.play("sceneAnim", 0); + animator.play(animations[0].name, 0); + }); + + engine.run(); +}); diff --git a/examples/animation-event.ts b/examples/animation-event.ts new file mode 100644 index 000000000..c64b36c91 --- /dev/null +++ b/examples/animation-event.ts @@ -0,0 +1,107 @@ +/** + * @title Animation Event + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*yQzHTaTZMs0AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AnimationEvent, + Animator, + Camera, + DirectLight, + GLTFResource, + Script, + SystemInfo, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +async function main() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + // initText + const textEntity = rootEntity.createChild("text"); + const textRenderer = textEntity.addComponent(TextRenderer); + textEntity.transform.setPosition(0, 2, 0); + textRenderer.fontSize = 12; + textRenderer.text = ""; + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((gltfResource) => { + const { defaultSceneRoot, animations } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + + const state = animator.findAnimatorState("walk"); + const clip = state.clip; + + const event0 = new AnimationEvent(); + event0.functionName = "event0"; + event0.time = 0.5; + clip.addEvent(event0); + + const event1 = new AnimationEvent(); + event1.functionName = "event1"; + event1.time = clip.length; + clip.addEvent(event1); + + defaultSceneRoot.addComponent( + class extends Script { + event0(): void { + textRenderer.text = "event0 called"; + } + + event1(): void { + textRenderer.text = "event1 called"; + } + } + ); + + animator.play("walk", 0); + + initDatGUI(animator, animations); + }); + + engine.run(); + + const initDatGUI = (animator, animations) => { + const animationNames = animations + .filter((clip) => !clip.name.includes("pose")) + .map((clip) => clip.name); + const debugInfo = { + animation: animationNames[4], + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + textRenderer.text = ""; + animator.play(v); + }); + + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +} +main(); diff --git a/examples/animation-sprite.ts b/examples/animation-sprite.ts new file mode 100644 index 000000000..67ed5701a --- /dev/null +++ b/examples/animation-sprite.ts @@ -0,0 +1,114 @@ +/** + * @title Sprite Animation + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*RXETR7ycqwoAAAAAAAAAAAAADiR2AQ/original + */ +import { + AnimatorController, + AnimatorControllerLayer, + AnimatorStateMachine, + AssetType, + Camera, + Sprite, + SpriteAtlas, + SpriteRenderer, + Vector3, + WebGLEngine, + AnimationRefCurve, + AnimationRectCurve, + Keyframe, + ReferResource, + AnimationClip, + Animator, + Texture2D, + Rect, + InterpolationType, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 4); + cameraEntity.addComponent(Camera).isOrthographic = true; + + const layer = new AnimatorControllerLayer("base"); + + const stateMachine = (layer.stateMachine = new AnimatorStateMachine()); + const atlasState = stateMachine.addState("spriteAtlas"); + const regionState = stateMachine.addState("spriteRegion"); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/da0bccd4-020a-41d5-82e0-a04f4413d9a6.atlas", + type: AssetType.SpriteAtlas, + }) + .then((atlas) => { + const spriteEntity = rootEntity.createChild(); + spriteEntity.transform.position = new Vector3(); + spriteEntity.transform.scale.set(100 / 32, 100 / 32, 100 / 32); + spriteEntity.addComponent(SpriteRenderer).sprite = + atlas.getSprite("npcs-11"); + + const spriteCurve = new AnimationRefCurve(); + for (let i = 0; i < 10; ++i) { + const key = new Keyframe(); + key.time = i; + key.value = atlas.getSprite(`npcs-${i}`); + spriteCurve.addKey(key); + } + const spriteClip = new AnimationClip("sprite"); + spriteClip.addCurveBinding("", SpriteRenderer, "sprite", spriteCurve); + atlasState.clip = spriteClip; + + const animator = spriteEntity.addComponent(Animator); + const animatorController = new AnimatorController(); + animator.animatorController = animatorController; + animatorController.addLayer(layer); + animator.play(atlasState.name); + }); + + // Load texture and create sprite sheet animation. + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*9nsHSpx28rAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + const spriteEntity = rootEntity.createChild("Sprite"); + const sprite = new Sprite(engine, texture); + spriteEntity.addComponent(SpriteRenderer).sprite = sprite; + const region = sprite.region; + spriteEntity.transform.position = new Vector3(2, 0, 0); + const spriteCurve = new AnimationRectCurve(); + spriteCurve.interpolation = InterpolationType.Step; + for (let i = 0; i <= 3; ++i) { + const key = new Keyframe(); + key.time = i; + key.value = new Rect((i / 3) % 1, 0, 0.32, 1); + spriteCurve.addKey(key); + } + const spriteClip = new AnimationClip("sprite"); + spriteClip.addCurveBinding( + "", + SpriteRenderer, + "sprite.region", + spriteCurve + ); + regionState.clip = spriteClip; + + const animator = spriteEntity.addComponent(Animator); + const animatorController = new AnimatorController(); + animator.animatorController = animatorController; + animatorController.addLayer(layer); + animator.play(regionState.name); + }); + + + engine.run(); +}); diff --git a/examples/animation-stateMachineScript.ts b/examples/animation-stateMachineScript.ts new file mode 100644 index 000000000..dd28b208e --- /dev/null +++ b/examples/animation-stateMachineScript.ts @@ -0,0 +1,113 @@ +/** + * @title AnimatorStateScript + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*XA6qQozlnUwAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Animator, + AnimatorState, + Camera, + DirectLight, + GLTFResource, + Logger, + StateMachineScript, + SystemInfo, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + // initText + const textEntity = rootEntity.createChild("text"); + const textRenderer = textEntity.addComponent(TextRenderer); + textEntity.transform.setPosition(0, 2, 0); + textRenderer.fontSize = 12; + textRenderer.text = ""; + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((gltfResource) => { + const { animations = [], defaultSceneRoot } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + + const animator = defaultSceneRoot.getComponent(Animator); + const state = animator.findAnimatorState("walk"); + + state.addStateMachineScript( + class extends StateMachineScript { + onStateEnter( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + textRenderer.text = "onStateEnter"; + console.log("onStateEnter: ", animatorState); + } + + onStateUpdate( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + console.log("onStateUpdate: ", animatorState); + } + + onStateExit( + animator: Animator, + animatorState: AnimatorState, + layerIndex: number + ): void { + textRenderer.text = "onStateExit"; + console.log("onStateExit: ", animatorState); + } + } + ); + + animator.play("walk"); + + initDatGUI(animator, animations); + }); + + engine.run(); + + const initDatGUI = (animator: Animator, animations) => { + const animationNames = animations + .filter((clip) => !clip.name.includes("pose")) + .map((clip) => clip.name); + const debugInfo = { + animation: animationNames[4], + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + animator.crossFade(v, 0.5); + }); + + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +}); diff --git a/examples/assets-gc.ts b/examples/assets-gc.ts new file mode 100644 index 000000000..e8884512c --- /dev/null +++ b/examples/assets-gc.ts @@ -0,0 +1,51 @@ +/** + * @title Sprite Garbage Collection + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*iPKuQKHfp1QAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AssetType, + Camera, + Entity, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; +import { GUI } from "dat.gui"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + const { resourceManager } = engine; + + let root: Entity | null = engine.sceneManager.scenes[0].createRootEntity(); + root.createChild().addComponent(Camera).isOrthographic = true; + engine.canvas.resizeByClientSize(); + + resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + if (!root) return; + const entity = root.createChild(); + entity.transform.position.set(0, 0, -1); + const renderer = entity.addComponent(SpriteRenderer); + renderer.sprite = new Sprite(engine, texture); + engine.run(); + }); + + new GUI().add( + { + GC: () => { + if (!root) return; + root.destroy(); + root = null; + resourceManager.gc(); + }, + }, + "GC" + ); +}); diff --git a/examples/background/gui.ts b/examples/background/gui.ts new file mode 100644 index 000000000..ce04bcde9 --- /dev/null +++ b/examples/background/gui.ts @@ -0,0 +1,82 @@ +import * as dat from "dat.gui"; +import { + BackgroundMode, TextureCube, Background +} from "@galacean/engine"; + +export function addGUI(cubeMaps: TextureCube[], background: Background) { + const gui = new dat.GUI(); + + let colorGUI = null; + let cubeMapGUI = null; + let fitModeGUI = null; + + function hide(_gui) { + _gui.__li.style.display = "none"; + } + function show(_gui) { + _gui.__li.style.display = "block"; + } + background.mode = BackgroundMode.Texture; + gui + .add(background, "mode", { + Sky: BackgroundMode.Sky, + SolidColor: BackgroundMode.SolidColor, + Texture: BackgroundMode.Texture, + }) + .onChange((v) => { + const mode = (background.mode = parseInt(v)); + hide(colorGUI); + hide(cubeMapGUI); + hide(fitModeGUI); + switch (mode) { + case BackgroundMode.Sky: + show(cubeMapGUI); + break; + case BackgroundMode.SolidColor: + show(colorGUI); + break; + case BackgroundMode.Texture: + show(fitModeGUI); + break; + } + }); + + const solidColor = background.solidColor; + let colorObj = { + color: [ + solidColor.r / 255, + solidColor.g / 255, + solidColor.b / 255, + solidColor.a, + ], + }; + colorGUI = gui.addColor(colorObj, "color").onChange((v) => { + background.solidColor.set(v[0] / 255, v[1] / 255, v[2] / 255, v[3]); + }); + + const obj = { + cubeMap: 0, + }; + + const mode = { + fitMode: 1, + }; + + cubeMapGUI = gui + .add(obj, "cubeMap", { cubeMap1: 0, cubeMap2: 1 }) + .onChange((v) => { + // @ts-ignore + background.sky.material.texture = cubeMaps[parseInt(v)]; + }); + fitModeGUI = gui + .add(mode, "fitMode", { AspectFitWidth: 0, AspectFitHeight: 1, Fill: 2 }) + .onChange((v) => { + background.textureFillMode = parseInt(v); + }); + + // init + background.mode = BackgroundMode.Texture; + hide(colorGUI); + hide(cubeMapGUI); + show(fitModeGUI); +} \ No newline at end of file diff --git a/examples/background/index.ts b/examples/background/index.ts new file mode 100644 index 000000000..7ab662f86 --- /dev/null +++ b/examples/background/index.ts @@ -0,0 +1,59 @@ +/** + * @title Scene Background + * @category Scene + * @thumbnail https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ + */ +import * as dat from "dat.gui"; +import { + AssetType, + BackgroundMode, + Camera, + PrimitiveMesh, + SkyBoxMaterial, + TextureCube, + Texture2D, + WebGLEngine, + Scene, +} from "@galacean/engine"; + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +import { addGUI } from "./gui"; +import { material_list } from "./material-list"; + +async function setupDefaultScene(scene: Scene){ + const root = scene.createRootEntity(); + const cameraEntity = root.createChild(); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); +} + +async function start() { + const engine = await WebGLEngine.create({ + canvas: document.getElementById('canvas') as HTMLCanvasElement + }); + + engine.canvas.resizeByClientSize(); + engine.canvas._webCanvas.addEventListener("onresize", () => { + engine.canvas.resizeByClientSize(); + }); + + const scene = engine.sceneManager.activeScene; + setupDefaultScene(engine.sceneManager.activeScene); + + engine.run(); + + // @ts-ignore + const [cubeMap1, cubeMap2, texture] = await engine.resourceManager.load<[TextureCube, TextureCube, Texture2D]>(material_list); + + const { background } = scene; + const skyMaterial = (background.sky.material = new SkyBoxMaterial(engine)); // 添加天空盒材质 + skyMaterial.texture = cubeMap1; // 设置立方体纹理 + background.sky.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); // 设置天空盒网格 + background.texture = texture; + addGUI([cubeMap1, cubeMap2], background); +} + +start(); + diff --git a/examples/background/material-list.ts b/examples/background/material-list.ts new file mode 100644 index 000000000..953b9c8d8 --- /dev/null +++ b/examples/background/material-list.ts @@ -0,0 +1,30 @@ +import { AssetType } from "@galacean/engine"; + +export const material_list = [ + { + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*TiT2TbN5cG4AAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8GF6Q4LZefUAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*D5pdRqUHC3IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_FooTIp6pNIAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*CYGZR7ogZfoAAAAAAAAAAAAAARQnAQ", + ], + type: AssetType.TextureCube, + }, + { + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Bk5FQKGOir4AAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_cPhR7JMDjkAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*trqjQp1nOMQAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_RXwRqwMK3EAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*q4Q6TroyuXcAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*DP5QTbTSAYgAAAAAAAAAAAAAARQnAQ", + ], + type: AssetType.TextureCube, + }, + { + url: "https://gw.alipayobjects.com/mdn/rms_2e421e/afts/img/A*BcWiRYM7hroAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, +]; \ No newline at end of file diff --git a/examples/benchmark-animation.ts b/examples/benchmark-animation.ts new file mode 100644 index 000000000..eec42ad38 --- /dev/null +++ b/examples/benchmark-animation.ts @@ -0,0 +1,73 @@ +/** + * @title Animation + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*IUB2SY8spCAAAAAAAAAAAAAADiR2AQ/original + */ + +import { Stats } from "@galacean/engine-toolkit-stats"; +import { + Animator, + AssetType, + Camera, + GLTFResource, + PBRMaterial, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine object +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity and get scene + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 12); + cameraEntity.addComponent(Camera); + // cameraEntity.addComponent(OrbitControl); + cameraEntity.addComponent(Stats); + + // Load resources and add models. + engine.resourceManager + .load([ + { + url: "https://gw.alipayobjects.com/os/loanprod/bf055064-3eec-4d40-bce0-ddf11dfbb88a/5d78db60f211d21a43834e23/4f5e6bb277dd2fab8e2097d7a418c5bc.gltf", + type: AssetType.GLTF, + }, + { + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*OStMT63k5o8AAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + ]) + .then((resources: Object[]) => { + const glTF = resources[0]; + const baseTexture = resources[1]; + const model = glTF.defaultSceneRoot; + + glTF.materials.forEach((material: PBRMaterial) => { + material.baseTexture = baseTexture; + material.baseColor.set(1, 1, 1, 1); + }); + + for (let i = 0; i < 30; i++) { + for (let j = 0; j < 18; j++) { + const modelClone = model.clone(); + rootEntity.addChild(modelClone); + + const { transform } = modelClone; + transform.setRotation(0, -90, 0); + transform.setScale(0.5, 0.5, 0.5); + transform.setPosition(i * 1.0 - 15.0, j * 1.2, -j * 3.5); + + modelClone.getComponent(Animator).play("A"); + } + } + }); + + // Run engine + engine.run(); +}); diff --git a/examples/benchmark-particle.ts b/examples/benchmark-particle.ts new file mode 100644 index 000000000..c02c57674 --- /dev/null +++ b/examples/benchmark-particle.ts @@ -0,0 +1,457 @@ +/** + * @title Particle + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*umtuSa9Os2QAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BlendMode, + Burst, + Camera, + Color, + ConeShape, + Engine, + Entity, + CurveKey, + Logger, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleScaleMode, + ParticleSimulationSpace, + PointerButton, + Script, + SphereShape, + Texture2D, + Vector2, + Vector3, + WebGLEngine, + WebGLMode, +} from "@galacean/engine"; +import { Stats } from "@galacean/engine-toolkit"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas", + graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }, +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor = new Color(0.25, 0.25, 0.25, 1); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.position = new Vector3(0, 1, 3); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(Stats); + camera.fieldOfView = 60; + + engine.run(); + + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: " https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JlayRa2WltYAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*cFafRr6WaWUAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*TASTTpESkIIAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((textures) => { + const fireEntity = createFireParticle(engine, textures[0]); + createFireGlowParticle(fireEntity, textures[1]); + createFireSmokeParticle(fireEntity, textures[2]); + createFireEmbersParticle(fireEntity, textures[3]); + + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 5; j++) { + const cloneFire = fireEntity.clone(); + cloneFire.transform.setPosition(i * 1 - 1.5, j - 1, -3); + rootEntity.addChild(cloneFire); + } + } + }); +}); + +function createFireParticle(engine: Engine, texture: Texture2D): Entity { + const particleEntity = new Entity(engine, "Fire"); + particleEntity.transform.scale.set(1.268892, 1.268892, 1.268892); + particleEntity.transform.rotate(90, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 2; + + const generator = particleRenderer.generator; + const { + main, + emission, + textureSheetAnimation, + sizeOverLifetime, + colorOverLifetime, + } = generator; + + // Main module + main.startLifetime.constantMin = 0.2; + main.startLifetime.constantMax = 0.8; + main.startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constantMin = 0.4; + main.startSpeed.constantMax = 1.6; + main.startSpeed.mode = ParticleCurveMode.TwoConstants; + + main.startSize.constantMin = 0.6; + main.startSize.constantMax = 0.9; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.simulationSpace = ParticleSimulationSpace.World; + + // Emission module + emission.rateOverTime.constant = 35; + + const coneShape = new ConeShape(); + coneShape.angle = 0.96; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].color.set(255 / 255, 127 / 255, 4 / 255, 1.0); + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 123 / 255, 0 / 255, 1.0); + gradient.addColorKey(0.157, new Color(1, 1, 1, 1)); + gradient.addColorKey(0.573, new Color(255 / 255, 255 / 255, 137 / 255, 1)); + gradient.alphaKeys[1].time = 0.089; + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 0.529; + curve.addKey(0.074, 0.428 + 0.2); + curve.addKey(0.718, 0.957 + 0.03); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(6, 6); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.mode = ParticleCurveMode.TwoCurves; + frameOverTime.curveMin = new ParticleCurve(new CurveKey(0, 0.47), new CurveKey(1, 1)); + + return particleEntity; +} + +function createFireGlowParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireGlow"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 1; + + const generator = particleRenderer.generator; + const { main, emission, sizeOverLifetime, colorOverLifetime } = generator; + + // Main module + main.startLifetime.constantMin = 0.2; + main.startLifetime.constantMax = 0.6; + main.startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constantMin = 0.0; + main.startSpeed.constantMax = 1.4; + main.startSpeed.mode = ParticleCurveMode.TwoConstants; + + main.startSize.constant = 1.2; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 100 / 255, + 0 / 255, + 168 / 255 + ); + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 20; + + const coneShape = new ConeShape(); + coneShape.angle = 15; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 50 / 255, 0 / 255, 1.0); + + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + + gradient.addAlphaKey(0.057, 247 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 1.0; + curve.addKey(0.057, 0.37); + curve.addKey(0.728, 0.958); +} + +function createFireSmokeParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireSmoke"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 0; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + textureSheetAnimation, + } = generator; + + // Main module + main.startLifetime.constantMin = 1; + main.startLifetime.constantMax = 1.2; + main.startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 1.5; + + main.startSize.constant = 1.2; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 255 / 255, + 255 / 255, + 84 / 255 + ); + + main.gravityModifier.constant = -0.05; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 25; + + const coneShape = new ConeShape(); + coneShape.angle = 10; + coneShape.radius = 0.1; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].time = 0; + colorKeys[0].color.set(255 / 255, 98 / 255, 0 / 255, 1.0); + colorKeys[1].time = 0.679; + colorKeys[1].color.set(0, 0, 0, 1.0); + gradient.addColorKey(0.515, new Color(255 / 255, 98 / 255, 0 / 255, 1.0)); + + const alphaKeys = gradient.alphaKeys; + alphaKeys[0].alpha = 0; + alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.121, 1); + gradient.addAlphaKey(0.329, 200 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.28; + keys[1].value = 1.0; + curve.addKey(0.607, 0.909); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(8, 8); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.curveMax.keys[1].value = 0.382; +} + +function createFireEmbersParticle( + fireEntity: Entity, + texture: Texture2D +): void { + const particleEntity = fireEntity.createChild("FireEmbers"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 3; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + velocityOverLifetime, + rotationOverLifetime, + } = generator; + + // Main module + main.duration = 3; + + main.startLifetime.constantMin = 1; + main.startLifetime.constantMax = 1.5; + main.startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 0.4; + + main.startSize.constantMin = 0.05; + main.startSize.constantMax = 0.2; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.gravityModifier.constant = -0.15; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 65; + emission.addBurst(new Burst(0, new ParticleCompositeCurve(15))); + + const sphereShape = new SphereShape(); + sphereShape.radius = 0.01; + emission.shape = sphereShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = -0.1; + velocityOverLifetime.velocityX.constantMax = 0.1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = -0.1; + velocityOverLifetime.velocityY.constantMax = 0.1; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = -0.1; + velocityOverLifetime.velocityZ.constantMax = 0.1; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.TwoGradients; + + const gradientMax = colorOverLifetime.color.gradientMax; + const maxColorKeys = gradientMax.colorKeys; + maxColorKeys[0].time = 0.315; + maxColorKeys[1].time = 0.998; + maxColorKeys[1].color.set(255 / 255, 92 / 255, 0, 1.0); + gradientMax.addColorKey(0.71, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + + const gradientMin = colorOverLifetime.color.gradientMin; + gradientMin.addColorKey(0.0, new Color(1.0, 1.0, 1.0, 1.0)); + gradientMin.addColorKey(0.486, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + gradientMin.addColorKey(1.0, new Color(255 / 255, 94 / 255, 0 / 255, 1.0)); + + gradientMin.addAlphaKey(0.0, 1); + gradientMin.addAlphaKey(0.229, 1); + gradientMin.addAlphaKey(0.621, 0); + gradientMin.addAlphaKey(0.659, 1); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + const curve = sizeOverLifetime.size.curve; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + curve.keys[0].value = 1; + curve.keys[1].value = 0; + + // Rotation over lifetime module + rotationOverLifetime.enabled = true; + rotationOverLifetime.rotationZ.mode = ParticleCurveMode.TwoConstants; + rotationOverLifetime.rotationZ.constantMin = 90; + rotationOverLifetime.rotationZ.constantMax = 360; + + // Renderer + particleRenderer.pivot = new Vector3(0.2, 0.2, 0); +} + +class FireMoveScript extends Script { + radius: number = 0.8; + angle: number = 0; + + onUpdate(deltaTime: number): void { + if (this.engine.inputManager.isPointerHeldDown(PointerButton.Primary)) { + this.angle -= deltaTime * 6.0; + const x = Math.cos(this.angle) * this.radius; + const y = Math.sin(this.angle) * this.radius; + this.entity.transform.setPosition(x, 0, 0); + } + } +} diff --git a/examples/benchmark-video.ts b/examples/benchmark-video.ts new file mode 100644 index 000000000..091c9c8c7 --- /dev/null +++ b/examples/benchmark-video.ts @@ -0,0 +1,137 @@ +/** + * @title Video + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*3E8EQaK_xVEAAAAAAAAAAAAADiR2AQ/original + */ + +import { + Camera, + DependentMode, + Entity, + Script, + Sprite, + SpriteRenderer, + Texture2D, + TextureFormat, + TextureUsage, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { Stats } from "@galacean/engine-toolkit"; + +async function main() { + // Create engine object + const engine = await WebGLEngine.create({ + canvas: "canvas", + }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 20); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(Stats); + + // Add videos + for (let i = 0; i < 7; ++i) { + const posX = -12 + i * 4; + addVideo(rootEntity, posX, 4); + addVideo(rootEntity, posX, -4); + } + + // Run engine + engine.run(); +} + +function addVideo(parent: Entity, posX: number, posY: number): void { + const videoEntity = parent.createChild(""); + videoEntity.addComponent(VideoScript); + videoEntity.transform.setPosition(posX, posY, 0); +} +export class VideoScript extends Script { + static videos = { + "540p_0": { + width: 480, + height: 960, + url: "https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*dftzSq2szUsAAAAAAAAAAAAADtN3AQ", + }, + "540p_1": { + width: 480, + height: 960, + url: "https://gw.alipayobjects.com/v/huamei_p0cigc/afts/video/A*7gPzSo3RxlQAAAAAAAAAAAAADtN3AQ", + }, + "540p_2": { + width: 512, + height: 1024, + url: "https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*ZOgXRbmVlsIAAAAAAAAAAAAADoB5AQ", + }, + "540p_3": { + width: 512, + height: 1024, + url: "https://mdn.alipayobjects.com/huamei_p0cigc/afts/file/A*8xcvSJqCc3IAAAAAAAAAAAAADoB5AQ", + }, + }; + static videoIndex: number = 0; + + video: HTMLVideoElement; + texture: Texture2D; + noVideoFrameCallback: boolean = false; + + onAwake() { + const { width, height, url } = + VideoScript.videos[`540p_${VideoScript.videoIndex++}`]; + + if (VideoScript.videoIndex === 4) { + VideoScript.videoIndex = 0; + } + + const spriteRenderer = this.entity.addComponent(SpriteRenderer); + const { engine } = this; + const texture = new Texture2D( + engine, + width, + height, + TextureFormat.R8G8B8A8, + false, + TextureUsage.Dynamic + ); + spriteRenderer.sprite = new Sprite(engine, texture); + this.entity.transform.setScale(0.75, 0.75, 0.75); + + const videoElement = document.createElement("video"); + videoElement.src = url; + videoElement.crossOrigin = "anonymous"; + videoElement.loop = true; + videoElement.muted = true; + videoElement.play(); + videoElement.playsInline = true; + document.body.onclick = () => { + videoElement.play(); + }; + + const updateVideo = () => { + videoElement.readyState >= 2 && texture.setImageSource(videoElement); + videoElement.requestVideoFrameCallback(updateVideo); + }; + + if ("requestVideoFrameCallback" in videoElement) { + updateVideo(); + } else { + this.texture = texture; + this.video = videoElement; + this.noVideoFrameCallback = true; + } + } + + onUpdate() { + if (this.noVideoFrameCallback && this.video.readyState >= 2) { + this.texture.setImageSource(this.video); + } + } +} + +main(); diff --git a/examples/blend-mode.ts b/examples/blend-mode.ts new file mode 100644 index 000000000..7bc693d37 --- /dev/null +++ b/examples/blend-mode.ts @@ -0,0 +1,68 @@ +/** + * @title Blend Mode + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*onv4TaXhKnUAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + Camera, + GLTFResource, + PBRMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 3, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/d099b30b-59a3-42e4-99eb-b158afa8e65d.glb" + ) + .then((asset) => { + const { defaultSceneRoot, materials } = asset; + rootEntity.addChild(defaultSceneRoot); + + const state = { + alphaCutoff: 0, + isTransparent: false, + opacity: 0, + }; + + // Do not debug first material + const debugMaterials = materials.slice(1); + gui.add(state, "alphaCutoff", 0, 1, 0.01).onChange((v) => { + debugMaterials.forEach((material) => { + (material as PBRMaterial).alphaCutoff = v; + }); + }); + + gui.add(state, "isTransparent").onChange((v) => { + debugMaterials.forEach((material) => { + (material as PBRMaterial).isTransparent = v; + }); + }); + + gui.add(state, "opacity", 0, 1, 0.01).onChange((v) => { + debugMaterials.forEach((material) => { + (material as PBRMaterial).baseColor.a = v; + }); + }); + }); + + engine.run(); +}); diff --git a/examples/blinn-phong.ts b/examples/blinn-phong.ts new file mode 100644 index 000000000..76c155fd3 --- /dev/null +++ b/examples/blinn-phong.ts @@ -0,0 +1,136 @@ +/** + * @title Blinn Phong Material + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*JTsWRb6c7nsAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + BlinnPhongMaterial, + Camera, + DirectLight, + GLTFResource, + MeshRenderer, + RenderFace, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 30); + cameraEntity.addComponent(Camera); + const control = cameraEntity.addComponent(OrbitControl); + control.target.y = 5; + + // Create Direct Light + const light1 = rootEntity.createChild(); + const light2 = rootEntity.createChild(); + light1.transform.lookAt(new Vector3(-1, -1, -1)); + light2.transform.lookAt(new Vector3(1, 1, 1)); + light1.addComponent(DirectLight); + light2.addComponent(DirectLight); + + engine.run(); + + engine.resourceManager + .load([ + { + type: AssetType.Texture2D, + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*g_HIRqQdNUcAAAAAAAAAAAAAARQnAQ", + }, + { + type: AssetType.Texture2D, + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*H7nMRY2SuWcAAAAAAAAAAAAAARQnAQ", + }, + { + type: AssetType.GLTF, + url: "https://gw.alipayobjects.com/os/bmw-prod/72a8e335-01da-4234-9e81-5f8b56464044.gltf", + }, + ]) + .then((res) => { + const baseTexture = res[0] as Texture2D; + const normalTexture = res[1] as Texture2D; + const gltf = res[2] as GLTFResource; + + const { defaultSceneRoot } = gltf; + const rendererArray = new Array(); + const materials = new Array(); + + rootEntity.addChild(defaultSceneRoot); + defaultSceneRoot.getComponentsIncludeChildren( + MeshRenderer, + rendererArray + ); + + rendererArray.forEach((renderer) => { + const material = new BlinnPhongMaterial(engine); + material.baseTexture = baseTexture; + material.normalTexture = normalTexture; + material.shininess = 64; + material.renderFace = RenderFace.Double; + renderer.setMaterial(material); + materials.push(material); + }); + + addGUI(materials); + }); + + function addGUI(materials: BlinnPhongMaterial[]): void { + const state = { + baseColor: [255, 255, 255], + specularColor: [255, 255, 255], + shininess: 64, + normalIntensity: 1, + isTransparent: false, + opacity: 1, + }; + + gui.addColor(state, "baseColor").onChange((v) => { + materials.forEach((material) => { + material.baseColor.set( + v[0] / 255, + v[1] / 255, + v[2] / 255, + state.opacity + ); + }); + }); + + gui.addColor(state, "specularColor").onChange((v) => { + materials.forEach((material) => { + material.specularColor.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + }); + gui.add(state, "shininess", 0, 100).onChange((v) => { + materials.forEach((material) => { + material.shininess = v; + }); + }); + gui.add(state, "normalIntensity", 0, 1, 0.1).onChange((v) => { + materials.forEach((material) => { + material.normalIntensity = v; + }); + }); + gui.add(state, "isTransparent").onChange((v) => { + materials.forEach((material) => { + material.isTransparent = v; + }); + }); + gui.add(state, "opacity", 0, 1, 0.1).onChange((v) => { + materials.forEach((material) => { + material.baseColor.a = v; + }); + }); + } +}); diff --git a/examples/bounding-box.ts b/examples/bounding-box.ts new file mode 100644 index 000000000..5ecdf8ee9 --- /dev/null +++ b/examples/bounding-box.ts @@ -0,0 +1,99 @@ +/** + * @title Bounding Box + * @category Advance + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*QxgsR4ScBPwAAAAAAAAAAAAADiR2AQ/original + */ +import { + BoundingBox, + Camera, + DirectLight, + Entity, + GLTFResource, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + Script, + SkinnedMeshRenderer, + Vector3, + WebGLEngine, +} from '@galacean/engine'; + +WebGLEngine.create({ canvas: 'canvas' }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = rootEntity.createChild('camera'); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + engine.sceneManager.activeScene.ambientLight.diffuseSolidColor.set( + 1, + 1, + 1, + 1 + ); + const lightEntity = rootEntity.createChild('DirectLight'); + lightEntity.addComponent(DirectLight); + lightEntity.transform.setPosition(3, 3, 3); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + class MoveScript extends Script { + private _rotation = 0; + private _boxEntity: Entity; + private _boundingBox: BoundingBox = new BoundingBox(); + private _centerVec: Vector3 = new Vector3(); + private _extentVec: Vector3 = new Vector3(); + private _tempVec: Vector3 = new Vector3(); + + onStart() { + const boxEntity = (this._boxEntity = rootEntity.createChild('box')); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new PBRMaterial(engine); + material.baseColor.set(0xaa / 0xff, 0x3e / 0xff, 0x53 / 0xff, 0.6); + material.isTransparent = true; + boxRenderer.setMaterial(material); + } + + onUpdate(deltaTime: number): void { + const rotation = (++this._rotation / 180) * Math.PI; + const { transform } = this.entity; + transform.rotate(this._tempVec.set(1, 1, 1)); + transform.setPosition(Math.sin(rotation), Math.cos(rotation), 0); + } + + onLateUpdate(deltaTime: number): void { + const renderers: SkinnedMeshRenderer[] = + this.entity.getComponentsIncludeChildren(SkinnedMeshRenderer, []); + const length = renderers.length; + if (length > 0) { + const { + _extentVec: extentVec, + _centerVec: centerVec, + _boundingBox: boundingBox, + } = this; + boundingBox.copyFrom(renderers[0].bounds); + for (let i = 1; i < length; i++) { + BoundingBox.merge(boundingBox, renderers[i].bounds, boundingBox); + } + const { transform } = this._boxEntity; + boundingBox.getExtent(extentVec).scale(2); + boundingBox.getCenter(centerVec); + transform.worldPosition = centerVec; + transform.scale = extentVec; + } + } + } + + engine.resourceManager + .load( + 'https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb' + ) + .then((glTF) => { + const glTFEntity = glTF.defaultSceneRoot; + glTFEntity.addComponent(MoveScript); + rootEntity.addChild(glTFEntity); + }); + + engine.run(); +}); diff --git a/examples/box-selection.ts b/examples/box-selection.ts new file mode 100644 index 000000000..81ea17488 --- /dev/null +++ b/examples/box-selection.ts @@ -0,0 +1,50 @@ +/** + * @title Box Selection Controls + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*MnsLTotWN2IAAAAAAAAAAAAADiR2AQ/original + */ + import { + Camera, + MeshRenderer, + PrimitiveMesh, + UnlitMaterial, + WebGLEngine +} from "@galacean/engine"; +import { OutlineManager } from "@galacean/engine-toolkit-outline"; +import { BoxSelectionComponent, BoxSelectionControls } from "@galacean/engine-toolkit-controls"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.run(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.addComponent(Camera); + // add BoxSelectionControls to camera entity + cameraEntity.addComponent(BoxSelectionControls); + cameraEntity.transform.setPosition(0, 0, 15); + + const outlineManager = cameraEntity.addComponent(OutlineManager); + outlineManager.size = 2; + + const mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new UnlitMaterial(engine); + for (let i = 0; i < 25; i++) { + const entity = rootEntity.createChild('e' + i); + // Entity with BoxSelectionComponent can be selected by controls. + const select = entity.addComponent(BoxSelectionComponent); + entity.transform.setPosition(-4 + (i % 5) * 2, -4 + Math.floor(i / 5) * 2, 0); + const renderer = entity.addComponent(MeshRenderer); + renderer.setMaterial(material); + renderer.mesh = mesh; + + select.onSelect = () => { + outlineManager.addEntity(entity); + } + select.onUnselect = () => { + outlineManager.removeEntity(entity); + } + } +}); diff --git a/examples/buffer-mesh-independent.ts b/examples/buffer-mesh-independent.ts new file mode 100644 index 000000000..fc67c764e --- /dev/null +++ b/examples/buffer-mesh-independent.ts @@ -0,0 +1,187 @@ +/** + * @title Buffer Mesh Independent + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*YnS0RIRZQv0AAAAAAAAAAAAADiR2AQ/original + */ +import { + BlinnPhongMaterial, + Buffer, + BufferBindFlag, + BufferMesh, + BufferUsage, + Camera, + Engine, + IndexFormat, + Mesh, + MeshRenderer, + PointLight, + Vector3, + VertexElement, + VertexElementFormat, + WebGLEngine, + Script, +} from "@galacean/engine"; + +/** + * Script for updating color buffer. + */ +class RandomColorScript extends Script { + /** Color data. */ + colorData: Float32Array; + /** Color buffer. */ + colorBuffer: Buffer; + + private _loopCount = 0; + + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + if (this._loopCount === 30) { + const { colorData } = this; + for (let i = 0; i < 6; i++) { + const r = Math.random(); + const g = Math.random(); + const b = Math.random(); + const faceOffset = i * 12; + for (let i = 0; i < 4; i++) { + const vertexOffset = i * 3; + colorData[faceOffset + vertexOffset] = r; + colorData[faceOffset + vertexOffset + 1] = g; + colorData[faceOffset + vertexOffset + 2] = b; + } + } + this.colorBuffer.setData(colorData); + this._loopCount = 0; + } + this._loopCount++; + } +} + +main(); + +async function main() { + // Create engine and get root entity. + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create light. + const lightEntity = rootEntity.createChild("pointLight"); + const pointLight = lightEntity.addComponent(PointLight); + pointLight.distance = 10; + lightEntity.transform.setPosition(2, 5, 5); + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 6, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(Camera); + + // Create custom cube. + // Use createCustomMesh() to create custom cube mesh. + const cubeEntity = rootEntity.createChild("Cube"); + const cubeRenderer = cubeEntity.addComponent(MeshRenderer); + const randomColorScript = cubeEntity.addComponent(RandomColorScript); + const cubeGeometry = createCustomMesh(engine, 1.0, randomColorScript); + const material = new BlinnPhongMaterial(engine); + cubeEntity.transform.rotate(0, 60, 0); + cubeRenderer.mesh = cubeGeometry; + cubeRenderer.setMaterial(material); + + // Run engine. + engine.run(); +} + +/** + * Create cube geometry with custom BufferGeometry. + * @param engine - Engine + * @param size - Cube size + * @returns Cube mesh + */ +function createCustomMesh( + engine: Engine, + size: number, + randomColorScript: RandomColorScript +): Mesh { + const cubeMesh = new BufferMesh(engine, "CustomCubeMesh"); + + // Create vertices position and normal data. + // prettier-ignore + const positionNormals = new Float32Array([ + // Up + -size, size, -size, 0, 1, 0, size, size, -size, 0, 1, 0, size, size, size, 0, 1, 0, -size, size, size, 0, 1, 0, + // Down + -size, -size, -size, 0, -1, 0, size, -size, -size, 0, -1, 0, size, -size, size, 0, -1, 0, -size, -size, size, 0, -1, 0, + // Left + -size, size, -size, -1, 0, 0, -size, size, size, -1, 0, 0, -size, -size, size, -1, 0, 0, -size, -size, -size, -1, 0, 0, + // Right + size, size, -size, 1, 0, 0, size, size, size, 1, 0, 0, size, -size, size, 1, 0, 0, size, -size, -size, 1, 0, 0, + // Front + -size, size, size, 0, 0, 1, size, size, size, 0, 0, 1, size, -size, size, 0, 0, 1, -size, -size, size, 0, 0, 1, + // Back + -size, size, -size, 0, 0, -1, size, size, -size, 0, 0, -1, size, -size, -size, 0, 0, -1, -size, -size, -size, 0, 0, -1]); + + // Create vertices color and init by white. + const colorData = new Float32Array(3 * 24); + colorData.fill(1.0); + + // Create indices data. + // prettier-ignore + const indices = new Uint16Array([ + // Up + 0, 2, 1, 2, 0, 3, + // Down + 4, 6, 7, 6, 4, 5, + // Left + 8, 10, 9, 10, 8, 11, + // Right + 12, 14, 15, 14, 12, 13, + // Front + 16, 18, 17, 18, 16, 19, + // Back + 20, 22, 23, 22, 20, 21]); + + // Create gpu vertex buffer and index buffer. + const posNorBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + positionNormals, + BufferUsage.Static + ); + const independentColorBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + colorData, + BufferUsage.Dynamic + ); + const indexBuffer = new Buffer( + engine, + BufferBindFlag.IndexBuffer, + indices, + BufferUsage.Static + ); + + // Bind buffer. + cubeMesh.setVertexBufferBinding(posNorBuffer, 24, 0); + cubeMesh.setVertexBufferBinding(independentColorBuffer, 12, 1); + cubeMesh.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16); + + // Set vertexElements. + cubeMesh.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0), + new VertexElement("COLOR_0", 0, VertexElementFormat.Vector3, 1), + ]); + + // Add one sub geometry. + cubeMesh.addSubMesh(0, indices.length); + + // Set `vertexColors` and `colorBuffer` to `randomColorScript`. + randomColorScript.colorData = colorData; + randomColorScript.colorBuffer = independentColorBuffer; + + return cubeMesh; +} diff --git a/examples/buffer-mesh-instance.ts b/examples/buffer-mesh-instance.ts new file mode 100644 index 000000000..a25c21f47 --- /dev/null +++ b/examples/buffer-mesh-instance.ts @@ -0,0 +1,191 @@ +/** + * @title Buffer Mesh Instance + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*jjZMTrp-vU8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Buffer, + BufferBindFlag, + BufferMesh, + BufferUsage, + Camera, + Engine, + IndexFormat, + Material, + Mesh, + MeshRenderer, + Shader, + Vector3, + VertexElement, + VertexElementFormat, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Get scene and root entity + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Init instance shader + const shader = initCustomShader(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + cameraEntity.transform.setPosition(0, 10, 160); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + camera.farClipPlane = 300; + + // Create Instance Cube + const cubeEntity = rootEntity.createChild("Cube"); + const cubeRenderer = cubeEntity.addComponent(MeshRenderer); + const material = new Material(engine, shader); + cubeEntity.transform.rotate(0, 60, 0); + cubeRenderer.mesh = createCustomMesh(engine, 1.0); // Use `createCustomMesh()` to create custom instance cube mesh. + cubeRenderer.setMaterial(material); + + // Run engine. + engine.run(); +}); + +/** + * Create cube geometry with custom BufferGeometry. + * @param engine - Engine + * @param size - Cube size + * @returns Cube mesh + */ +function createCustomMesh(engine: Engine, size: number): Mesh { + const geometry = new BufferMesh(engine, "CustomCubeGeometry"); + + // Create vertices data. + // prettier-ignore + const vertices: Float32Array = new Float32Array([ + // Up + -size, size, -size, 0, 1, 0, size, size, -size, 0, 1, 0, size, size, size, 0, 1, 0, -size, size, size, 0, 1, 0, + // Down + -size, -size, -size, 0, -1, 0, size, -size, -size, 0, -1, 0, size, -size, size, 0, -1, 0, -size, -size, size, 0, -1, 0, + // Left + -size, size, -size, -1, 0, 0, -size, size, size, -1, 0, 0, -size, -size, size, -1, 0, 0, -size, -size, -size, -1, 0, 0, + // Right + size, size, -size, 1, 0, 0, size, size, size, 1, 0, 0, size, -size, size, 1, 0, 0, size, -size, -size, 1, 0, 0, + // Front + -size, size, size, 0, 0, 1, size, size, size, 0, 0, 1, size, -size, size, 0, 0, 1, -size, -size, size, 0, 0, 1, + // Back + -size, size, -size, 0, 0, -1, size, size, -size, 0, 0, -1, size, -size, -size, 0, 0, -1, -size, -size, -size, 0, 0, -1]); + + // Create instance data. + const instanceCount = 4000; + const instanceStride = 6; + const instanceData: Float32Array = new Float32Array( + instanceCount * instanceStride + ); + for (let i = 0; i < instanceCount; i++) { + const offset = i * instanceStride; + // instance offset + instanceData[offset] = (Math.random() - 0.5) * 60; + instanceData[offset + 1] = (Math.random() - 0.5) * 60; + instanceData[offset + 2] = (Math.random() - 0.5) * 60; + // instance color + instanceData[offset + 3] = Math.random(); + instanceData[offset + 4] = Math.random(); + instanceData[offset + 5] = Math.random(); + } + + // Create indices data. + // prettier-ignore + const indices: Uint16Array = new Uint16Array([ + // Up + 0, 2, 1, 2, 0, 3, + // Down + 4, 6, 7, 6, 4, 5, + // Left + 8, 10, 9, 10, 8, 11, + // Right + 12, 14, 15, 14, 12, 13, + // Front + 16, 18, 17, 18, 16, 19, + // Back + 20, 22, 23, 22, 20, 21]); + + // Create gpu vertex buffer and index buffer. + const vertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + vertices, + BufferUsage.Static + ); + const instanceVertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + instanceData, + BufferUsage.Static + ); + const indexBuffer = new Buffer( + engine, + BufferBindFlag.IndexBuffer, + indices, + BufferUsage.Static + ); + + // Bind buffer + geometry.setVertexBufferBinding(vertexBuffer, 24, 0); + geometry.setVertexBufferBinding(instanceVertexBuffer, 24, 1); + geometry.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16); + + // Add vertexElements + geometry.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0, 0), // Bind to VertexBuffer 0 + new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0, 0), // Bind to VertexBuffer 0 + new VertexElement("INSTANCE_OFFSET", 0, VertexElementFormat.Vector3, 1, 1), // Bind instance offset to VertexBuffer 1, and enable instance by set instanceStepRate with 1 + new VertexElement("INSTANCE_COLOR", 12, VertexElementFormat.Vector3, 1, 1), // Bind instance color to VertexBuffer 1, and enable instance by set instanceStepRate with 1 + ]); + + // Add one sub geometry. + geometry.addSubMesh(0, indices.length); + + geometry.instanceCount = instanceCount; + + return geometry; +} + +/** + * Create custom instance shader. + */ +function initCustomShader(): Shader { + const shader = Shader.create( + "CustomShader", + `uniform mat4 renderer_MVPMat; + attribute vec4 POSITION; + attribute vec3 INSTANCE_OFFSET; + attribute vec3 INSTANCE_COLOR; + + uniform mat4 renderer_MVMat; + + varying vec3 v_position; + varying vec3 v_color; + + void main() { + vec4 position = POSITION; + position.xyz += INSTANCE_OFFSET; + gl_Position = renderer_MVPMat * position; + + v_color = INSTANCE_COLOR; + }`, + + ` + varying vec3 v_color; + uniform vec4 u_color; + + void main() { + vec4 color = vec4(v_color,1.0); + gl_FragColor = color; + } + ` + ); + return shader; +} diff --git a/examples/buffer-mesh-interleaved.ts b/examples/buffer-mesh-interleaved.ts new file mode 100644 index 000000000..f901cacbd --- /dev/null +++ b/examples/buffer-mesh-interleaved.ts @@ -0,0 +1,123 @@ +/** + * @title Buffer Mesh Interleaved + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*vikrSYHt6mAAAAAAAAAAAAAADiR2AQ/original + */ +import { + BlinnPhongMaterial, + Buffer, + BufferBindFlag, + BufferMesh, + BufferUsage, + Camera, + Engine, + IndexFormat, + Mesh, + MeshRenderer, + PointLight, + Vector3, + VertexElement, + VertexElementFormat, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine and get root entity +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create light. + const lightEntity = rootEntity.createChild("pointLight"); + const pointLight = lightEntity.addComponent(PointLight); + pointLight.distance = 10; + lightEntity.transform.setPosition(2, 5, 5); + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 6, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(Camera); + + // Create custom cube. + // Use createCustomMesh() to create custom cube mesh. + const cubeEntity = rootEntity.createChild("Cube"); + const cubeRenderer = cubeEntity.addComponent(MeshRenderer); + const cubeGeometry = createCustomMesh(engine, 1.0); + const material = new BlinnPhongMaterial(engine); + cubeEntity.transform.rotate(0, 60, 0); + cubeRenderer.mesh = cubeGeometry; + cubeRenderer.setMaterial(material); + + // Run engine. + engine.run(); + + /** + * Create cube geometry with custom BufferGeometry. + * @param engine - Engine + * @param size - Cube size + * @returns Cube mesh + */ + function createCustomMesh(engine: Engine, size: number): Mesh { + const geometry = new BufferMesh(engine, "CustomCubeGeometry"); + + // prettier-ignore + // Create vertices data. + const vertices: Float32Array = new Float32Array([ + // Up + -size, size, -size, 0, 1, 0, size, size, -size, 0, 1, 0, size, size, size, 0, 1, 0, -size, size, size, 0, 1, 0, + // Down + -size, -size, -size, 0, -1, 0, size, -size, -size, 0, -1, 0, size, -size, size, 0, -1, 0, -size, -size, size, 0, -1, 0, + // Left + -size, size, -size, -1, 0, 0, -size, size, size, -1, 0, 0, -size, -size, size, -1, 0, 0, -size, -size, -size, -1, 0, 0, + // Right + size, size, -size, 1, 0, 0, size, size, size, 1, 0, 0, size, -size, size, 1, 0, 0, size, -size, -size, 1, 0, 0, + // Front + -size, size, size, 0, 0, 1, size, size, size, 0, 0, 1, size, -size, size, 0, 0, 1, -size, -size, size, 0, 0, 1, + // Back + -size, size, -size, 0, 0, -1, size, size, -size, 0, 0, -1, size, -size, -size, 0, 0, -1, -size, -size, -size, 0, 0, -1]); + + // prettier-ignore + // Create indices data. + const indices: Uint16Array = new Uint16Array([ + // Up + 0, 2, 1, 2, 0, 3, + // Down + 4, 6, 7, 6, 4, 5, + // Left + 8, 10, 9, 10, 8, 11, + // Right + 12, 14, 15, 14, 12, 13, + // Front + 16, 18, 17, 18, 16, 19, + // Back + 20, 22, 23, 22, 20, 21]); + + // Create gpu vertex buffer and index buffer. + const vertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + vertices, + BufferUsage.Static + ); + const indexBuffer = new Buffer( + engine, + BufferBindFlag.IndexBuffer, + indices, + BufferUsage.Static + ); + + // Bind buffer + geometry.setVertexBufferBinding(vertexBuffer, 24); + geometry.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16); + + // Add vertexElement + geometry.setVertexElements([ + new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0), + new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0), + ]); + + // Add one sub geometry. + geometry.addSubMesh(0, indices.length); + return geometry; + } +}); diff --git a/examples/buffer-mesh-particle-shader-effect.ts b/examples/buffer-mesh-particle-shader-effect.ts new file mode 100644 index 000000000..900ab5287 --- /dev/null +++ b/examples/buffer-mesh-particle-shader-effect.ts @@ -0,0 +1,280 @@ +/** + * @title Buffer Mesh Particle Shader Effect + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*9bIIRZ7rnNgAAAAAAAAAAAAADiR2AQ/original + */ +import { + Buffer, + BufferBindFlag, + BufferMesh, + Camera, + MeshRenderer, + RenderFace, + Texture2D, + VertexBufferBinding, + VertexElement, + VertexElementFormat, + Engine, + Shader, + BaseMaterial, + Script, + WebGLEngine, +} from '@galacean/engine'; +import { OrbitControl } from '@galacean/engine-toolkit-controls'; +import { ShaderLab } from '@galacean/engine-shader-lab'; + +const shaderLab = new ShaderLab(); + +const shaderSource = `Shader "ParticleEffect" { + SubShader "Default" { + Pass "0" { + VertexShader = vert; + FragmentShader = frag; + + #define PI 3.14159265359 + mat4 renderer_MVPMat; + + struct a2v { + vec3 POSITION; + vec3 INDEX; + vec2 UV; + } + + struct v2f { + vec2 v_uv; + } + + v2f vert(a2v v) { + v2f o; + + o.v_uv = v.UV; + vec4 position = vec4(v.POSITION , 1.0); + float distance = length(v.INDEX.xy); + float maxDistance = 40.0 * 1.414; + float wait = distance / maxDistance * 0.5; + + float p = clamp(progress-wait, 0.0, 2.0); + position.z += sin(p * PI * 6.0) * 3.0 * (maxDistance - distance * 0.5) / maxDistance * (2.0 - progress) * 0.5; + + gl_Position = renderer_MVPMat * position; + return o; + } + + float progress; + sampler2D texture1; + sampler2D texture2; + + // This function could be changed. Some great effects could be referred to https://gl-transitions.com/gallery + vec4 transition(vec2 p, float progress) { + vec2 dir = p - vec2(0.5); + float dist = length(dir); + + if (dist > progress) { + return mix(texture2D(texture1, v_uv), texture2D(texture2, v_uv), progress); + } else { + vec2 offset = dir * sin(dist * 30.0 - progress * 30.0); + return mix(texture2D(texture1, v_uv + offset), texture2D(texture2, v_uv), progress); + } + } + + void frag(v2f i) { + gl_FragColor = transition(i.v_uv, clamp(progress, 0.0, 1.0)); + } + } + } +} +`; + +class ParticleMeshMaterial extends BaseMaterial { + constructor(engine: Engine, shader: Shader) { + super(engine, shader); + } + + clone(): ParticleMeshMaterial { + const dest = new ParticleMeshMaterial(this._engine, this.shader); + this.cloneTo(dest); + return dest; + } + + get texture1(): Texture2D { + return this.shaderData.getTexture('texture1'); + } + + set texture1(value: Texture2D) { + this.shaderData.setTexture('texture1', value); + } + + get texture2(): Texture2D { + return this.shaderData.getTexture('texture2'); + } + + set texture2(value: Texture2D) { + this.shaderData.setTexture('texture2', value); + } + + get progress(): number { + return this.shaderData.getFloat('progress'); + } + + set progress(value: number) { + this.shaderData.setFloat('progress', value); + } +} + +class AnimationComponent extends Script { + time = 0; + mtl: ParticleMeshMaterial | undefined; + onAwake() { + this.mtl = this.entity + .getComponent(MeshRenderer)! + .getMaterial() as ParticleMeshMaterial; + } + onUpdate(time: number) { + this.time += time; + if (this.mtl) { + this.mtl.progress = (this.time / 5) % 2; + } + } +} + +// segmentX and segmentY handle how many particles we create +function createPlaneParticleMesh( + engine: WebGLEngine, + width: number, + height: number, + segmentX: number, + segmentY: number, + isIn: boolean +) { + const triangleCount = segmentX * segmentY * 2; // we create segmentX * segmentY rectangles, each rectangle has 2 triangles + const vertexCount = triangleCount * 3; + + const halfWidth = width * 0.5; + const halfHeight = height * 0.5; + const segmentWidth = width / segmentX; + const segmentHeight = height / segmentY; + + let positionBuffer = new Float32Array(vertexCount * 3); + let uvBuffer = new Float32Array(vertexCount * 2); + let indexBuffer = new Float32Array(vertexCount * 3); + + let i = 0; + for (let y = 0; y < segmentY; y++) { + for (let x = 0; x < segmentX; x++) { + // create vertex attribute buffer according to each square seperated by segemntX and segmentY + let index = i * 3 * 3; + positionBuffer[index] = -halfWidth + x * segmentWidth; + positionBuffer[index + 1] = -halfHeight + y * segmentHeight; + positionBuffer[index + 2] = 0; + positionBuffer[index + 3] = -halfWidth + (x + 1) * segmentWidth; + positionBuffer[index + 4] = -halfHeight + y * segmentHeight; + positionBuffer[index + 5] = 0; + positionBuffer[index + 6] = -halfWidth + x * segmentWidth; + positionBuffer[index + 7] = -halfHeight + (y + 1) * segmentHeight; + positionBuffer[index + 8] = 0; + positionBuffer[index + 9] = -halfWidth + (x + 1) * segmentWidth; + positionBuffer[index + 10] = -halfHeight + y * segmentHeight; + positionBuffer[index + 11] = 0; + positionBuffer[index + 12] = -halfWidth + (x + 1) * segmentWidth; + positionBuffer[index + 13] = -halfHeight + (y + 1) * segmentHeight; + positionBuffer[index + 14] = 0; + positionBuffer[index + 15] = -halfWidth + x * segmentWidth; + positionBuffer[index + 16] = -halfHeight + (y + 1) * segmentHeight; + positionBuffer[index + 17] = 0; + + indexBuffer[index] = x * 2 - segmentX; + indexBuffer[index + 1] = y * 2 - segmentY; + indexBuffer[index + 2] = i; + indexBuffer[index + 3] = x * 2 - segmentX; + indexBuffer[index + 4] = y * 2 - segmentY; + indexBuffer[index + 5] = i; + indexBuffer[index + 6] = x * 2 - segmentX; + indexBuffer[index + 7] = y * 2 - segmentY; + indexBuffer[index + 8] = i; + indexBuffer[index + 9] = x * 2 + 1 - segmentX; + indexBuffer[index + 10] = y * 2 + 1 - segmentY; + indexBuffer[index + 11] = i + 1; + indexBuffer[index + 12] = x * 2 + 1 - segmentX; + indexBuffer[index + 13] = y * 2 + 1 - segmentY; + indexBuffer[index + 14] = i + 1; + indexBuffer[index + 15] = x * 2 + 1 - segmentX; + indexBuffer[index + 16] = y * 2 + 1 - segmentY; + indexBuffer[index + 17] = i + 1; + + index = i * 2 * 3; + uvBuffer[index] = x / segmentX; + uvBuffer[index + 1] = 1 - y / segmentY; + uvBuffer[index + 2] = (x + 1) / segmentX; + uvBuffer[index + 3] = 1 - y / segmentY; + uvBuffer[index + 4] = x / segmentX; + uvBuffer[index + 5] = 1 - (y + 1) / segmentY; + uvBuffer[index + 6] = (x + 1) / segmentX; + uvBuffer[index + 7] = 1 - y / segmentY; + uvBuffer[index + 8] = (x + 1) / segmentX; + uvBuffer[index + 9] = 1 - (y + 1) / segmentY; + uvBuffer[index + 10] = x / segmentX; + uvBuffer[index + 11] = 1 - (y + 1) / segmentY; + + i += 2; + } + } + + const mesh = new BufferMesh(engine); + mesh.setVertexBufferBindings([ + new VertexBufferBinding( + new Buffer(engine, BufferBindFlag.VertexBuffer, positionBuffer), + 3 * Float32Array.BYTES_PER_ELEMENT + ), + new VertexBufferBinding( + new Buffer(engine, BufferBindFlag.VertexBuffer, uvBuffer), + 2 * Float32Array.BYTES_PER_ELEMENT + ), + new VertexBufferBinding( + new Buffer(engine, BufferBindFlag.VertexBuffer, indexBuffer), + 3 * Float32Array.BYTES_PER_ELEMENT + ), + ]); + + mesh.setVertexElements([ + new VertexElement('POSITION', 0, VertexElementFormat.Vector3, 0), + new VertexElement('UV', 0, VertexElementFormat.Vector2, 1), + new VertexElement('INDEX', 0, VertexElementFormat.Vector3, 2), + ]); + + mesh.addSubMesh(0, vertexCount); + return mesh; +} + +WebGLEngine.create({ canvas: 'canvas', shaderLab }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const particleMeshShader = Shader.create(shaderSource); + + const cameraEntity = rootEntity.createChild('camera'); + cameraEntity.addComponent(Camera); + cameraEntity.transform.position.set(0, 0, 50); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load([ + 'https://gw.alipayobjects.com/zos/OasisHub/440001901/3736/spring.jpeg', + 'https://gw.alipayobjects.com/zos/OasisHub/440001901/9546/winter.jpeg', + ]) + .then((assets) => { + const entity = rootEntity.createChild('plane'); + const renderer = entity.addComponent(MeshRenderer); + const mesh = createPlaneParticleMesh(engine, 20, 20, 80, 80, true); + const mtl = new ParticleMeshMaterial(engine, particleMeshShader); + renderer.setMaterial(mtl); + renderer.mesh = mesh; + mtl.texture1 = assets[0] as Texture2D; + mtl.texture2 = assets[1] as Texture2D; + mtl.renderFace = RenderFace.Double; + + entity.addComponent(AnimationComponent); + }); + + engine.run(); +}); diff --git a/examples/camera-depth-texture.ts b/examples/camera-depth-texture.ts new file mode 100644 index 000000000..76bc44018 --- /dev/null +++ b/examples/camera-depth-texture.ts @@ -0,0 +1,139 @@ +/** + * @title Camera Depth Texture + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Zg1mRbLWEVMAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BaseMaterial, + Camera, + Color, + DepthTextureMode, + DirectLight, + Engine, + Entity, + FogMode, + GLTFResource, + MeshRenderer, + PrimitiveMesh, + RenderFace, + Shader, + ShadowType, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { FreeControl } from "@galacean/engine-toolkit-controls"; + +async function main() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + + // Set background color to corn flower blue + const cornFlowerBlue = new Color(130 / 255, 163 / 255, 255 / 255); + scene.background.solidColor = cornFlowerBlue; + + // Set fog + scene.fogMode = FogMode.ExponentialSquared; + scene.fogDensity = 0.015; + scene.fogEnd = 200; + scene.fogColor = cornFlowerBlue; + + const rootEntity = scene.createRootEntity(); + + // Create camera entity and components + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(-6, 2, -22); + cameraEntity.transform.rotate(new Vector3(0, -110, 0)); + cameraEntity.addComponent(FreeControl).floorMock = false; + + const camera = cameraEntity.addComponent(Camera); + camera.depthTextureMode = DepthTextureMode.PrePass; + + // Create light entity and component + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0, 0.7, 0.5); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // Enable light cast shadow + const directLight = lightEntity.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + // Add ambient light + const ambientLight = await engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/09904c03-0d23-4834-aa73-64e11e2287b0.bin", + type: AssetType.Env, + }); + scene.ambientLight = ambientLight; + + // Add model + const glTFResource = await engine.resourceManager.load( + "https://gw.alipayobjects.com/os/OasisHub/19748279-7b9b-4c17-abdf-2c84f93c54c8/oasis-file/1670226408346/low_poly_scene_forest_waterfall.gltf" + ); + rootEntity.addChild(glTFResource.defaultSceneRoot); + + showDepthPlane(engine, cameraEntity); + + engine.run(); +} + +function showDepthPlane(engine: Engine, camera: Entity): void { + const entity = camera.createChild("Plane"); + entity.transform.setPosition(0, 0, -1); + entity.transform.rotate(new Vector3(90, 0, 0)); + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine,0.5,0.5); + + // Create material + const material = new BaseMaterial(engine, Shader.find("RenderDepthTexture")); + renderer.setMaterial(material); +} + +const renderDepthVS = ` + #include + #include + #include + #include + #include + + void main() { + #include + #include + #include + #include + #include + + #include + }`; + +const renderDepthFS = ` + #include + #include + #include + + uniform sampler2D camera_DepthTexture; + + void main() { + float nonLinearDepth = texture2D(camera_DepthTexture, v_uv).r; + float depth = remapDepthBufferLinear01(nonLinearDepth); + + vec4 baseColor = vec4(depth, depth, depth, 1.0); + gl_FragColor = baseColor; + + #ifndef MATERIAL_IS_TRANSPARENT + gl_FragColor.a = 1.0; + #endif + + #include + + #ifndef ENGINE_IS_COLORSPACE_GAMMA + gl_FragColor = linearToGamma(gl_FragColor); + #endif + }`; + +Shader.create("RenderDepthTexture", renderDepthVS, renderDepthFS); + +main(); diff --git a/examples/cascaded-shadow.ts b/examples/cascaded-shadow.ts new file mode 100644 index 000000000..b382de704 --- /dev/null +++ b/examples/cascaded-shadow.ts @@ -0,0 +1,284 @@ +/** + * @title Cascaded Stable Shadow + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Kw6RSoysvhoAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AmbientLight, + AssetType, + BaseMaterial, + Camera, + Color, + DirectLight, + Engine, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + RenderFace, + Script, + Shader, + ShadowCascadesMode, + ShadowResolution, + ShadowType, + Vector3, + WebGLEngine, + WebGLMode, +} from "@galacean/engine"; + +async function main() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + createShadowMapVisualShader(); + + const scene = engine.sceneManager.activeScene; + scene.shadowResolution = ShadowResolution.High; + scene.shadowDistance = 1000; + scene.shadowCascades = ShadowCascadesMode.FourCascades; + + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0, 10, 50); + cameraEntity.transform.lookAt(new Vector3()); + cameraEntity.addComponent(OrbitControl); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 1000; + + // Create light + const light = rootEntity.createChild("light"); + light.transform.setPosition(10, 10, 0); + light.transform.lookAt(new Vector3()); + const rotation = light.addComponent(Rotation); + const directLight = light.addComponent(DirectLight); + directLight.shadowStrength = 1.0; + directLight.shadowType = ShadowType.SoftLow; + + // Create plane + const planeEntity = rootEntity.createChild("PlaneEntity"); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeRenderer.mesh = PrimitiveMesh.createPlane(engine, 10, 400); + + const planeMaterial = new PBRMaterial(engine); + planeMaterial.baseColor = new Color(1.0, 0.2, 0, 1.0); + planeMaterial.roughness = 0.8; + planeMaterial.metallic = 0.2; + planeMaterial.renderFace = RenderFace.Double; + + planeRenderer.setMaterial(planeMaterial); + + // Create box + const boxRenderers = new Array(); + const boxMesh = PrimitiveMesh.createCuboid(engine, 2.0, 2.0, 2.0); + const boxMaterial = new PBRMaterial(engine); + boxMaterial.roughness = 0.2; + boxMaterial.metallic = 1; + for (let i = 0; i < 40; i++) { + const boxEntity = rootEntity.createChild("BoxEntity"); + boxEntity.transform.setPosition(0, 2, i * 10 - 200); + + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxRenderer.mesh = boxMesh; + boxRenderer.setMaterial(boxMaterial); + boxRenderers.push(boxRenderer); + } + + const visualMaterial = new CSSMVisualMaterial(engine); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + openDebug(); + engine.run(); + }); + + function openDebug(): void { + const info = { + rotation: false, + debugMode: false, + cascadeMode: ShadowCascadesMode.FourCascades, + resolution: ShadowResolution.High, + shadowType: ShadowType.SoftLow, + shadowTwoCascadeSplitRatio: 1.0 / 3, + shadowFourCascadeSplitRatioX: 1.0 / 15, + shadowFourCascadeSplitRatioY: 3.0 / 15.0, + shadowFourCascadeSplitRatioZ: 7.0 / 15.0, + }; + + const gui = new dat.GUI(); + gui.add(info, "rotation").onChange((v) => { + rotation.pause = !v; + }); + + gui.add(info, "debugMode").onChange((v) => { + if (v) { + planeRenderer.setMaterial(visualMaterial); + for (let i = 0; i < boxRenderers.length; i++) { + const boxRenderer = boxRenderers[i]; + boxRenderer.setMaterial(visualMaterial); + } + } else { + planeRenderer.setMaterial(planeMaterial); + for (let i = 0; i < boxRenderers.length; i++) { + const boxRenderer = boxRenderers[i]; + boxRenderer.setMaterial(boxMaterial); + } + } + }); + + gui.add(directLight, "shadowBias", 0, 10); + gui.add(directLight, "shadowNormalBias", 0, 10); + gui.add(directLight, "shadowStrength", 0, 1); + gui + .add(info, "shadowType", { + None: ShadowType.None, + Hard: ShadowType.Hard, + SoftLow: ShadowType.SoftLow, + VerySoft: ShadowType.SoftHigh, + }) + .onChange((v) => { + directLight.shadowType = parseInt(v); + }); + + gui + .add(info, "cascadeMode", { + NoCascades: ShadowCascadesMode.NoCascades, + TwoCascades: ShadowCascadesMode.TwoCascades, + FourCascades: ShadowCascadesMode.FourCascades, + }) + .onChange((v) => { + scene.shadowCascades = parseInt(v); + }); + + gui + .add(info, "resolution", { + Low: ShadowResolution.Low, + Medium: ShadowResolution.Medium, + High: ShadowResolution.High, + VeryHigh: ShadowResolution.VeryHigh, + }) + .onChange((v) => { + scene.shadowResolution = parseInt(v); + }); + gui.add(info, "shadowTwoCascadeSplitRatio", 0, 1).onChange((v) => { + scene.shadowTwoCascadeSplits = v; + }); + gui.add(info, "shadowFourCascadeSplitRatioX", 0, 1).onChange((v) => { + scene.shadowFourCascadeSplits.x = v; + }); + gui.add(info, "shadowFourCascadeSplitRatioY", 0, 1).onChange((v) => { + scene.shadowFourCascadeSplits.y = v; + }); + gui.add(info, "shadowFourCascadeSplitRatioZ", 0, 1).onChange((v) => { + scene.shadowFourCascadeSplits.z = v; + }); + } + + function createShadowMapVisualShader(): void { + Shader.create( + "shadow-map-visual", + ` + #include + #include + #include + #include + #include + #include + #include + #include + + #include + #include + + void main() { + + #include + #include + #include + #include + #include + #include + #include + #include + #include + + #include + + #include + + }`, + ` + #include + #include + + #include + #include + #include + #include + + #include + #include + #include + + void main() { + vec4 emission = material_EmissiveColor; + vec4 diffuse = material_BaseColor; + vec4 specular = material_SpecularColor; + vec4 ambient = vec4(scene_EnvMapLight.diffuse * scene_EnvMapLight.diffuseIntensity, 1.0) * diffuse; + + #ifdef SCENE_IS_CALCULATE_SHADOWS + int cascadeIndex = computeCascadeIndex(v_pos); + if (cascadeIndex == 0) { + diffuse = vec4(1.0, 1.0, 1.0, 1.0); + } else if (cascadeIndex == 1) { + diffuse = vec4(1.0, 0.0, 0.0, 1.0); + } else if (cascadeIndex == 2) { + diffuse = vec4(0.0, 1.0, 0.0, 1.0); + } else if (cascadeIndex == 3) { + diffuse = vec4(0.0, 0.0, 1.0, 1.0); + } + #endif + + gl_FragColor = emission + ambient + diffuse + specular; + gl_FragColor.a = diffuse.a; + } + ` + ); + } +} + +class CSSMVisualMaterial extends BaseMaterial { + constructor(engine: Engine) { + super(engine, Shader.find("shadow-map-visual")); + this.shaderData.enableMacro("SCENE_SHADOW_CASCADED_COUNT", "1"); + this.shaderData.enableMacro("MATERIAL_NEED_WORLDPOS"); + } +} + +class Rotation extends Script { + pause = true; + private _time = 0; + private _center = new Vector3(); + + onUpdate(deltaTime: number) { + if (!this.pause) { + this._time += deltaTime / 1000; + this.entity.transform.setPosition( + 10 * Math.cos(this._time), + 10, + 10 * Math.sin(this._time) + ); + this.entity.transform.lookAt(this._center); + } + } +} + +main(); diff --git a/examples/compressed-texture.ts b/examples/compressed-texture.ts new file mode 100644 index 000000000..9c39b4f45 --- /dev/null +++ b/examples/compressed-texture.ts @@ -0,0 +1,96 @@ +/** + * @title Compressed Texture + * @category Texture + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Zd-8TZmuuaoAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + DirectLight, + Logger, + MeshRenderer, + PrimitiveMesh, + Texture2D, + TextureFormat, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit"; +Logger.enable(); +WebGLEngine.create({ canvas: "canvas", ktx2Loader: { workerCount: 4 } }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.position = new Vector3(0, 0, 3); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild(); + lightEntity.addComponent(DirectLight).intensity = 0.5; + lightEntity.transform.setPosition(-5, 5, 5); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // material ball + const ball = rootEntity.createChild("ball"); + const ballRender = ball.addComponent(MeshRenderer); + const material = new UnlitMaterial(engine); + ball.transform.setRotation(90, 0, 0); + ballRender.mesh = PrimitiveMesh.createPlane(engine, 1, 1); + ballRender.setMaterial(material); + + // debug + const gui = new dat.GUI(); + + const fileList = { + etc1s: + "https://mdn.alipayobjects.com/rms/afts/img/A*ONENTaxi-LAAAAAAAAAAAAAAARQnAQ/2d_etc1s.ktx2", + uastc: + "https://mdn.alipayobjects.com/rms/afts/img/A*aP4mRJcGi6AAAAAAAAAAAAAAARQnAQ/2d_uastc.ktx2", + }; + + const formats: string[] = []; + const debugInfo = { + colorModel: "", + }; + for (let format in fileList) { + formats.push(format); + debugInfo.colorModel = "etc1s"; + } + + const transcodeFormat = { format: "" }; + + gui.add(debugInfo, "colorModel", formats).onChange((v) => { + loadTexture(v); + }); + + const formatController = gui.add(transcodeFormat, "format"); + console.log(formatController); + + function loadTexture(formatDes: string) { + const url = fileList[formatDes]; + engine.resourceManager + .load({ + type: AssetType.KTX2, + url, + }) + .then((res) => { + const compressedTexture = res; + material.baseTexture = compressedTexture; + formatController.setValue(TextureFormat[res.format]) + }); + } + + if (debugInfo.colorModel) { + loadTexture(debugInfo.colorModel); + } + + engine.run(); + } +); diff --git a/examples/controls-free.ts b/examples/controls-free.ts new file mode 100644 index 000000000..d244a8bcb --- /dev/null +++ b/examples/controls-free.ts @@ -0,0 +1,73 @@ +/** + * @title Free Controls + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*kGvvTZNvUJEAAAAAAAAAAAAADiR2AQ/original + */ +/** + * 本示例展示如何使用几何体渲染器功能、如何创建几何体资源对象、如何创建材质对象 + */ +import { + BlinnPhongMaterial, + Camera, + DirectLight, + MeshRenderer, + MeshTopology, + PrimitiveMesh, + WebGLEngine, +} from "@galacean/engine"; +import { FreeControl } from "@galacean/engine-toolkit-controls"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootNode = scene.createRootEntity("root"); + + // 在场景中创建相机节点、配置位置和目标方向 + const cameraNode = rootNode.createChild("Camera"); + cameraNode.transform.setPosition(0, 0, 20); + const camera = cameraNode.addComponent(Camera); + const controler = cameraNode.addComponent(FreeControl); + camera.farClipPlane = 2000; + controler.movementSpeed = 100; + controler.rotateSpeed = 1; + + const lightNode = rootNode.createChild("Light"); + lightNode.transform.setRotation(-45, 45, 0); + lightNode.addComponent(DirectLight); + + const cuboid = PrimitiveMesh.createCuboid(engine, 50, 50, 50); + const material = new BlinnPhongMaterial(engine); + + const groundGeometry = PrimitiveMesh.createPlane( + engine, + 2000, + 2000, + 100, + 100 + ); + groundGeometry.subMesh.topology = MeshTopology.LineStrip; + const groundMaterial = new BlinnPhongMaterial(engine); + + // create meshes in scene + for (let i = 0; i < 100; i++) { + let cube = rootNode.createChild("cube"); + cube.transform.setPosition( + Math.random() * 2000 - 1000, + Math.random() * 200, + Math.random() * 2000 - 1000 + ); + const cubeRenderer = cube.addComponent(MeshRenderer); + cubeRenderer.mesh = cuboid; + cubeRenderer.setMaterial(material); + } + + // Ground + const ground = rootNode.createChild("ground"); + ground.transform.setPosition(0, -25, 0); + const groundRender = ground.addComponent(MeshRenderer); + groundRender.mesh = groundGeometry; + groundRender.setMaterial(groundMaterial); + + // Run engine + engine.run(); +}); diff --git a/examples/culling-mask.ts b/examples/culling-mask.ts new file mode 100644 index 000000000..72a50b74b --- /dev/null +++ b/examples/culling-mask.ts @@ -0,0 +1,79 @@ +/** + * @title Culling Mask + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*iln0RKi5dIkAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + DirectLight, + Logger, + WebGLEngine, + Camera, + Vector3, + MeshRenderer, + BlinnPhongMaterial, + Color, + Layer, + PrimitiveMesh, +} from "@galacean/engine"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + const pos = cameraEntity.transform.position; + pos.set(10, 10, 10); + cameraEntity.transform.position = pos; + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + const lightNode = rootEntity.createChild("Light"); + lightNode.transform.setRotation(-30, 0, 0); + lightNode.addComponent(DirectLight); + + // init cube + const cubeEntity = rootEntity.createChild("cube"); + const renderer = cubeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + + engine.run(); + + function addGUI() { + const gui = new dat.GUI(); + const cameraFolder = gui.addFolder("camera cullingMask"); + cameraFolder.open(); + const constMap = { + EveryThing: Layer.Everything, + Layer1: Layer.Layer1, + Layer2: Layer.Layer2, + Layer3: Layer.Layer3, + }; + const cameraController = cameraFolder.add( + { cullingMask: "EveryThing" }, + "cullingMask", + Object.keys(constMap) + ); + cameraController.onChange((v) => { + camera.cullingMask = constMap[v]; + }); + + const boxFolder = gui.addFolder("box layer"); + boxFolder.open(); + const boxController = boxFolder.add( + { layer: "EveryThing" }, + "layer", + Object.keys(constMap) + ); + boxController.onChange((v) => { + renderer.entity.layer = constMap[v]; + }); + } + + addGUI(); +}); diff --git a/examples/custom-mesh.ts b/examples/custom-mesh.ts new file mode 100644 index 000000000..a531974e7 --- /dev/null +++ b/examples/custom-mesh.ts @@ -0,0 +1,351 @@ +/** + * @title Custom mesh + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*hxtTRZxXFOAAAAAAAAAAAAAADiR2AQ/original + */ +import { + BoundingBox, + Buffer, + BufferBindFlag, + Camera, + Entity, + MeshRenderer, + MeshTopology, + ModelMesh, + RenderFace, + SubMesh, + UnlitMaterial, + VertexAttribute, + VertexBufferBinding, + VertexElement, + VertexElementFormat, + WebGLEngine, +} from "@galacean/engine"; +import * as dat from "dat.gui"; +const gui = new dat.GUI(); +const debugInfo = { + shape: "Circle", + Circle: { radius: 100 }, + Ellipse: { halfWidth: 100, halfHeight: 50 }, + RoundedRect: { width: 200, height: 100, radius: 20 }, +}; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity + const root: Entity | null = engine.sceneManager.scenes[0].createRootEntity(); + + // Create camera + const cameraEntity = root.createChild("camera"); + cameraEntity.transform.setPosition(0, 0, 600); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + camera.orthographicSize = engine.canvas.height / 2; + camera.farClipPlane = 2000; + + const entity = root.createChild("renderer"); + const renderer = entity.addComponent(MeshRenderer); + const modelMesh = resetModelMesh("Circle"); + renderer.mesh = modelMesh; + const material = new UnlitMaterial(engine); + material.renderFace = RenderFace.Double; + renderer.setMaterial(material); + + engine.run(); + + function resetModelMesh(shape: string): ModelMesh { + const shapeInfo = debugInfo[shape]; + const modelMesh = new ModelMesh(engine); + const points = []; + switch (shape) { + case "Circle": + buildCircle(shapeInfo.radius, points); + break; + case "Ellipse": + buildEllipse(shapeInfo.halfWidth, shapeInfo.halfHeight, points); + break; + case "RoundedRect": + buildRoundedRect( + shapeInfo.width, + shapeInfo.height, + shapeInfo.radius, + points + ); + break; + default: + break; + } + const vertexData = new Float32Array((points.length / 2 + 1) * 3); + const indicesData = new Uint16Array((points.length / 2) * 3); + triangulate(points, vertexData, 3, 0, indicesData, 0, modelMesh.bounds); + modelMesh.setVertexElements([ + new VertexElement( + VertexAttribute.Position, + 0, + VertexElementFormat.Vector3, + 0 + ), + ]); + modelMesh.setVertexBufferBinding( + new VertexBufferBinding( + new Buffer(engine, BufferBindFlag.VertexBuffer, vertexData), + 12 + ) + ); + modelMesh.setIndices(indicesData); + modelMesh.addSubMesh( + new SubMesh(0, indicesData.length, MeshTopology.Triangles) + ); + modelMesh.uploadData(true); + return modelMesh; + } + + const initDatGUI = (shape: string) => { + let curFolder; + gui + .add(debugInfo, "shape", ["Circle", "Ellipse", "RoundedRect"]) + .onChange((v) => { + gui.removeFolder(curFolder); + curFolder = addFolder(v); + renderer.mesh = resetModelMesh(v); + }); + + curFolder = addFolder(shape); + function addFolder(shape: string) { + let folder; + switch (shape) { + case "Circle": + folder = gui.addFolder("Circle"); + folder.add(debugInfo.Circle, "radius", 1, 200).onChange((v) => { + renderer.mesh = resetModelMesh("Circle"); + }); + break; + case "Ellipse": + folder = gui.addFolder("Ellipse"); + folder.add(debugInfo.Ellipse, "halfWidth", 1, 200).onChange((v) => { + renderer.mesh = resetModelMesh("Ellipse"); + }); + folder.add(debugInfo.Ellipse, "halfHeight", 1, 200).onChange((v) => { + renderer.mesh = resetModelMesh("Ellipse"); + }); + break; + case "RoundedRect": + folder = gui.addFolder("RoundedRect"); + folder.add(debugInfo.RoundedRect, "width", 1, 200).onChange((v) => { + renderer.mesh = resetModelMesh("RoundedRect"); + }); + folder.add(debugInfo.RoundedRect, "height", 1, 200).onChange((v) => { + renderer.mesh = resetModelMesh("RoundedRect"); + }); + folder.add(debugInfo.RoundedRect, "radius", 1, 50).onChange((v) => { + renderer.mesh = resetModelMesh("RoundedRect"); + }); + break; + } + folder.open(); + return folder; + } + }; + initDatGUI("Circle"); +}); + +function buildCircle(radius: number, pointers: number[]): number[] { + const x = 0; + const y = 0; + const rx = radius; + const ry = radius; + const dx = 0; + const dy = 0; + build(x, y, rx, ry, dx, dy, pointers); + return pointers; +} + +function buildEllipse( + halfWidth: number, + halfHeight: number, + pointers: number[] +): number[] { + const x = 0; + const y = 0; + const rx = halfWidth; + const ry = halfHeight; + const dx = 0; + const dy = 0; + build(x, y, rx, ry, dx, dy, pointers); + return pointers; +} + +function buildRoundedRect( + width: number, + height: number, + radius: number, + pointers: number[] +): number[] { + const halfWidth = width / 2; + const halfHeight = height / 2; + const x = 0; + const y = 0; + const temp = Math.max(0, Math.min(radius, Math.min(halfWidth, halfHeight))); + const rx = temp; + const ry = temp; + const dx = halfWidth - temp; + const dy = halfHeight - temp; + build(x, y, rx, ry, dx, dy, pointers); + return pointers; +} + +function build( + x: number, + y: number, + rx: number, + ry: number, + dx: number, + dy: number, + points: number[] +) { + if (!(rx >= 0 && ry >= 0 && dx >= 0 && dy >= 0)) { + return points; + } + // Choose a number of segments such that the maximum absolute deviation from the circle is approximately 0.029 + const n = Math.ceil(2.3 * Math.sqrt(rx + ry)); + const m = n * 8 + (dx ? 4 : 0) + (dy ? 4 : 0); + if (m === 0) { + return points; + } + + if (n === 0) { + points[0] = points[6] = x + dx; + points[1] = points[3] = y + dy; + points[2] = points[4] = x - dx; + points[5] = points[7] = y - dy; + + return points; + } + + let j1 = 0; + let j2 = n * 4 + (dx ? 2 : 0) + 2; + let j3 = j2; + let j4 = m; + + let x0 = dx + rx; + let y0 = dy; + let x1 = x + x0; + let x2 = x - x0; + let y1 = y + y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j2] = y1; + points[--j2] = x2; + + if (dy) { + const y2 = y - y0; + + points[j3++] = x2; + points[j3++] = y2; + points[--j4] = y2; + points[--j4] = x1; + } + + for (let i = 1; i < n; i++) { + const a = (Math.PI / 2) * (i / n); + const x0 = dx + Math.cos(a) * rx; + const y0 = dy + Math.sin(a) * ry; + const x1 = x + x0; + const x2 = x - x0; + const y1 = y + y0; + const y2 = y - y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j2] = y1; + points[--j2] = x2; + points[j3++] = x2; + points[j3++] = y2; + points[--j4] = y2; + points[--j4] = x1; + } + + x0 = dx; + y0 = dy + ry; + x1 = x + x0; + x2 = x - x0; + y1 = y + y0; + const y2 = y - y0; + + points[j1++] = x1; + points[j1++] = y1; + points[--j4] = y2; + points[--j4] = x1; + + if (dx) { + points[j1++] = x2; + points[j1++] = y1; + points[--j4] = y2; + points[--j4] = x2; + } + + return points; +} + +function triangulate( + points: number[], + vertices: Float32Array, + verticesStride: number, + verticesOffset: number, + indices: Uint16Array, + indicesOffset: number, + bounds: BoundingBox +) { + if (points.length === 0) { + return; + } + + // Compute center (average of all points) + let centerX = 0; + let centerY = 0; + let minX: number, minY: number, minZ: number; + let maxX: number, maxY: number, maxZ: number; + + for (let i = 0; i < points.length; i += 2) { + centerX += points[i]; + centerY += points[i + 1]; + } + centerX /= points.length / 2; + centerY /= points.length / 2; + + // Set center vertex + let count = verticesOffset; + vertices[count * verticesStride] = minX = maxX = centerX; + vertices[count * verticesStride + 1] = minY = maxY = centerY; + vertices[count * verticesStride + 2] = minZ = maxY = 0; + const centerIndex = count++; + + // Set edge vertices and indices + for (let i = 0; i < points.length; i += 2) { + const x = points[i]; + const y = points[i + 1]; + const z = 0; + vertices[count * verticesStride] = x; + vertices[count * verticesStride + 1] = y; + vertices[count * verticesStride + 2] = z; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + if (i > 0) { + // Skip first point for indices + indices[indicesOffset++] = count; + indices[indicesOffset++] = centerIndex; + indices[indicesOffset++] = count - 1; + } + count++; + } + + // Connect last point to the first edge point + indices[indicesOffset++] = centerIndex + 1; + indices[indicesOffset++] = centerIndex; + indices[indicesOffset++] = count - 1; +} diff --git a/examples/device-restore.ts b/examples/device-restore.ts new file mode 100644 index 000000000..444925453 --- /dev/null +++ b/examples/device-restore.ts @@ -0,0 +1,107 @@ +/** + * @title Device restore + * @category Advance + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*U3AXS7Iq-AQAAAAAAAAAAAAADiR2AQ/original + */ +import { + Animator, + Camera, + Color, + GLTFResource, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + rootEntity.scene.background.solidColor = new Color(0.39, 0.31, 0.55, 1.0); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0.5, 7); + cameraEntity.transform.lookAt(new Vector3(0, 0.5, 0)); + + // Model from sketchfab: https://sketchfab.com/3d-models/cloud-station-26f81b24d83441ba88c7e80a52adbaaf + // Created by Alex Kruckenberg, Licensed under CC BY 4.0 + engine.resourceManager + .load( + "https://mdn.alipayobjects.com/huamei_b4l2if/afts/file/A*AGAoTLQHpJoAAAAAAAAAAAAADil6AQ/cloud_station.glb" + ) + .then((glTFResource) => { + const { defaultSceneRoot, animations } = glTFResource; + rootEntity.addChild(defaultSceneRoot); + + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(animations![0].name); + + engine.on("devicelost", () => { + domText.textContent = "Device is lost!"; + restoreButton.disabled = false; + }); + + engine.on("devicerestored", () => { + domText.textContent = "Device restored!"; + lostButton.disabled = false; + }); + + engine.run(); + + // Simulate device loss and recovery + // The actual project does not need to call `forceLoseDevice()` and `forceRestoreDevice()` + // These two methods are only used to simulate whether the performance of device loss and restoration is correct + initDomElement(engine); + }); +}); + +const domText = document.createElement("div"); +const lostButton = document.createElement("button"); +const restoreButton = document.createElement("button"); + +function initDomElement(engine: WebGLEngine): void { + // Text + const textStyle = domText.style; + textStyle.whiteSpace = "nowrap"; + textStyle.position = "absolute"; + textStyle.top = "40%"; + textStyle.left = "50%"; + textStyle.transform = "translate(-50%, -50%)"; + textStyle.fontSize = "60px"; + textStyle.color = "orange"; + textStyle.fontWeight = "bold"; + document.body.appendChild(domText); + + // Lost button + lostButton.textContent = "Force lost device"; + let lostStyle = lostButton.style; + lostStyle.position = "absolute"; + lostStyle.bottom = "10%"; + lostStyle.left = "40%"; + lostStyle.transform = "translate(-50%, -50%)"; + lostStyle.width = "200px"; + lostStyle.height = "50px"; + document.body.appendChild(lostButton); + lostButton.addEventListener("click", function () { + engine.forceLoseDevice(); + domText.textContent = "Force lost device..."; + lostButton.disabled = true; + }); + + // Restore button + restoreButton.textContent = "Force restore device"; + const restoreStyle = restoreButton.style; + restoreStyle.position = "absolute"; + restoreStyle.bottom = "10%"; + restoreStyle.right = "40%"; + restoreStyle.transform = "translate(50%, -50%)"; + restoreStyle.width = "200px"; + restoreStyle.height = "50px"; + restoreButton.disabled = true; + document.body.appendChild(restoreButton); + restoreButton.addEventListener("click", function () { + engine.forceRestoreDevice(); + domText.textContent = "Force restore device..."; + restoreButton.disabled = true; + }); +} diff --git a/examples/draw-lines.ts b/examples/draw-lines.ts new file mode 100644 index 000000000..7c0d53dac --- /dev/null +++ b/examples/draw-lines.ts @@ -0,0 +1,326 @@ +/** + * @title Draw Lines + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*bpb1TYb2Q_gAAAAAAAAAAAAADiR2AQ/original + */ +import { + WebGLEngine, + Mesh, + ModelMesh, + Script, + Entity, + StaticCollider, + BoxColliderShape, + MathUtil, + Quaternion, + RenderFace, + Camera, + Color, + Engine, + MeshRenderer, + UnlitMaterial, + Vector3, +} from "@galacean/engine"; +import * as dat from "dat.gui"; +import { LitePhysics } from "@galacean/engine-physics-lite"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +const gui = new dat.GUI(); +const tempLine = new Vector3(); +const tempPerpendicular = new Vector3(); +const tempP1 = new Vector3(); +const tempP2 = new Vector3(); +const tempP3 = new Vector3(); +const tempP4 = new Vector3(); +const tempZAxis = new Vector3(0, 0, 1); +const tempRotateAxis = new Vector3(); + +class DrawScript extends Script { + private _preDrawTime: number = 0; + private _prePointer: Vector3 = new Vector3(); + private _meshEntity: Entity; + private _meshMaterial: UnlitMaterial; + + private _camera: Camera; + private _depth: number = 10; + private _lineWidth: number = 0.1; + private _forward: Vector3 = new Vector3(); + private _precision: number = 15; + private _drawInterval: number = 0.03; + private _color: Color = new Color(1, 1, 1, 1); + + private tempPointer: Vector3 = new Vector3(); + + set camera(val: Camera) { + this._camera = val; + this._forward.copyFrom(val.entity.transform.worldForward); + } + + set lineWidth(val: number) { + this._lineWidth = val; + } + + set precision(val: number) { + this._precision = val; + } + + set depth(val: number) { + this._depth = val; + } + + set drawInterval(val: number) { + this._drawInterval = val; + } + + setColor(r: number, g: number, b: number, a: number) { + this._meshMaterial = new UnlitMaterial(this.engine); + this._meshMaterial.renderFace = RenderFace.Double; + this._meshMaterial.baseColor = this._color.set(r, g, b, a); + } + + onStart(): void { + this._meshEntity = this.entity.createChild("mesh"); + this._meshMaterial = new UnlitMaterial(this.engine); + this._meshMaterial.renderFace = RenderFace.Double; + this._meshMaterial.baseColor = this._color; + } + + onPointerDrag(): void { + const now = this.engine.time.elapsedTime; + if (now - this._preDrawTime >= this._drawInterval) { + this._preDrawTime = now; + const { tempPointer: endPointer, _prePointer: startPointer } = this; + const { x: screenX, y: screenY } = + this.engine.inputManager.pointers[0].position; + this._camera.screenToWorldPoint( + endPointer.set(screenX, screenY, this._depth), + endPointer + ); + const { x: sx, y: sy, z: sz } = startPointer; + const { x: ex, y: ey, z: ez } = endPointer; + if (sx === ex && sy === ey && sz === ez) { + return; + } + const { + _meshEntity: meshEntity, + _forward: forward, + _lineWidth: lineWidth, + _meshMaterial: meshMaterial, + } = this; + // Draw circle. + const rendererCircle = meshEntity.addComponent(MeshRenderer); + rendererCircle.mesh = createCircleMesh( + this.engine, + endPointer, + forward, + lineWidth, + this._precision + ); + rendererCircle.setMaterial(meshMaterial); + // Draw line. + const renderer = meshEntity.addComponent(MeshRenderer); + renderer.mesh = createLineMesh( + this.engine, + startPointer, + endPointer, + forward, + lineWidth + ); + renderer.setMaterial(meshMaterial); + startPointer.set(ex, ey, ez); + } + } + + onPointerDown(): void { + // Screen pointer to world pointer. + this._preDrawTime = this.engine.time.elapsedTime; + const { x: screenX, y: screenY } = + this.engine.inputManager.pointers[0].position; + const { _prePointer: startPointer } = this; + this._camera.screenToWorldPoint( + startPointer.set(screenX, screenY, this._depth), + startPointer + ); + // Draw circle. + const renderer = this._meshEntity.addComponent(MeshRenderer); + renderer.mesh = createCircleMesh( + this.engine, + this._prePointer, + this._forward, + this._lineWidth, + this._precision + ); + renderer.setMaterial(this._meshMaterial); + } +} + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // init light + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const cameraControl = cameraEntity.addComponent(OrbitControl); + cameraControl.enabled = false; + + // init plane + const planeEntity = rootEntity.createChild("camera"); + const planeCollider = planeEntity.addComponent(StaticCollider); + const planeShape = new BoxColliderShape(); + planeShape.size.set(20, 20, 1); + planeCollider.addShape(planeShape); + const planeScript = planeEntity.addComponent(DrawScript); + planeScript.camera = camera; + + engine.run(); + + // Debug + const debugInfo = { + mode: "Draw", + lineWidth: 0.1, + precision: 15, + depth: 10, + lineColor: [255, 255, 255], + drawInterval: 30, + resetView: () => { + cameraControl.enabled && cameraEntity.transform.setPosition(0, 0, 10); + }, + }; + + gui.add(debugInfo, "mode", ["Observe", "Draw"]).onChange((v: string) => { + if (v === "Draw") { + planeScript.camera = camera; + planeScript.enabled = true; + cameraControl.enabled = false; + } else { + planeScript.enabled = false; + cameraControl.enabled = true; + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + } + }); + + gui.add(debugInfo, "lineWidth", 0.01, 2, 0.02).onChange((v: number) => { + planeScript.lineWidth = v; + }); + + gui.add(debugInfo, "precision", 4, 40, 1).onChange((v: number) => { + planeScript.precision = v; + }); + + gui.add(debugInfo, "depth", 5, 15, 0.5).onChange((v: number) => { + planeScript.depth = v; + }); + + gui.add(debugInfo, "drawInterval", 15, 100, 1).onChange((v: number) => { + planeScript.drawInterval = v; + }); + + gui.addColor(debugInfo, "lineColor").onChange((v: number) => { + planeScript.setColor(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + + gui.add(debugInfo, "resetView"); + } +); + +/** + * Draw a line segment perpendicular to the forward vector. + * @param startPos - Start world position + * @param endPos - End world position + * @param forwardVec3 - Forward vector + * @param lineWidth - Line width + * @returns ModelMesh containing mesh information + */ +function createLineMesh( + engine: Engine, + startPos: Vector3, + endPos: Vector3, + forwardVec3: Vector3, + lineWidth: number +): Mesh { + // Get direction vector. + Vector3.subtract(endPos, startPos, tempLine); + // Get perpendicular vector. + Vector3.cross(tempLine, forwardVec3, tempPerpendicular); + tempPerpendicular.normalize().scale(lineWidth / 2); + // Get four vertices. + Vector3.add(startPos, tempPerpendicular, tempP1); + Vector3.subtract(startPos, tempPerpendicular, tempP2); + Vector3.add(tempP1, tempLine, tempP3); + Vector3.add(tempP2, tempLine, tempP4); + // Draw two triangles. + const mesh = new ModelMesh(engine); + mesh.setPositions([tempP1, tempP2, tempP3, tempP4]); + mesh.setIndices(new Uint16Array([0, 1, 2, 2, 1, 3])); + mesh.addSubMesh(0, 6); + mesh.uploadData(false); + return mesh; +} + +/** + * Draw a circle perpendicular to the forward vector. + * @param pos - The world position of the center of the circle + * @param forwardVec3 - Forward vector + * @param lineWidth - Line width + * @param precision - Precision + * @returns ModelMesh containing mesh information + */ +function createCircleMesh( + engine: Engine, + pos: Vector3, + forwardVec3: Vector3, + lineWidth: number, + precision: number +): Mesh { + Vector3.cross(tempZAxis, forwardVec3, tempRotateAxis); + const vec3Arr: Vector3[] = []; + const axisLen = tempRotateAxis.length(); + const rad = (2 * Math.PI) / precision; + if (axisLen <= MathUtil.zeroTolerance) { + for (let i = 0; i < precision; i++) { + const vec3 = new Vector3( + (lineWidth * Math.sin(rad * i)) / 2, + (lineWidth * Math.cos(rad * i)) / 2, + 0 + ); + vec3Arr.push(vec3.add(pos)); + } + } else { + const rotateVal = + Vector3.dot(tempZAxis, forwardVec3) > 0 + ? Math.asin(axisLen) + : Math.PI - Math.asin(axisLen); + const quat = new Quaternion(); + quat.rotationAxisAngle(tempRotateAxis, rotateVal); + for (let i = 0; i < precision; i++) { + const vec3 = new Vector3( + (lineWidth * Math.sin(rad * i)) / 2, + (lineWidth * Math.cos(rad * i)) / 2, + 0 + ); + vec3.transformByQuat(quat); + vec3Arr.push(vec3.add(pos)); + } + } + vec3Arr.push(pos); + const mesh = new ModelMesh(engine); + mesh.setPositions(vec3Arr); + const indexArr = []; + for (let i = 0; i < precision; i++) { + indexArr.push(i, precision, (i + 1) % precision); + } + mesh.setIndices(new Uint16Array(indexArr)); + mesh.addSubMesh(0, indexArr.length); + mesh.uploadData(false); + return mesh; +} diff --git a/examples/filter-mode.ts b/examples/filter-mode.ts new file mode 100644 index 000000000..09708816c --- /dev/null +++ b/examples/filter-mode.ts @@ -0,0 +1,74 @@ +/** + * @title Filter Mode + * @category Texture + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*EC0zSI4-gC8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + MeshRenderer, + PrimitiveMesh, + RenderFace, + Texture2D, + TextureFilterMode, + UnlitMaterial, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 1); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create Plane + const mesh = PrimitiveMesh.createPlane(engine, 2, 2); + const material = new UnlitMaterial(engine); + material.renderFace = RenderFace.Double; + const planeEntity = rootEntity.createChild("ground"); + planeEntity.transform.setRotation(5, 0, 0); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeRenderer.mesh = mesh; + planeRenderer.setMaterial(material); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_CtuR7LW4C0AAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + material.baseTexture = texture; + material.tilingOffset.set(30, 30, 0, 0); + addGUI(texture); + engine.run(); + }); + + function addGUI(texture: Texture2D) { + const filterMap: Record = { + [TextureFilterMode.Point]: "Point", + [TextureFilterMode.Bilinear]: "Bilinear", + [TextureFilterMode.Trilinear]: "Trilinear", + }; + const state = { + filterMode: filterMap[texture.filterMode], + }; + gui.add(state, "filterMode", Object.values(filterMap)).onChange((v) => { + for (let key in filterMap) { + const value = filterMap[key]; + if (v === value) { + texture.filterMode = Number(key); + } + } + }); + } +}); diff --git a/examples/flappy-bird.ts b/examples/flappy-bird.ts new file mode 100644 index 000000000..4e841fd4a --- /dev/null +++ b/examples/flappy-bird.ts @@ -0,0 +1,806 @@ +/** + * @title Flappy Bird + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*T7WET7OAjkAAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BoxColliderShape, + Camera, + Engine, + Entity, + Keys, + MeshRenderer, + PrimitiveMesh, + Rect, + Script, + Sprite, + SpriteRenderer, + StaticCollider, + Texture2D, + UnlitMaterial, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import * as TWEEN from "@tweenjs/tween.js"; +import { LitePhysics } from "@galacean/engine-physics-lite"; + +enum EnumBirdState { + Alive = 0, + Dead = 1, +} + +enum EnumGameState { + Idel = 0, + Start = 1, + End = 2, +} + +/** The y coordinate of the ground collision detection. */ +const groundY = -3.1; + +const GameEvent = { + fly: "fly", + stateChange: "stateChange", + showGui: "showGui", + checkHitGui: "checkHitGui", + checkHit: "checkHit", + gameOver: "gameOver", + addScore: "addScore", + reStartGame: "reStartGame", +}; + +let gameResArray: Texture2D[]; +// We can customize the size of the interface that is finally presented to the player. +const designWidth = 768; +const designHeight = 896; +const aspectRatio = designWidth / designHeight; +const canvas = document.getElementById("canvas"); +const parentEle = canvas.parentElement; +let { clientWidth, clientHeight } = parentEle; +if (clientWidth / clientHeight > aspectRatio) { + clientWidth = clientHeight * aspectRatio; + canvas.style.width = clientWidth + "px"; + canvas.style.marginLeft = (parentEle.clientWidth - clientWidth) / 2 + "px"; +} else { + clientHeight = clientWidth / aspectRatio; + canvas.style.height = clientHeight + "px"; + canvas.style.marginTop = (parentEle.clientHeight - clientHeight) / 2 + "px"; +} +// Create engine object. +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(designHeight / clientHeight); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(0.3, 0, 5); + const camera = cameraEntity.addComponent(Camera); + // 2D is more suitable for orthographic cameras. + camera.isOrthographic = true; + // @ts-ignore + camera.orthographicSize = engine.canvas.height / Engine._pixelsPerUnit / 2; + + // Load the resources needed by the game. + engine.resourceManager + // @ts-ignore + .load([ + { + // Background. + url: "https://gw.alipayobjects.com/zos/OasisHub/315000157/5244/background.png", + type: AssetType.Texture2D, + }, + { + // Pipe. + url: "https://gw.alipayobjects.com/zos/OasisHub/315000157/5987/pipe.png", + type: AssetType.Texture2D, + }, + { + // Ground. + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Sj7OS4YJHDIAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + // Bird. + url: "https://gw.alipayobjects.com/zos/OasisHub/315000157/8356/bird.png", + type: AssetType.Texture2D, + }, + { + // Restart. + url: "https://gw.alipayobjects.com/zos/OasisHub/315000157/6695/restart.png", + type: AssetType.Texture2D, + }, + { + // Number. + url: "https://gw.alipayobjects.com/zos/OasisHub/315000157/8709/527-number.png", + type: AssetType.Texture2D, + }, + ]) + .then((texture2DArr: Texture2D[]) => { + // Record the resources. + gameResArray = texture2DArr; + // Initialize location information and component information. + // Background. + const nodeBg = rootEntity.createChild("nodeBg"); + nodeBg.transform.setPosition(0.3, 0, -10); + nodeBg.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + texture2DArr[0] + ); + + // Pipe. + const nodePipe = rootEntity.createChild("nodePipe"); + nodePipe.transform.setPosition(0, 0, -3); + nodePipe.addComponent(ScriptPipe); + + // Ground. + const nodeGround = rootEntity.createChild("nodeGround"); + nodeGround.transform.setPosition(0.3, -4.125, -2); + nodeGround.transform.setRotation(90, 0, 0); + const groundRenderer = nodeGround.addComponent(MeshRenderer); + groundRenderer.mesh = PrimitiveMesh.createPlane(engine, 7.68, 1.28); + const groundMaterial = new UnlitMaterial(engine); + groundRenderer.setMaterial(groundMaterial); + groundMaterial.baseTexture = texture2DArr[2]; + groundMaterial.tilingOffset.set(21, 1, 0, 0); + nodeGround.addComponent(ScriptGround); + + // Bird. + const nodeBird = rootEntity.createChild("nodeBird"); + nodeBird.transform.setPosition(-1, 1.15, 0); + nodeBird.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + texture2DArr[3] + ); + nodeBird.addComponent(ScriptBird); + + // Death Effect. + const nodeDeathEff = rootEntity.createChild("nodeEff"); + nodeDeathEff.transform.setPosition(0, 0, -1); + nodeDeathEff.transform.setRotation(90, 0, 0); + const effRenderer = nodeDeathEff.addComponent(MeshRenderer); + effRenderer.mesh = PrimitiveMesh.createPlane(engine, 20, 20); + const material = new UnlitMaterial(engine); + effRenderer.setMaterial(material); + // Can be transparent. + material.isTransparent = true; + material.baseColor.set(0, 0, 0, 0); + nodeDeathEff.addComponent(ScriptDeathEff); + + // GUI. + const nodeGui = rootEntity.createChild("nodeGui"); + nodeGui.transform.setPosition(0.3, 0, 1); + // Restart. + const nodeRestart = nodeGui.createChild("nodeRestart"); + nodeRestart.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + texture2DArr[4] + ); + // Score. + const nodeScore = nodeGui.createChild("nodeScore"); + nodeScore.transform.setPosition(0, 3.2, 0); + nodeScore.transform.setScale(0.3, 0.3, 0.3); + nodeScore.addComponent(ScriptScore); + nodeGui.addComponent(ScriptGUI); + + // GameCtrl controls the global game. + rootEntity.addComponent(GameCtrl); + }); + + engine.run(); + + class ScriptPipe extends Script { + /** When there is no pipe in the pool, use this instance to clone. */ + private _originPipe: Entity; + /** All current pipes. */ + private _nowPipeArr: Array = []; + /** Pool for reuse. */ + private _pipePool: Array = []; + /** Timestamp of the start of the game. */ + private _curStartTimeStamp: number; + /** Hide when the x coordinate of the pipe is less than -4.6. */ + private _pipeHideX: number = 4.6; + /** Vertical distance of pipe. */ + private _pipeVerticalDis: number = 10.8; + /** Horizontal distance of pipe. */ + private _pipeHorizontalDis: number = 4; + /** Random location range generated by pipes. */ + private _pipeRandomPosY: number = 3.5; + /** Water pipe debut time(s). */ + private _pipeDebutTime: number = 3; + /** Horizontal movement speed. */ + private _pipeHorizontalV: number = 3; + + onAwake() { + // Init originPipe. + const pipe = (this._originPipe = new Entity(engine)); + const node1 = pipe.createChild("node1"); + const node2 = pipe.createChild("node2"); + const verticalDis = this._pipeVerticalDis; + node1.transform.setPosition(0, -verticalDis / 2, 0); + node2.transform.setPosition(0, verticalDis / 2, 0); + node2.transform.setScale(1, -1, 1); + node1.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + gameResArray[1] + ); + node2.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + gameResArray[1] + ); + this._pipePool.push(pipe); + + // Control the performance of the pipe according to the change of the game state. + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + switch (gameState) { + case EnumGameState.Idel: + this.enabled = false; + this._destroyPipe(); + break; + case EnumGameState.Start: + this.enabled = true; + this._curStartTimeStamp = engine.time.actualElapsedTime; + break; + case EnumGameState.End: + this.enabled = false; + break; + } + }); + + // When checkHit is monitored, check the collision between the pipe and the bird. + engine.on(GameEvent.checkHit, (birdY: number) => { + var len = this._nowPipeArr.length; + for (var i = 0; i < len; i++) { + var pipePos = this._nowPipeArr[i].transform.position; + if (Math.abs(pipePos.x) < 0.9) { + if (Math.abs(pipePos.y - birdY) > 1.2) { + engine.dispatch(GameEvent.gameOver); + } + break; + } + } + }); + } + + /** + * Three things will be done here every frame: + * 1.Adjust the generation of the pipe. + * 2.Adjust the position of the pipe. + * 3.Judge whether to get a point. + * @param deltaTime - The deltaTime when the script update + */ + onUpdate(deltaTime: number) { + const debutTime = this._pipeDebutTime; + // The water pipe will be displayed after the start of the game pipeDebutTime. + if ( + engine.time.actualElapsedTime - this._curStartTimeStamp >= + debutTime + ) { + let bAddScore = false; + // After deltaTime, the distance the pipe has moved. + const changeVal = deltaTime * this._pipeHorizontalV; + const pipeLen = this._nowPipeArr.length; + const { + _pipeHorizontalDis: horizontalDis, + _pipeRandomPosY: randomPosY, + _pipeHideX: hideX, + } = this; + // Adjust the position of all pipes. + if (pipeLen > 0) { + for (let i = pipeLen - 1; i >= 0; i--) { + const pipe = this._nowPipeArr[i]; + const pipeTrans = pipe.transform; + const pipePos = pipeTrans.position; + if (pipePos.x < -hideX) { + // The invisible pipe can be destroyed. + this._destroyPipe(i); + } else { + if ( + !bAddScore && + pipePos.x > -1 && + pipePos.x - changeVal <= -1 + ) { + // Get a point. + engine.dispatch(GameEvent.addScore); + bAddScore = true; + } + pipeTrans.setPosition( + pipePos.x - changeVal, + pipePos.y, + pipePos.z + ); + } + // Judge whether the pipe needs to be regenerated according to the X coordinate. + if (i == pipeLen - 1 && pipePos.x <= hideX - horizontalDis) { + this._createPipe( + hideX, + randomPosY * Math.random() - randomPosY / 2 + 0.8, + 0 + ); + } + } + } else { + // Need to regenerate a pipe. + this._createPipe( + hideX, + randomPosY * Math.random() - randomPosY / 2 + 0.8, + 0 + ); + } + } + } + + private _createPipe(posX: number, posY: number, posZ: number) { + const pipePool = this._pipePool; + const pipe = + pipePool.length > 0 + ? pipePool.pop() + : this._originPipe.clone(); + pipe.transform.setPosition(posX, posY, posZ); + this.entity.addChild(pipe); + this._nowPipeArr.push(pipe); + } + + /** + * It’s not really destroyed, we just put it in the pool. + * @param pipeIndex - If pipeIndex is less than 0, we recycle all pipes + */ + private _destroyPipe(pipeIndex: number = -1) { + const { entity, _pipePool, _nowPipeArr } = this; + const removePipe = (pipe: Entity) => { + entity.removeChild(pipe); + _pipePool.push(pipe); + }; + if (pipeIndex >= 0) { + removePipe(_nowPipeArr[pipeIndex]); + _nowPipeArr.splice(pipeIndex, 1); + } else { + for (let index = _nowPipeArr.length - 1; index >= 0; index--) { + removePipe(_nowPipeArr[index]); + } + _nowPipeArr.length = 0; + } + } + } + + class ScriptScore extends Script { + /** The sprite array used by the score(0~9). */ + private _spriteArray: Sprite[] = []; + /** Interval between each number. */ + private _numInv = 2; + /** Each number in the score. */ + private _scoreEntitys: Entity[] = []; + private _scoreRenderer: SpriteRenderer[] = []; + private _myScore = 0; + + onAwake() { + // Init spriteArray. + const spriteArray = this._spriteArray; + // Cut digital resources into ten. + for (var i = 0; i < 10; i++) { + spriteArray.push( + new Sprite(engine, gameResArray[5], new Rect(i * 0.1, 0, 0.1, 1)) + ); + } + + engine.on(GameEvent.addScore, () => { + ++this._myScore; + this._showScore("" + this._myScore); + }); + + // Control the performance of the score according to the change of the game state. + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + switch (gameState) { + case EnumGameState.Idel: + this.entity.isActive = false; + break; + case EnumGameState.Start: + this.entity.isActive = true; + this._myScore = 0; + this._showScore("0"); + break; + case EnumGameState.End: + break; + } + }); + } + + private _showScore(scoreNumStr: string) { + const scoreLen = scoreNumStr.length; + const { + entity, + _numInv: inv, + _scoreEntitys: scoreEntitys, + _spriteArray: spriteArray, + _scoreRenderer: scoreRenderers, + } = this; + var nowEntityLen = scoreEntitys.length; + let scoreEntity: Entity; + let scoreRenderer: SpriteRenderer; + // If the entity is not enough, new one. + if (scoreLen > nowEntityLen) { + for (let i = nowEntityLen; i < scoreLen; i++) { + scoreEntity = new Entity(engine); + scoreRenderer = scoreEntity.addComponent(SpriteRenderer); + scoreRenderers.push(scoreRenderer); + scoreEntitys.push(scoreEntity); + entity.addChild(scoreEntity); + } + } + + // At the moment nowEntityLen >= scoreLen. + nowEntityLen = scoreEntitys.length; + const startX = ((1 - scoreLen) * inv) / 2; + for (let i = 0; i < nowEntityLen; i++) { + scoreEntity = scoreEntitys[i]; + if (i >= scoreLen) { + scoreEntity.isActive = false; + } else { + scoreEntity.isActive = true; + scoreEntity.transform.setPosition(startX + i * inv, 0, 0); + scoreRenderers[i].sprite = spriteArray[parseInt(scoreNumStr[i])]; + } + } + } + } + + class ScriptGround extends Script { + /** Swap two pieces of ground to achieve constant movement. */ + private _groundMaterial: UnlitMaterial; + /** Horizontal movement speed. */ + private _groundHorizontalV: number = 8.2; + + onAwake() { + this._groundMaterial = ( + this.entity.getComponent(MeshRenderer).getMaterial() + ); + // Control the performance of the ground according to the change of the game state. + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + switch (gameState) { + case EnumGameState.Idel: + case EnumGameState.Start: + this.enabled = true; + break; + case EnumGameState.End: + this.enabled = false; + break; + default: + break; + } + }); + + // When checkHit is monitored, check the collision between the ground and the bird. + engine.on(GameEvent.checkHit, (birdY) => { + birdY < groundY && engine.dispatch(GameEvent.gameOver); + }); + } + + onUpdate(deltaTime: number) { + // After deltaTime, the distance the ground has moved. + this._groundMaterial.tilingOffset.z += + deltaTime * this._groundHorizontalV; + } + } + + class GameCtrl extends Script { + private _gameState: EnumGameState; + + onAwake() { + engine.on(GameEvent.reStartGame, () => { + this._setGameState(EnumGameState.Idel); + }); + + engine.on(GameEvent.gameOver, () => { + this._setGameState(EnumGameState.End); + }); + + const boxCollider: StaticCollider = + this.entity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(10, 10, 0.001); + boxCollider.addShape(boxColliderShape); + } + + onStart() { + // Give a state at the beginning. + this._setGameState(EnumGameState.Idel); + } + + onUpdate() { + // Update TWEEN. + TWEEN.update(); + if (engine.inputManager.isKeyDown(Keys.Space)) { + this._dispatchFly(); + } + } + + onPointerDown() { + this._dispatchFly(); + } + + private _dispatchFly() { + switch (this._gameState) { + case EnumGameState.Idel: + this._setGameState(EnumGameState.Start); + engine.dispatch(GameEvent.fly); + break; + case EnumGameState.Start: + engine.dispatch(GameEvent.fly); + break; + default: + break; + } + } + + /** + * The status will be distributed to all objects in the game. + * @param state - EnumGameState + */ + private _setGameState(state: EnumGameState) { + if (this._gameState != state) { + this._gameState = state; + engine.dispatch(GameEvent.stateChange, state); + } + } + } + + class ScriptGUI extends Script { + onAwake() { + const { entity } = this; + const resetBtnNode = entity.findByName("nodeRestart"); + + // Add BoxCollider. + const boxCollider: StaticCollider = + resetBtnNode.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(2.14, 0.75, 0.001); + boxCollider.addShape(boxColliderShape); + resetBtnNode.addComponent(Script).onPointerClick = () => { + engine.dispatch(GameEvent.reStartGame); + }; + + // Control the performance of the GUI according to the change of the game state. + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + switch (gameState) { + case EnumGameState.Idel: + case EnumGameState.Start: + resetBtnNode.isActive = false; + break; + case EnumGameState.End: + break; + default: + break; + } + }); + + engine.on(GameEvent.showGui, () => { + resetBtnNode.isActive = true; + }); + } + } + + class ScriptBird extends Script { + /** Offsets of sprite sheet animation. */ + private _regions: Vector2[] = [ + new Vector2(0, 0), + new Vector2(1 / 3, 0), + new Vector2(2 / 3, 0), + ]; + /** Reciprocal Of SliceWidth. */ + private _reciprocalSliceWidth: number = 1 / 3; + /** Reciprocal Of SliceHeight. */ + private _reciprocalSliceHeight: number = 1; + /** Frame interval time, the unit of time is s. */ + private _frameInterval = 0.15; + /** Total frames. */ + private _totalFrames = 3; + /** Maximum downward speed */ + private _maxDropV = -8; + /** Acceleration of gravity */ + private _gravity = -35; + /** Initial upward speed given during fly */ + private _startFlyV = 10; + + private _cumulativeTime: number = 0; + private _birdState = EnumBirdState.Alive; + + private _sprite: Sprite; + private _curFrameIndex: number; + private _startY: number; + private _flyStartTime: number; + private _gameState: EnumGameState; + private _yoyoTween; + private _dropTween; + + onAwake() { + this._initDataAndUI(); + this._initTween(); + this._initListener(); + } + + onUpdate(deltaTime: number) { + // Update the performance of the bird. + switch (this._birdState) { + case EnumBirdState.Alive: + const { _frameInterval, _totalFrames } = this; + this._cumulativeTime += deltaTime; + if (this._cumulativeTime >= _frameInterval) { + // Need update frameIndex. + const addFrameCount = Math.floor( + this._cumulativeTime / _frameInterval + ); + this._cumulativeTime -= addFrameCount * _frameInterval; + this._setFrameIndex( + (this._curFrameIndex + addFrameCount) % _totalFrames + ); + } + + // Update bird's location information. + if (this._gameState == EnumGameState.Start) { + // Free fall and uniform motion are superimposed to obtain the current position. + let endY; + const { _maxDropV, _startFlyV, _gravity } = this; + const transform = this.entity.transform; + const position = transform.position; + // How much time has passed. + const subTime = + engine.time.actualElapsedTime - this._flyStartTime; + // How long has it been in free fall. + const addToMaxUseTime = (_maxDropV - _startFlyV) / _gravity; + if (subTime <= addToMaxUseTime) { + // Free fall. + endY = + ((_startFlyV + (_startFlyV + subTime * _gravity)) * subTime) / + 2 + + this._startY; + } else { + // Falling at a constant speed. + endY = + ((_maxDropV + _startFlyV) * addToMaxUseTime) / 2 + + _maxDropV * (subTime - addToMaxUseTime) + + this._startY; + } + transform.setPosition(position.x, endY, position.z); + } + break; + case EnumBirdState.Dead: + this._setFrameIndex(0); + break; + } + } + + onLateUpdate() { + // After updating the position, check the collision. + engine.dispatch(GameEvent.checkHit, this.entity.transform.position.y); + } + + private _initDataAndUI() { + const { entity } = this; + const renderer = entity.getComponent(SpriteRenderer); + renderer.sprite = this._sprite = new Sprite(engine, gameResArray[3]); + this._setFrameIndex(0); + } + + private _initTween() { + const transform = this.entity.transform; + const rotation = transform.rotation; + const position = transform.position; + this._yoyoTween = new TWEEN.Tween(position) + .to({ y: 0.25 }, 380) + .repeat(Infinity) + .onUpdate((target) => { + transform.position = target; + }) + .yoyo(true) + .easing(TWEEN.Easing.Sinusoidal.InOut); + this._dropTween = new TWEEN.Tween(rotation) + .to({ z: -90 }, 380) + .onUpdate((target) => { + transform.rotation = target; + }) + .delay(520); + } + + private _initListener() { + const transform = this.entity.transform; + engine.on(GameEvent.fly, () => { + // Record start time and location. + this._startY = transform.position.y; + this._flyStartTime = engine.time.actualElapsedTime; + // Flying performance. + this._yoyoTween.stop(); + this._dropTween.stop(); + transform.setRotation(transform.rotation.x, transform.rotation.y, 20); + this._dropTween.start(); + }); + + // Control the performance of the bird according to the change of the game state. + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + this._gameState = gameState; + switch (gameState) { + case EnumGameState.Idel: + transform.setPosition(0, 0, 0); + transform.rotation = new Vector3(0, 0, 0); + this._birdState = EnumBirdState.Alive; + this._yoyoTween.start(); + break; + case EnumGameState.Start: + break; + case EnumGameState.End: + this._birdState = EnumBirdState.Dead; + setTimeout(() => { + const { position, rotation } = transform; + const birdPosY = position.y; + if (birdPosY > groundY) { + this._yoyoTween.stop(); + this._dropTween.stop(); + new TWEEN.Tween(rotation) + .duration((birdPosY - groundY) * 40) + .to({ z: -90 }) + .onUpdate((target) => { + transform.rotation = target; + }) + .start(); + new TWEEN.Tween(position) + .duration((birdPosY - groundY) * 120) + .to({ y: groundY }) + .onUpdate((target) => { + transform.position = target; + }) + .onComplete(() => { + engine.dispatch(GameEvent.showGui); + }) + .start(); + } else { + engine.dispatch(GameEvent.showGui); + } + }, 300); + break; + } + }); + } + + private _setFrameIndex(frameIndex: number) { + if (this._curFrameIndex !== frameIndex) { + this._curFrameIndex = frameIndex; + const frameInfo = this._regions[frameIndex]; + const region = this._sprite.region; + region.set( + frameInfo.x, + frameInfo.y, + this._reciprocalSliceWidth, + this._reciprocalSliceHeight + ); + this._sprite.region = region; + } + } + } + + class ScriptDeathEff extends Script { + onAwake() { + const entity = this.entity; + const renderer = entity.getComponent(MeshRenderer); + const material = renderer.getMaterial(); + + // init Tween. + const baseColor = material.baseColor; + const shockTween = new TWEEN.Tween(baseColor) + .to({ a: 1 }, 80) + .repeat(1) + .yoyo(true) + .delay(20); + engine.on(GameEvent.stateChange, (gameState: EnumGameState) => { + switch (gameState) { + case EnumGameState.End: + shockTween.start(); + break; + } + }); + } + } + } +); diff --git a/examples/framebuffer-picker.ts b/examples/framebuffer-picker.ts new file mode 100644 index 000000000..e34b8a1a9 --- /dev/null +++ b/examples/framebuffer-picker.ts @@ -0,0 +1,164 @@ +/** + * @title Framebuffer Picker + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*xk9IQqUAigEAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + PBRMaterial, + PointerButton, + Script, + Vector3, + WebGLEngine, + PrimitiveMesh, + MeshRenderer, + AmbientLight, + AssetType, + Vector2, + Layer, +} from "@galacean/engine"; +import { + FramebufferPicker, + OutlineManager, + LineDrawer, +} from "@galacean/engine-toolkit"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + const cameraEntity = rootEntity.createChild("camera_node"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 30); + + const mesh = PrimitiveMesh.createCuboid(engine); + for (let i = 0; i < 100; i++) { + const entity = rootEntity.createChild(); + entity.layer = Layer.Layer1; + + entity.transform.setPosition( + Math.random() * 80 - 40, + Math.random() * 45 - 25, + Math.random() * 45 - 25 + ); + entity.transform.setRotation( + Math.random() * 180, + Math.random() * 180, + Math.random() * 180 + ); + entity.transform.setScale( + Math.random() * 2 + 1, + Math.random() * 2 + 1, + Math.random() * 2 + 1 + ); + + const render = entity.addComponent(MeshRenderer); + render.mesh = mesh; + const mtl = new PBRMaterial(engine); + mtl.metallic = 0.0; + mtl.roughness = 0.5; + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + + render.setMaterial(mtl); + } + + const outlineManager = cameraEntity.addComponent(OutlineManager); + + const framebufferPicker = cameraEntity.addComponent(FramebufferPicker); + const { width, height } = engine.canvas; + framebufferPicker.frameBufferSize = new Vector2(width, height); + + const boxBorderEntity = rootEntity.createChild("line"); + boxBorderEntity.addComponent(MeshRenderer); + boxBorderEntity.addComponent(LineDrawer); + + class ClickScript extends Script { + public depth: number = 0.1; + private _startPoint: Vector2 = new Vector2(); + private _endPoint: Vector2 = new Vector2(); + + private _tempVec0: Vector3 = new Vector3(); + private _tempVec1: Vector3 = new Vector3(); + private _tempVec2: Vector3 = new Vector3(); + private _tempVec3: Vector3 = new Vector3(); + + getRectVertex(from: Vector2, to: Vector2) { + this._tempVec0.set(from.x, from.y, this.depth); + this._tempVec1.set(to.x, from.y, this.depth); + this._tempVec2.set(to.x, to.y, this.depth); + this._tempVec3.set(from.x, to.y, this.depth); + + camera.screenToWorldPoint(this._tempVec0, this._tempVec0); + camera.screenToWorldPoint(this._tempVec1, this._tempVec1); + camera.screenToWorldPoint(this._tempVec2, this._tempVec2); + camera.screenToWorldPoint(this._tempVec3, this._tempVec3); + } + + onUpdate(): void { + const inputManager = this.engine.inputManager; + const { pointers } = inputManager; + if (pointers && inputManager.isPointerDown(PointerButton.Primary)) { + if (pointers.length > 0) { + this._startPoint.copyFrom(pointers[0].position); + + // single selection + framebufferPicker + .pick(this._startPoint.x, this._startPoint.y) + .then((renderer) => { + if (!renderer || !renderer.entity) { + outlineManager.clear(); + return; + } + if (renderer.entity.layer === Layer.Layer1) { + outlineManager.addEntity(renderer.entity); + } + }); + } + } + + if (pointers && inputManager.isPointerHeldDown(PointerButton.Primary)) { + if (pointers.length > 0) { + this._endPoint.copyFrom(pointers[0].position); + this.getRectVertex(this._startPoint, this._endPoint); + + // multi selection + LineDrawer.drawRect( + this._tempVec0, + this._tempVec1, + this._tempVec2, + this._tempVec3 + ); + + framebufferPicker + .regionPick( + this._startPoint.x, + this._startPoint.y, + this._endPoint.x, + this._endPoint.y + ) + .then((renderers) => { + renderers.forEach((value) => { + if (value.entity.layer === Layer.Layer1) { + outlineManager.addEntity(value.entity); + } + }); + }); + } + } + } + } + + cameraEntity.addComponent(ClickScript); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); +}); diff --git a/examples/gizmo.ts b/examples/gizmo.ts new file mode 100644 index 000000000..45bbe772d --- /dev/null +++ b/examples/gizmo.ts @@ -0,0 +1,238 @@ +/** + * @title Gizmo + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*RdMcTqLMQtcAAAAAAAAAAAAADiR2AQ/original + */ + +/** + * 本示例展示如何使用Navigation Gizmo控制场景相机, 以及通过Gizmo控制物体移动、缩放、旋转 + */ + +import { + Camera, + Color, + DirectLight, + Entity, + GLTFResource, + Layer, + PointerButton, + Renderer, + Script, + ShadowType, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; +import { FramebufferPicker, GridControl, NavigationGizmo, OrbitControl } from "@galacean/engine-toolkit"; +import { AnchorType, CoordinateType, Gizmo, Group, State } from "@galacean/engine-toolkit-gizmo"; +import * as dat from "dat.gui"; + +const LayerSetting = { + Entity: Layer.Layer22, + NavigationGizmo: Layer.Layer30, + Gizmo: Layer.Layer31 +}; + +const gui = new dat.GUI(); +const traverseEntity = (entity: Entity, callback: (entity: Entity) => any) => { + callback(entity); + for (const child of entity.children) { + traverseEntity(child, callback); + } +}; + +// setup scene +WebGLEngine.create({ + canvas: "canvas", + physics: new LitePhysics() +}).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const { background } = scene; + background.solidColor = new Color(0.8, 0.8, 0.8, 1); + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("fullscreen-camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(8, 5, 8); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + camera.cullingMask &= ~LayerSetting.NavigationGizmo; + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setPosition(20, 20, 20); + lightEntity.transform.setRotation(-45, 0, 0); + const light = lightEntity.addComponent(DirectLight); + light.shadowType = ShadowType.None; + + const ambientLight = scene.ambientLight; + ambientLight.diffuseSolidColor.set(0.8, 0.8, 1, 1); + ambientLight.diffuseIntensity = 0.5; + + const grid = rootEntity.addComponent(GridControl); + grid.camera = camera; + grid.distance = 2; + + class ControlScript extends Script { + public group = new Group(); + public gizmo: Gizmo; + + private _framebufferPicker: FramebufferPicker; + private _orbitControl: OrbitControl; + private _navigator: NavigationGizmo; + + constructor(entity: Entity) { + super(entity); + // add framebufferPicker + this._framebufferPicker = cameraEntity.addComponent(FramebufferPicker); + + // add orbit control + this._orbitControl = camera.entity.addComponent(OrbitControl); + this._orbitControl.maxPolarAngle = Infinity; + this._orbitControl.minPolarAngle = -Infinity; + + // add gizmo + const gizmoEntity = entity.createChild("gizmo"); + const gizmo = gizmoEntity.addComponent(Gizmo); + gizmo.init(camera, this.group); + gizmo.state = State.scale; + gizmo.layer = LayerSetting.Gizmo; + this.gizmo = gizmo; + + gizmoEntity.isActive = false; + + // add navigation gizmo + const navigatorEntity = entity.createChild("navigation-gizmo"); + this._navigator = navigatorEntity.addComponent(NavigationGizmo); + this._navigator.camera = camera; + this._navigator.layer = LayerSetting.NavigationGizmo; + + this._addGUI(); + } + + onUpdate(deltaTime: number): void { + const { inputManager } = engine; + const { pointers } = inputManager; + + // sharing same camera target + this._navigator.target = this._orbitControl.target; + + // single select + if (pointers && inputManager.isPointerDown(PointerButton.Primary)) { + const { position } = pointers[0]; + this._framebufferPicker.pick(position.x, position.y).then((result) => { + this._selectHandler(result); + }); + } + } + + // left mouse for single selection + private _selectHandler(result: Renderer) { + const selectedEntity = result?.entity; + + switch (selectedEntity?.layer) { + case undefined: { + this._orbitControl.enabled = true; + this.group.reset(); + this.gizmo.entity.isActive = false; + break; + } + case LayerSetting.Entity: { + this._orbitControl.enabled = true; + this.group.reset(); + this.group.addEntity(selectedEntity); + this.gizmo.entity.isActive = true; + break; + } + case LayerSetting.Gizmo: { + this._orbitControl.enabled = false; + break; + } + } + } + + private _addGUI() { + const info = { + Gizmo: State.translate, + Coordinate: CoordinateType.Local, + Anchor: AnchorType.Center + }; + const gizmoConfig = ["null", "translate", "rotate", "scale", "all"]; + const orientationConfig = ["global", "local"]; + const pivotConfig = ["center", "pivot"]; + + gui + .add(info, "Gizmo", gizmoConfig) + .onChange((v: string) => { + switch (v) { + case "null": + // @ts-ignore + this.gizmo.state = null; + break; + case "translate": + this.gizmo.state = State.translate; + break; + case "rotate": + this.gizmo.state = State.rotate; + break; + case "scale": + this.gizmo.state = State.scale; + break; + case "all": + this.gizmo.state = State.all; + break; + } + }) + .setValue("all"); + + gui + .add(info, "Coordinate", orientationConfig) + .onChange((v: string) => { + switch (v) { + case "global": + this.group.coordinateType = CoordinateType.Global; + break; + case "local": + this.group.coordinateType = CoordinateType.Local; + break; + } + }) + .setValue("local"); + + gui + .add(info, "Anchor", pivotConfig) + .onChange((v: string) => { + switch (v) { + case "center": + this.group.anchorType = AnchorType.Center; + break; + case "pivot": + this.group.anchorType = AnchorType.Pivot; + break; + } + }) + .setValue("center"); + } + } + + const controlEntity = rootEntity.createChild("control"); + const sceneControl = controlEntity.addComponent(ControlScript); + + engine.resourceManager + .load("https://mdn.alipayobjects.com/oasis_be/afts/file/A*AmbsSpS0IAcAAAAAAAAAAAAADkp5AQ/boxPBR.glb") + .then((gltf) => { + const { defaultSceneRoot } = gltf; + rootEntity.addChild(defaultSceneRoot); + defaultSceneRoot.transform.scale.set(0.01, 0.01, 0.01); + traverseEntity(defaultSceneRoot, (entity) => { + entity.layer = LayerSetting.Entity; + }); + + // init scene as selected state + sceneControl.group.reset(); + sceneControl.group.addEntity(defaultSceneRoot); + sceneControl.gizmo.entity.isActive = true; + }) + .then(() => { + engine.run(); + }); +}); diff --git a/examples/gltf-basic.ts b/examples/gltf-basic.ts new file mode 100644 index 000000000..904a03b58 --- /dev/null +++ b/examples/gltf-basic.ts @@ -0,0 +1,35 @@ +/** + * @title GLTF Basic + * @category Basic + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*FplEQ5vCzl8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { Camera, GLTFResource, WebGLEngine } from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(3, 3, 3); + cameraEntity.addComponent(OrbitControl); + + engine.sceneManager.activeScene.ambientLight.diffuseSolidColor.set( + 1, + 1, + 1, + 1 + ); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf" + ) + .then((gltf) => { + rootEntity.addChild(gltf.defaultSceneRoot); + }); + + engine.run(); +}); diff --git a/examples/gltf-loader.ts b/examples/gltf-loader.ts new file mode 100644 index 000000000..cdd9d42e4 --- /dev/null +++ b/examples/gltf-loader.ts @@ -0,0 +1,467 @@ +/** + * @title GLTF Loader + * @category Advance + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*omVHSr3cHpIAAAAAAAAAAAAADiR2AQ/original + */ +import { + AmbientLight, + AnimationClip, + Animator, + AssetType, + BackgroundMode, + BoundingBox, + Camera, + Color, + DirectLight, + Entity, + GLTFResource, + Logger, + Material, + MeshRenderer, + PBRBaseMaterial, + PBRMaterial, + PBRSpecularMaterial, + PrimitiveMesh, + Renderer, + Scene, + SkyBoxMaterial, + Texture2D, + UnlitMaterial, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const envList = { + sunset: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + pisa: "https://gw.alipayobjects.com/os/bmw-prod/6470ea5e-094b-4a77-a05f-4945bf81e318.bin", + foot_2K: "https://gw.alipayobjects.com/os/bmw-prod/23c1893a-fe29-4e91-bd6a-bb1c4201a876.bin" +}; + +class Oasis { + static guiToColor(gui: number[], color: Color) { + color.set(gui[0] / 255, gui[1] / 255, gui[2] / 255, color.a); + } + + static colorToGui(color: Color = new Color(1, 1, 1)): number[] { + const v: number[] = []; + v[0] = color.r * 255; + v[1] = color.g * 255; + v[2] = color.b * 255; + return v; + } + + textures: Record = {}; + env: Record = {}; + + engine: WebGLEngine; + scene: Scene; + skyMaterial: SkyBoxMaterial; + + // Entity + rootEntity: Entity; + cameraEntity: Entity; + gltfRootEntity: Entity; + lightEntity1: Entity; + lightEntity2: Entity; + + // Component + camera: Camera; + controler: OrbitControl; + light1: DirectLight; + light2: DirectLight; + + // Debug + gui = new dat.GUI(); + materialFolder = null; + animationFolder = null; + state = { + // Scene + background: false, + // Lights + env: "sunset", + envIntensity: 1, + addLights: true, + lightColor: Oasis.colorToGui(new Color(1, 1, 1)), + lightIntensity: 0.8, + // GLTF Model List + defaultGLTF: "fox", + gltfList: { + "2CylinderEngine": "https://gw.alipayobjects.com/os/bmw-prod/48a1e8b3-06b4-4269-807d-79274e58283a.glb", + alphaBlendModeTest: "https://gw.alipayobjects.com/os/bmw-prod/d099b30b-59a3-42e4-99eb-b158afa8e65d.glb", + animatedCube: "https://gw.alipayobjects.com/os/bmw-prod/8cc524dd-2481-438d-8374-3c933adea3b6.gltf", + antiqueCamera: "https://gw.alipayobjects.com/os/bmw-prod/93196534-bab3-4559-ae9f-bcb3e36a6419.glb", + avocado: "https://gw.alipayobjects.com/os/bmw-prod/0f978c4d-1cd6-4cec-9a4c-b58c8186e063.glb", + avocado_draco: "https://gw.alipayobjects.com/os/bmw-prod/b3b73614-cf06-4f41-940d-c1bc04cf6410.gltf", + avocado_specular: "https://gw.alipayobjects.com/os/bmw-prod/3cf50452-0015-461e-a172-7ea1f8135e53.gltf", + avocado_quantized: "https://gw.alipayobjects.com/os/bmw-prod/6aff5409-8e82-4e77-a6ac-55b6adfd0cf5.gltf", + barramundiFish: "https://gw.alipayobjects.com/os/bmw-prod/79d7935c-404b-4d8d-9ad3-5f8c273f0e4a.glb", + boomBox: "https://gw.alipayobjects.com/os/bmw-prod/2e98b1c0-18e8-45d0-b54e-dcad6ef05e22.glb", + boomBoxWithAxes: "https://gw.alipayobjects.com/os/bmw-prod/96e1b8b2-9be6-4b64-98ea-8c008c6dc422.gltf", + boxVertexColors: "https://gw.alipayobjects.com/os/bmw-prod/6cff6fcb-5191-465e-9a38-dee42a07cc65.glb", + brianStem: "https://gw.alipayobjects.com/os/bmw-prod/e3b37dd9-9407-4b5c-91b3-c5880d081329.glb", + buggy: "https://gw.alipayobjects.com/os/bmw-prod/d6916a14-b004-42d5-86dd-d8520b719288.glb", + cesiumMan: "https://gw.alipayobjects.com/os/bmw-prod/3a7d9597-7354-4bef-b314-b84509bef9b6.glb", + cesiumMilkTruck: "https://gw.alipayobjects.com/os/bmw-prod/7701125a-7d0d-4281-a7d8-7f0dfc8d0792.glb", + corset: "https://gw.alipayobjects.com/os/bmw-prod/3deaa5a4-5b2a-4a0d-8512-a8c4377a08ff.glb", + DamagedHelmet: "https://gw.alipayobjects.com/os/bmw-prod/a1da72a4-023e-4bb1-9629-0f4b0f6b6fc4.glb", + Duck: "https://gw.alipayobjects.com/os/bmw-prod/6cb8f543-285c-491a-8cfd-57a1160dc9ab.glb", + environmentTest: "https://gw.alipayobjects.com/os/bmw-prod/7c7b887c-05d6-43dd-b354-216e738e81ed.gltf", + flightHelmet: "https://gw.alipayobjects.com/os/bmw-prod/d6dbf161-48e2-4e6d-bbca-c481ed9f1a2d.gltf", + fox: "https://gw.alipayobjects.com/os/bmw-prod/f40ef8dd-4c94-41d4-8fac-c1d2301b6e47.glb", + animationInterpolationTest: "https://gw.alipayobjects.com/os/bmw-prod/4f410ef2-20b4-494d-85f1-a806c5070bfb.glb", + lantern: "https://gw.alipayobjects.com/os/bmw-prod/9557420a-c622-4e9c-bb46-f7af8b19d7de.glb", + materialsVariantsShoe: "https://gw.alipayobjects.com/os/bmw-prod/b1a414bb-61ea-4667-94d2-ef6cf179ac0d.glb", + metalRoughSpheres: "https://gw.alipayobjects.com/os/bmw-prod/67b39ede-1c82-4321-8457-0ad83ca9413a.glb", + normalTangentTest: "https://gw.alipayobjects.com/os/bmw-prod/4bb1a66c-55e3-4898-97d7-a9cc0f239686.glb", + normalTangentMirrorTest: "https://gw.alipayobjects.com/os/bmw-prod/8335f555-2061-47f5-9252-366c6ebf882a.glb", + orientationTest: "https://gw.alipayobjects.com/os/bmw-prod/16cdf390-ac42-411c-9d2b-8e112dfd723b.glb", + sparseTest: "https://gw.alipayobjects.com/os/bmw-prod/f00de659-53ea-49d1-8f2c-d0a412542b80.gltf", + specGlossVsMetalRough: "https://gw.alipayobjects.com/os/bmw-prod/4643bd7b-f988-4144-8245-4a390023d92d.glb", + sponza: "https://gw.alipayobjects.com/os/bmw-prod/ca50859b-d736-4a3e-9fc3-241b0bd2afef.gltf", + suzanne: "https://gw.alipayobjects.com/os/bmw-prod/3869e495-2e04-4e80-9d22-13b37116379a.gltf", + sheenChair: "https://gw.alipayobjects.com/os/bmw-prod/34847225-bc1b-4bef-9cb9-6b9711ca2f8c.glb", + sheenCloth: "https://gw.alipayobjects.com/os/bmw-prod/4529d2e8-22a8-47af-9b38-8eaff835f6bf.gltf", + textureCoordinateTest: "https://gw.alipayobjects.com/os/bmw-prod/5fd23201-51dd-4eea-92d3-c4a72ecc1f2b.glb", + textureEncodingTest: "https://gw.alipayobjects.com/os/bmw-prod/b8795e57-3c2c-4412-b4f0-7ffa796e4917.glb", + textureSettingTest: "https://gw.alipayobjects.com/os/bmw-prod/a4b26d58-bd49-4051-8f05-0fe8b532e716.glb", + textureTransformMultiTest: "https://gw.alipayobjects.com/os/bmw-prod/8fa18786-5188-4c14-94d7-77bf6b8483f9.glb", + textureTransform: "https://gw.alipayobjects.com/os/bmw-prod/6c59d5d0-2e2e-4933-a737-006d431977fd.gltf", + toyCar: "https://gw.alipayobjects.com/os/bmw-prod/8138b7d7-45aa-494a-8aea-0c67598b96d2.glb", + transmissionTest: "https://gw.alipayobjects.com/os/bmw-prod/1dd51fe8-bdd3-42e4-99be-53de5dc106b8.glb", + unlitTest: "https://gw.alipayobjects.com/os/bmw-prod/06a855be-ac8c-4705-b5d7-659b8b391189.glb", + vc: "https://gw.alipayobjects.com/os/bmw-prod/b71c4212-25fb-41bb-af88-d79ce6d3cc58.glb", + vertexColorTest: "https://gw.alipayobjects.com/os/bmw-prod/8fc70cc6-66d8-43c8-b460-f7d2aaa9edcf.glb", + waterBottle: "https://gw.alipayobjects.com/os/bmw-prod/0f403530-96f5-455a-8a39-b31ac68b6ed5.glb" + } + }; + _materials: Material[] = []; + + // temporary + _boundingBox: BoundingBox = new BoundingBox(); + _center: Vector3 = new Vector3(); + _extent: Vector3 = new Vector3(); + + constructor() { + WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + this.engine = engine; + this.scene = this.engine.sceneManager.activeScene; + this.skyMaterial = new SkyBoxMaterial(this.engine); + this.rootEntity = this.scene.createRootEntity("root"); + this.cameraEntity = this.rootEntity.createChild("camera"); + this.gltfRootEntity = this.rootEntity.createChild("gltf"); + this.lightEntity1 = this.rootEntity.createChild("direct_light1"); + this.lightEntity2 = this.rootEntity.createChild("direct_light2"); + this.camera = this.cameraEntity.addComponent(Camera); + this.controler = this.cameraEntity.addComponent(OrbitControl); + this.light1 = this.lightEntity1.addComponent(DirectLight); + this.light2 = this.lightEntity2.addComponent(DirectLight); + + this.loadEnv(this.state.env).then(() => { + this.initScene(); + this.addGLTFList(); + this.addSceneGUI(); + }); + }); + } + + loadEnv(envName: string) { + return new Promise((resolve) => { + this.engine.resourceManager + .load({ + type: AssetType.Env, + url: envList[envName] + }) + .then((env) => { + this.env[envName] = env; + + this.scene.ambientLight = env; + this.skyMaterial.texture = env.specularTexture; + this.skyMaterial.textureDecodeRGBM = true; + resolve(true); + }); + }); + } + + initScene() { + this.engine.canvas.resizeByClientSize(); + this.controler.minDistance = 0; + + // debug sync + if (this.state.background) { + this.scene.background.mode = BackgroundMode.Sky; + } + if (!this.state.addLights) { + this.light1.enabled = this.light2.enabled = false; + } + this.light1.intensity = this.light2.intensity = this.state.lightIntensity; + this.lightEntity1.transform.setRotation(30, 0, 0); + this.lightEntity2.transform.setRotation(-30, 180, 0); + this.scene.ambientLight.specularIntensity = this.state.envIntensity; + this.scene.background.sky.material = this.skyMaterial; + this.scene.background.sky.mesh = PrimitiveMesh.createCuboid(this.engine, 1, 1, 1); + this.engine.run(); + } + + addGLTFList() { + this.loadGLTF(this.state.gltfList[this.state.defaultGLTF]); + this.gui + .add(this.state, "defaultGLTF", Object.keys(this.state.gltfList)) + .name("GLTF List") + .onChange((v) => { + this.loadGLTF(this.state.gltfList[v]); + }); + } + + addSceneGUI() { + const { gui } = this; + // Display controls. + const dispFolder = gui.addFolder("Scene"); + dispFolder.add(this.state, "background").onChange((v: boolean) => { + if (v) { + this.scene.background.mode = BackgroundMode.Sky; + } else { + this.scene.background.mode = BackgroundMode.SolidColor; + } + }); + + // Lighting controls. + const lightFolder = gui.addFolder("Lighting"); + lightFolder + .add(this.state, "env", [...Object.keys(envList)]) + .name("IBL") + .onChange((v) => { + this.loadEnv(v); + }); + + lightFolder + .add(this.state, "addLights") + .onChange((v) => { + this.light1.enabled = this.light2.enabled = v; + }) + .name("直接光"); + lightFolder.addColor(this.state, "lightColor").onChange((v) => { + Oasis.guiToColor(v, this.light1.color); + Oasis.guiToColor(v, this.light2.color); + }); + lightFolder + .add(this.state, "lightIntensity", 0, 2) + .onChange((v) => { + this.light1.intensity = this.light2.intensity = v; + }) + .name("直接光强度"); + } + + setCenter(renderers: Renderer[]) { + const boundingBox = this._boundingBox; + const center = this._center; + const extent = this._extent; + + boundingBox.min.set(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); + boundingBox.max.set(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); + + renderers.forEach((renderer) => { + BoundingBox.merge(renderer.bounds, boundingBox, boundingBox); + }); + boundingBox.getExtent(extent); + const size = extent.length(); + + boundingBox.getCenter(center); + this.controler.target.set(center.x, center.y, center.z); + this.cameraEntity.transform.setPosition(center.x, center.y, size * 3); + + this.camera.farClipPlane = size * 12; + + if (this.camera.nearClipPlane > size) { + this.camera.nearClipPlane = size / 10; + } else { + this.camera.nearClipPlane = 0.1; + } + + this.controler.maxDistance = size * 10; + } + + loadGLTF(url: string) { + this.destroyGLTF(); + this.engine.resourceManager + .load({ + type: AssetType.GLTF, + url + }) + .then((asset) => { + const { materials, animations } = asset; + const defaultSceneRoot = asset.instantiateSceneRoot(); + this.gltfRootEntity = defaultSceneRoot; + this.rootEntity.addChild(defaultSceneRoot); + + const meshRenderers = []; + defaultSceneRoot.getComponentsIncludeChildren(MeshRenderer, meshRenderers); + + this.setCenter(meshRenderers); + this.loadMaterialGUI(materials); + this.loadAnimationGUI(animations as AnimationClip[]); + }) + .catch((e) => { + console.error(e); + }); + } + + destroyGLTF() { + this.gltfRootEntity.destroy(); + } + + loadMaterialGUI(materials?: Material[]) { + const { gui } = this; + if (this.materialFolder) { + gui.removeFolder(this.materialFolder); + this.materialFolder = null; + } + if (!materials) { + materials = this._materials; + } + this._materials = materials; + if (!materials.length) return; + + const folder = (this.materialFolder = gui.addFolder("Material")); + const folderName = {}; + + materials.forEach((material) => { + if (!(material instanceof PBRBaseMaterial) && !(material instanceof UnlitMaterial)) return; + if (!material.name) { + material.name = "default"; + } + const state = { + opacity: material.baseColor.a, + baseColor: Oasis.colorToGui(material.baseColor), + emissiveColor: Oasis.colorToGui((material as PBRBaseMaterial).emissiveColor), + specularColor: Oasis.colorToGui((material as PBRSpecularMaterial).specularColor), + baseTexture: material.baseTexture ? "origin" : "", + roughnessMetallicTexture: (material as PBRMaterial).roughnessMetallicTexture ? "origin" : "", + normalTexture: (material as PBRBaseMaterial).normalTexture ? "origin" : "", + emissiveTexture: (material as PBRBaseMaterial).emissiveTexture ? "origin" : "", + occlusionTexture: (material as PBRBaseMaterial).occlusionTexture ? "origin" : "", + specularGlossinessTexture: (material as PBRSpecularMaterial).specularGlossinessTexture ? "origin" : "" + }; + + const originTexture = { + baseTexture: material.baseTexture, + roughnessMetallicTexture: (material as PBRMaterial).roughnessMetallicTexture, + normalTexture: (material as PBRBaseMaterial).normalTexture, + emissiveTexture: (material as PBRBaseMaterial).emissiveTexture, + occlusionTexture: (material as PBRBaseMaterial).occlusionTexture, + specularGlossinessTexture: (material as PBRSpecularMaterial).specularGlossinessTexture + }; + + const f = folder.addFolder( + folderName[material.name] ? `${material.name}_${folderName[material.name] + 1}` : material.name + ); + + folderName[material.name] = folderName[material.name] == null ? 1 : folderName[material.name] + 1; + + // metallic + if (material instanceof PBRMaterial) { + const mode1 = f.addFolder("金属模式"); + mode1.add(material, "metallic", 0, 1).step(0.01); + mode1.add(material, "roughness", 0, 1).step(0.01); + mode1.add(material, "ior", 0, 5).step(0.01); + + mode1 + .add(state, "roughnessMetallicTexture", ["None", "origin", ...Object.keys(this.textures)]) + .onChange((v) => { + material.roughnessMetallicTexture = + v === "None" ? null : this.textures[v] || originTexture.roughnessMetallicTexture; + }); + mode1.open(); + } + // specular + else if (material instanceof PBRSpecularMaterial) { + const mode2 = f.addFolder("高光模式"); + mode2.add(material, "glossiness", 0, 1).step(0.01); + mode2.addColor(state, "specularColor").onChange((v) => { + Oasis.guiToColor(v, material.specularColor); + }); + mode2 + .add(state, "specularGlossinessTexture", ["None", "origin", ...Object.keys(this.textures)]) + .onChange((v) => { + material.specularGlossinessTexture = + v === "None" ? null : this.textures[v] || originTexture.specularGlossinessTexture; + }); + mode2.open(); + } else if (material instanceof UnlitMaterial) { + f.add(state, "baseTexture", ["None", "origin", ...Object.keys(this.textures)]).onChange((v) => { + material.baseTexture = v === "None" ? null : this.textures[v] || originTexture.baseTexture; + }); + + f.addColor(state, "baseColor").onChange((v) => { + Oasis.guiToColor(v, material.baseColor); + }); + } + + // common + if (!(material instanceof UnlitMaterial)) { + const common = f.addFolder("通用"); + + common + .add(state, "opacity", 0, 1) + .step(0.01) + .onChange((v) => { + material.baseColor.a = v; + }); + common.add(material, "isTransparent"); + common.add(material, "alphaCutoff", 0, 1).step(0.01); + + common.addColor(state, "baseColor").onChange((v) => { + Oasis.guiToColor(v, material.baseColor); + }); + common.addColor(state, "emissiveColor").onChange((v) => { + Oasis.guiToColor(v, material.emissiveColor); + }); + common.add(state, "baseTexture", ["None", "origin", ...Object.keys(this.textures)]).onChange((v) => { + material.baseTexture = v === "None" ? null : this.textures[v] || originTexture.baseTexture; + }); + common.add(state, "normalTexture", ["None", "origin", ...Object.keys(this.textures)]).onChange((v) => { + material.normalTexture = v === "None" ? null : this.textures[v] || originTexture.normalTexture; + }); + common.add(state, "emissiveTexture", ["None", "origin", ...Object.keys(this.textures)]).onChange((v) => { + material.emissiveTexture = v === "None" ? null : this.textures[v] || originTexture.emissiveTexture; + }); + common.add(state, "occlusionTexture", ["None", "origin", ...Object.keys(this.textures)]).onChange((v) => { + material.occlusionTexture = v === "None" ? null : this.textures[v] || originTexture.occlusionTexture; + }); + common.open(); + } + }); + } + + loadAnimationGUI(animations: AnimationClip[]) { + if (this.animationFolder) { + this.gui.removeFolder(this.animationFolder); + this.animationFolder = null; + } + + if (animations?.length) { + this.animationFolder = this.gui.addFolder("Animation"); + this.animationFolder.open(); + const animator = this.gltfRootEntity.getComponent(Animator); + animator.play(animations[0].name); + const state = { + animation: animations[0].name + }; + this.animationFolder + .add(state, "animation", ["None", ...animations.map((animation) => animation.name)]) + .onChange((name) => { + if (name === "None") { + animator.speed = 0; + } else { + animator.speed = 1; + animator.play(name); + } + }); + } + } +} + +new Oasis(); diff --git a/examples/hdr-loader.ts b/examples/hdr-loader.ts new file mode 100644 index 000000000..fd996d83d --- /dev/null +++ b/examples/hdr-loader.ts @@ -0,0 +1,50 @@ +/** + * @title HDR Background + * @category Scene + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*28IwT4efgy8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + BackgroundMode, + Camera, + Logger, + PrimitiveMesh, + SkyBoxMaterial, + TextureCube, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.position = new Vector3(-3, 0, 3); + const camera = cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + camera.fieldOfView = 65; + + // Create sky + const sky = scene.background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + scene.background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + engine.resourceManager + .load({ + type: AssetType.HDR, + url: "https://gw.alipayobjects.com/os/bmw-prod/b578946a-8a25-4543-8161-fa92f92ae1ac.bin", + }) + .then((texture) => { + skyMaterial.texture = texture; + // HDR output is in RGBM format. + skyMaterial.textureDecodeRGBM = true; + engine.run(); + }); +}); diff --git a/examples/ibl-baker.ts b/examples/ibl-baker.ts new file mode 100644 index 000000000..d883a9941 --- /dev/null +++ b/examples/ibl-baker.ts @@ -0,0 +1,229 @@ +/** + * @title IBL Baker + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*CbqvQpg1l0QAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BackgroundMode, + Camera, + CullMode, + DiffuseMode, + Entity, + Logger, + Material, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + Shader, + SkyBoxMaterial, + SphericalHarmonics3, + Texture2D, + TextureCube, + TextureCubeFace, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { BakerResolution, IBLBaker, SphericalHarmonics3Baker } from "@galacean/tools-baker"; +import * as dat from "dat.gui"; +Logger.enable(); + +const gui = new dat.GUI(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const { ambientLight } = scene; + const rootEntity = scene.createRootEntity(); + const groupEntity = rootEntity.createChild("group"); + const sky = scene.background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + scene.background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + // Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.position = new Vector3(0, 0, 10); + const camera = cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + Promise.all([ + engine.resourceManager.load({ + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5bs-Sb80qcUAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*rLUCT4VPBeEAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*LjSHTI5iSPoAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*pgCvTJ85RUYAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*0BKxR6jgRDAAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Pir4RoxLm3EAAAAAAAAAAAAAARQnAQ" + ], + type: AssetType.TextureCube + }), + engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/10c5d68d-8580-4bd9-8795-6f1035782b94.bin", // sunset_1K + type: AssetType.HDR + }) + ]).then((textures: TextureCube[]) => { + const ldrCubeMap = textures[0]; + const hdrCubeMap = textures[1]; + skyMaterial.texture = hdrCubeMap; + skyMaterial.textureDecodeRGBM = true; + + engine.run(); + + gui.add(skyMaterial, "rotation", 0, 360, 1); + gui.add(skyMaterial, "exposure", 0, 10, 0.1); + + debugIBL(ldrCubeMap, hdrCubeMap); + + function debugIBL(ldrCubeMap: TextureCube, hdrCubeMap: TextureCube) { + Shader.create( + "ibl debug test", + ` + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + + uniform mat4 renderer_MVPMat; + varying vec2 v_uv; + + void main(){ + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_uv = TEXCOORD_0; + } + `, + ` + uniform sampler2D u_env; + uniform int u_face; + varying vec2 v_uv; + + vec4 RGBMToLinear( in vec4 value, in float maxRange ) { + return vec4( value.rgb * value.a * maxRange, 1.0 ); + } + + + void main(){ + vec2 uv = v_uv; + if(u_face == 2){ + uv.x = v_uv.y; + uv.y= 1.0 - v_uv.x; + }else if(u_face == 3){ + uv.x = 1.0 - v_uv.y; + uv.y= v_uv.x; + } + + gl_FragColor = RGBMToLinear(texture2D(u_env, uv), 5.0); + + gl_FragColor.rgb = pow(gl_FragColor.rgb, vec3(1.0 / 2.2)); + } + ` + ); + + let debugTexture: TextureCube; + const size = hdrCubeMap.width; + + // Create Sphere + const sphereEntity = groupEntity.createChild("box"); + sphereEntity.transform.setPosition(-1, 2, 0); + const sphereMaterial = new PBRMaterial(engine); + sphereMaterial.roughness = 0; + sphereMaterial.metallic = 1; + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 1, 64); + renderer.setMaterial(sphereMaterial); + + // Create planes + const planes = new Array(6); + const planeMaterials = new Array(6); + + for (let i = 0; i < 6; i++) { + const test = groupEntity.createChild(i + ""); + // const bakerEntity = rootEntity.createChild("IBL Baker Entity"); + const bakerEntity = test; + bakerEntity.transform.setRotation(90, 0, 0); + const bakerMaterial = new Material(engine, Shader.find("ibl debug test")); + bakerMaterial.renderState.rasterState.cullMode = CullMode.Off; + const bakerRenderer = bakerEntity.addComponent(MeshRenderer); + bakerRenderer.mesh = PrimitiveMesh.createPlane(engine, 2, 2); + bakerRenderer.setMaterial(bakerMaterial); + planes[i] = bakerEntity; + planeMaterials[i] = bakerMaterial; + } + + planes[0].transform.setPosition(1, 0, 0); // PX + planes[1].transform.setPosition(-3, 0, 0); // NX + planes[2].transform.setPosition(1, 2, 0); // PY + planes[3].transform.setPosition(1, -2, 0); // NY + planes[4].transform.setPosition(-1, 0, 0); // PZ + planes[5].transform.setPosition(3, 0, 0); // NZ + + //debug + gui.add(sphereMaterial, "metallic", 0, 1, 0.01); + gui.add(sphereMaterial, "roughness", 0, 1, 0.01); + + function changeMip(mipLevel: number) { + const mipSize = size >> mipLevel; + for (let i = 0; i < 6; i++) { + const material = planeMaterials[i]; + const data = new Uint8Array(mipSize * mipSize * 4); + const planeTexture = new Texture2D(engine, mipSize, mipSize, undefined, false); // no mipmap + debugTexture.getPixelBuffer(TextureCubeFace.PositiveX + i, 0, 0, mipSize, mipSize, mipLevel, data); + planeTexture.setPixelBuffer(data); + material.shaderData.setTexture("u_env", planeTexture); + material.shaderData.setInt("u_face", i); + } + } + + const state = { + mipLevel: 0, + HDR: true, + bake: () => { + const specularTime = performance.now(); + const awaitTime = performance.now(); + + const bakedTexture = IBLBaker.fromScene(scene, BakerResolution.R256); + ambientLight.specularTexture = bakedTexture; + ambientLight.specularTextureDecodeRGBM = true; + + console.log(`%c specularTime:${performance.now() - specularTime}`, "color:yellow;"); + const sh = new SphericalHarmonics3(); + const shTime = performance.now(); + SphericalHarmonics3Baker.fromTextureCube(bakedTexture, sh).then((sh) => { + ambientLight.diffuseMode = DiffuseMode.SphericalHarmonics; + ambientLight.diffuseSphericalHarmonics = sh; + console.log(`%c SH time:${performance.now() - shTime}`, "color:yellow;"); + }); + + // SphericalHarmonics3Baker.fromTextureCubeMap(bakedTexture, sh); + // ambientLight.diffuseMode = DiffuseMode.SphericalHarmonics; + // ambientLight.diffuseSphericalHarmonics = sh; + // console.log(`%c SH time:${performance.now() - shTime}`, "color:yellow;"); + + console.log(`%c 堵塞时间:${performance.now() - awaitTime}`, "color:red;"); + debugTexture = bakedTexture; + changeMip(state.mipLevel); + } + }; + + gui.add(state, "mipLevel", 0, hdrCubeMap.mipmapCount - 1, 1).onChange((mipLevel: number) => { + changeMip(mipLevel); + }); + + gui.add(state, "HDR").onChange((v) => { + if (v) { + skyMaterial.texture = hdrCubeMap; + skyMaterial.textureDecodeRGBM = true; + } else { + skyMaterial.texture = ldrCubeMap; + skyMaterial.textureDecodeRGBM = false; + } + }); + + gui.add(state, "bake").name("点我烘焙"); + + // bake first + state.bake(); + } + }); +}); diff --git a/examples/infinity-grid.ts b/examples/infinity-grid.ts new file mode 100644 index 000000000..e4c4542b6 --- /dev/null +++ b/examples/infinity-grid.ts @@ -0,0 +1,39 @@ +/** + * @title Infinity Grid + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*MtwTQrwoxnsAAAAAAAAAAAAADiR2AQ/original + */ + +import { Camera, GLTFResource, WebGLEngine, Vector3 } from "@galacean/engine"; +import { OrbitControl, GridControl } from "@galacean/engine-toolkit"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.sceneManager.activeScene.ambientLight.diffuseSolidColor.set( + 1, + 1, + 1, + 1 + ); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(6, 6, 6); + cameraEntity.transform.lookAt(new Vector3()); + cameraEntity.addComponent(OrbitControl); + + const grid = rootEntity.addComponent(GridControl); + grid.camera = camera; + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf" + ) + .then((gltf) => { + rootEntity.addChild(gltf.defaultSceneRoot); + }); + + engine.run(); +}); diff --git a/examples/input-glTF.ts b/examples/input-glTF.ts new file mode 100644 index 000000000..db6d5af33 --- /dev/null +++ b/examples/input-glTF.ts @@ -0,0 +1,89 @@ +/** + * @title glTF Pointer + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*_bc8Tp6t_7UAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + BoxColliderShape, + Camera, + DirectLight, + GLTFResource, + MeshRenderer, + Script, + StaticCollider, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; + +class GlTFCollider extends Script { + private _tempVec30: Vector3 = new Vector3(); + private _tempVec31: Vector3 = new Vector3(); + + onStart(): void { + const renderers = this.entity.getComponentsIncludeChildren( + MeshRenderer, + [] + ); + for (let i = renderers.length - 1; i >= 0; i--) { + this._addBoundingBox(renderers[i]); + } + } + + private _addBoundingBox(renderer: MeshRenderer): void { + const { _tempVec30: localSize, _tempVec31: localPosition } = this; + // Calculate the position and size of the collider. + const boundingBox = renderer.mesh.bounds; + const entity = renderer.entity; + boundingBox.getCenter(localPosition); + Vector3.subtract(boundingBox.max, boundingBox.min, localSize); + // Add collider. + const boxCollider = entity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.position.set( + localPosition.x, + localPosition.y, + localPosition.z + ); + boxColliderShape.size.set(localSize.x, localSize.y, localSize.z); + boxCollider.addShape(boxColliderShape); + // Add click script. + entity.addComponent(Script).onPointerClick = () => { + window.alert("Click:" + entity.name); + }; + } +} + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const directLightNode = rootEntity.createChild("dir_light"); + directLightNode.addComponent(DirectLight); + directLightNode.transform.setRotation(30, 0, 0); + + //Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 0, 10); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/48a1e8b3-06b4-4269-807d-79274e58283a.glb" + ) + .then((glTF) => { + const glTFRoot = glTF.defaultSceneRoot; + const entity = rootEntity.createChild("glTF"); + entity.addChild(glTFRoot); + glTFRoot.transform.setScale(0.005, 0.005, 0.005); + glTFRoot.addComponent(GlTFCollider); + engine.run(); + }); + } +); diff --git a/examples/input-glTFMerge.ts b/examples/input-glTFMerge.ts new file mode 100644 index 000000000..217defea1 --- /dev/null +++ b/examples/input-glTFMerge.ts @@ -0,0 +1,81 @@ +/** + * @title glTF Pointer Merge + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*pxZZSpoiVKQAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + BoundingBox, + BoxColliderShape, + Camera, + DirectLight, + GLTFResource, + Matrix, + MeshRenderer, + Script, + StaticCollider, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; + +class GlTFCollider extends Script { + onStart(): void { + const { entity } = this; + const renderers = entity.getComponentsIncludeChildren(MeshRenderer, []); + const boundingBox = renderers[0].bounds.clone(); + for (let i = renderers.length - 1; i > 0; i--) { + BoundingBox.merge(boundingBox, renderers[i].bounds, boundingBox); + } + const worldPosition = new Vector3(); + const worldSize = new Vector3(); + const worldMatrix = new Matrix(); + // Calculate the position and size of the collider. + boundingBox.getCenter(worldPosition); + Vector3.subtract(boundingBox.max, boundingBox.min, worldSize); + // Add entity and calculate the world matrix of the collider. + const boxEntity = entity.createChild("box"); + boxEntity.transform.worldMatrix = worldMatrix.translate(worldPosition); + // Add collider. + const boxCollider = boxEntity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(worldSize.x, worldSize.y, worldSize.z); + boxCollider.addShape(boxColliderShape); + // Add click script. + boxEntity.addComponent(Script).onPointerClick = () => { + window.alert("click glTF!"); + }; + } +} + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const directLightNode = rootEntity.createChild("dir_light"); + directLightNode.addComponent(DirectLight); + directLightNode.transform.setRotation(30, 0, 0); + + //Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 0, 10); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/48a1e8b3-06b4-4269-807d-79274e58283a.glb" + ) + .then((glTF) => { + const glTFRoot = glTF.defaultSceneRoot; + glTFRoot.transform.setScale(0.005, 0.005, 0.005); + glTFRoot.addComponent(GlTFCollider); + rootEntity.addChild(glTFRoot); + engine.run(); + }); + } +); diff --git a/examples/input-log.ts b/examples/input-log.ts new file mode 100644 index 000000000..18d6c528b --- /dev/null +++ b/examples/input-log.ts @@ -0,0 +1,57 @@ +/** + * @title Input Logger + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*GX8yQIgHyUYAAAAAAAAAAAAADiR2AQ/original + */ +import { WebGLEngine } from "@galacean/engine"; +import * as dat from "dat.gui"; +import { InputLogger } from "@galacean/engine-toolkit-input-logger"; + +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.canvas._webCanvas.style.touchAction = "none"; + const inputLogger = new InputLogger(engine); + engine.run(); + + // Debug + const debugInfo = { + Pointer: true, + Keyboard: true, + Size: 1, + Color: [255, 255, 255], + OffsetX: 0, + OffsetY: 0, + }; + + gui.add(debugInfo, "Pointer").onChange((v: boolean) => { + inputLogger.showPointer = v; + }); + + gui.add(debugInfo, "Keyboard").onChange((v: boolean) => { + inputLogger.showKeyBoard = v; + }); + + const textFolder = gui.addFolder("Info"); + textFolder.add(debugInfo, "OffsetX", 0, 1, 0.02).onChange((v: number) => { + inputLogger.offset.x = v; + inputLogger.offset = inputLogger.offset; + }); + + textFolder.add(debugInfo, "OffsetY", 0, 1, 0.02).onChange((v: number) => { + inputLogger.offset.y = v; + inputLogger.offset = inputLogger.offset; + }); + + textFolder.add(debugInfo, "Size", 0.5, 2, 0.1).onChange((v: number) => { + inputLogger.scale = v; + }); + + textFolder.addColor(debugInfo, "Color").onChange((v: number) => { + inputLogger.color.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + + textFolder.open(); +}); diff --git a/examples/input-pointer.ts b/examples/input-pointer.ts new file mode 100644 index 000000000..fd366d388 --- /dev/null +++ b/examples/input-pointer.ts @@ -0,0 +1,165 @@ +/** + * @title input-pointer + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*t7xZRok9NpEAAAAAAAAAAAAADiR2AQ/original + */ +import { + BlinnPhongMaterial, + BoxColliderShape, + Camera, + MeshRenderer, + PointLight, + PrimitiveMesh, + Script, + StaticCollider, + Vector2, + Pointer, + Entity, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const invCanvasWidth = 1 / engine.canvas.width; + const invCanvasHeight = 1 / engine.canvas.height; + // @ts-ignore + const inputManager = engine.inputManager; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // init point light + let light = rootEntity.createChild("light1"); + light.transform.setPosition(-8, -2, 8); + light.addComponent(PointLight).intensity = 0.12; + + light = rootEntity.createChild("light2"); + light.transform.setPosition(8, -2, 0); + light.addComponent(PointLight).intensity = 0.12; + + class PanScript extends Script { + private startPointerPos = new Vector3(); + private tempVec2: Vector2 = new Vector2(); + private tempVec3: Vector3 = new Vector3(); + private zValue: number = 0; + + onPointerDown(pointer: Pointer) { + this.zValue = camera.worldToViewportPoint( + this.entity.transform.worldPosition, + this.tempVec3 + ).z; + const { tempVec2, tempVec3 } = this; + tempVec2.copyFrom(pointer.position); + tempVec3.set( + tempVec2.x * invCanvasWidth, + tempVec2.y * invCanvasHeight, + this.zValue + ); + camera.viewportToWorldPoint(tempVec3, this.startPointerPos); + } + + onPointerDrag(pointer: Pointer) { + const { tempVec2, tempVec3, startPointerPos } = this; + const { transform } = this.entity; + tempVec2.copyFrom(pointer.position); + tempVec3.set( + tempVec2.x * invCanvasWidth, + tempVec2.y * invCanvasHeight, + this.zValue + ); + camera.viewportToWorldPoint(tempVec3, tempVec3); + Vector3.subtract(tempVec3, startPointerPos, startPointerPos); + transform.worldPosition.add(startPointerPos); + startPointerPos.copyFrom(tempVec3); + } + } + + class ClickScript extends Script { + private material: BlinnPhongMaterial; + onStart() { + this.material = ( + this.entity.getComponent(MeshRenderer).getInstanceMaterial() + ); + } + + onPointerClick() { + this.material.baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + } + + class EnterExitScript extends Script { + private material: BlinnPhongMaterial; + onStart() { + this.material = ( + this.entity.getComponent(MeshRenderer).getInstanceMaterial() + ); + } + + onPointerEnter() { + this.material.baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onPointerExit() { + this.material.baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + } + + function createBox(x: number, y: number, z: number): Entity { + // create box test entity + const cubeSize = 2.0; + const boxEntity = rootEntity.createChild("BoxEntity"); + boxEntity.transform.setPosition(x, y, z); + + const boxMtl = new BlinnPhongMaterial(engine); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxMtl.baseColor.set(0.6, 0.3, 0.3, 1.0); + boxRenderer.mesh = PrimitiveMesh.createCuboid( + engine, + cubeSize, + cubeSize, + cubeSize + ); + boxRenderer.setMaterial(boxMtl); + + const boxCollider: StaticCollider = + boxEntity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(cubeSize, cubeSize, cubeSize); + boxCollider.addShape(boxColliderShape); + return boxEntity; + } + createBox(0, 0, 0).addComponent(PanScript); + createBox(3, 0, -3).addComponent(ClickScript); + createBox(-3, 0, 3).addComponent(EnterExitScript); + + // Run engine + engine.run(); + } +); diff --git a/examples/input-pointerButton.ts b/examples/input-pointerButton.ts new file mode 100644 index 000000000..e76b0d1f5 --- /dev/null +++ b/examples/input-pointerButton.ts @@ -0,0 +1,71 @@ +/** + * @title input-pointerButton + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*ytliRpOHgvgAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + DirectLight, + MeshRenderer, + PBRMaterial, + PointerButton, + PrimitiveMesh, + Script, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // add light + const lightEntity = rootEntity.createChild("light"); + lightEntity.addComponent(DirectLight); + lightEntity.transform.setRotation(-45, 0, 0); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // init box + const boxEntity = rootEntity.createChild("box"); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + renderer.setMaterial(new PBRMaterial(engine)); + boxEntity.addComponent(RotateScript); + + // add tip + const textEntity = rootEntity.createChild("text"); + textEntity.transform.rotationQuaternion = + cameraEntity.transform.rotationQuaternion; + textEntity.transform.setPosition(0, 5, 0); + textEntity.transform.setScale(2, 2, 2); + const textRenderer = textEntity.addComponent(TextRenderer); + textRenderer.fontSize = 40; + textRenderer.text = + "Hold down the 'Primary' to rotate left\nHold down the 'Secondary' to rotate right\n"; + + // Run engine + engine.run(); + } +); + +class RotateScript extends Script { + onUpdate(deltaTime: number): void { + const { engine, entity } = this; + if (engine.inputManager.isPointerHeldDown(PointerButton.Primary)) { + entity.transform.rotate(0, -1, 0); + } + if (engine.inputManager.isPointerHeldDown(PointerButton.Secondary)) { + entity.transform.rotate(0, 1, 0); + } + } +} diff --git a/examples/input-pointerRaycast.ts b/examples/input-pointerRaycast.ts new file mode 100644 index 000000000..d62d1c96f --- /dev/null +++ b/examples/input-pointerRaycast.ts @@ -0,0 +1,193 @@ +/** + * @title input-pointerRaycast + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*unTBRblIIkUAAAAAAAAAAAAADiR2AQ/original + */ +import { + BlinnPhongMaterial, + BoxColliderShape, + Camera, + Color, + DirectLight, + Entity, + HitResult, + Layer, + MeshRenderer, + PrimitiveMesh, + Ray, + Script, + StaticCollider, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { LitePhysics } from "@galacean/engine-physics-lite"; +import { WireframeManager } from "@galacean/engine-toolkit"; + +// Create engine +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + engine.canvas._webCanvas.style.touchAction = "none"; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // add light + const lightEntity = rootEntity.createChild("light"); + lightEntity.addComponent(DirectLight); + lightEntity.transform.setRotation(-45, 0, 0); + + // init main camera + const mainCameraEntity = rootEntity.createChild("camera"); + const mainCamera = mainCameraEntity.addComponent(Camera); + mainCameraEntity.transform.setPosition(0, 0, 20); + mainCameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + mainCamera.cullingMask = Layer.Layer0 | Layer.Layer1; + mainCamera.fieldOfView = 35; + mainCamera.nearClipPlane = 1; + mainCamera.farClipPlane = 30; + // add wire frame + rootEntity.addComponent(MeshRenderer); + rootEntity.addComponent(WireframeManager).addCameraWireframe(mainCamera); + + // init side camera + const sideCameraEntity = rootEntity.createChild("sideCamera"); + const sideCamera = sideCameraEntity.addComponent(Camera); + sideCamera.priority = 1; + sideCamera.viewport.set(0, 0.6, 0.4, 0.4); + sideCameraEntity.transform.setPosition(-45, 0, 0); + sideCameraEntity.transform.rotation.set(0, -90, 0); + sideCamera.cullingMask = Layer.Layer0 | Layer.Layer2; + sideCameraEntity.addComponent( + class extends Script { + onBeginRender(camera: Camera): void { + scene.background.solidColor.set(0, 0, 0, 1); + } + onEndRender(camera: Camera): void { + scene.background.solidColor.set(0.25, 0.25, 0.25, 1.0); + } + } + ); + + for (let i = 0; i < 30; i++) { + createRandomBox(rootEntity); + } + + rootEntity.addComponent( + class extends Script { + private _entities: Entity[] = []; + private _tempRay: Ray = new Ray(); + private _tempVec3: Vector3 = new Vector3(); + private _hitResult: HitResult = new HitResult(); + + onUpdate(deltaTime: number): void { + const { pointers } = engine.inputManager; + const { + _tempRay: tempRay, + _entities: entities, + _hitResult: hitResult, + } = this; + for (let i = 0, n = entities.length; i < n; i++) { + entities[i].isActive = false; + } + for (let i = 0, n = pointers.length; i < n; i++) { + mainCamera.screenPointToRay(pointers[i].position, tempRay); + if (scene.physics.raycast(tempRay, 100, hitResult)) { + ( + hitResult.entity.getComponent(BoxScript) as BoxScript + ).hitFrameCount = engine.time.frameCount; + } + this._adjustByRay(this._getOrCreateRayEntity(i), tempRay); + } + } + + private _getOrCreateRayEntity(index: number) { + const { _entities: entities } = this; + let entity = entities[index]; + if (!entity) { + entity = entities[index] = this.entity.createChild(`ray${index}`); + const ray = entity.createChild(); + ray.layer = Layer.Layer2; + const rayRenderer = ray.addComponent(MeshRenderer); + rayRenderer.mesh = PrimitiveMesh.createCylinder( + engine, + 0.05, + 0.05, + 40 + ); + ray.transform.position = new Vector3(0, 0, -20); + ray.transform.rotation = new Vector3(-90, 0, 0); + const material = new UnlitMaterial(engine); + material.baseColor = new Color(0, 1, 0); + rayRenderer.setMaterial(material); + const ball = entity.createChild(); + ball.layer = Layer.Layer1; + const ballRenderer = ball.addComponent(MeshRenderer); + ballRenderer.mesh = PrimitiveMesh.createSphere(engine, 0.008); + ballRenderer.setMaterial(material); + } else { + entity.isActive = true; + } + return entities[index]; + } + + private _adjustByRay(rayEntity: Entity, ray: Ray) { + const { _tempVec3: tempVec3 } = this; + const { origin, direction } = ray; + Vector3.scale(direction, 0.5, tempVec3); + Vector3.add(origin, tempVec3, rayEntity.transform.position); + Vector3.add(origin, direction, tempVec3); + rayEntity.transform.lookAt(tempVec3); + } + } + ); + + // Run engine + engine.run(); + } +); + +function createRandomBox(root: Entity) { + const { engine } = root; + const boxEntity = root.createChild("box"); + boxEntity.transform.setPosition( + 5 - Math.random() * 10, + 5 - Math.random() * 10, + 8 - Math.random() * 16 + ); + + const renderer = boxEntity.addComponent(MeshRenderer); + const width = Math.random() * 1.5 + 0.5; + const height = Math.random() * 1.5 + 0.5; + const depth = Math.random() * 1.5 + 0.5; + renderer.mesh = PrimitiveMesh.createCuboid(engine, width, height, depth); + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 1, 1); + renderer.setMaterial(material); + + const collider = boxEntity.addComponent(StaticCollider); + const shape = new BoxColliderShape(); + shape.size.set(width, height, depth); + collider.addShape(shape); + + boxEntity.addComponent(BoxScript); +} + +class BoxScript extends Script { + hitFrameCount: number = 0; + private _material: BlinnPhongMaterial; + + onStart(): void { + this._material = ( + this.entity.getComponent(MeshRenderer) as MeshRenderer + ).getMaterial() as BlinnPhongMaterial; + } + + onLateUpdate(deltaTime: number): void { + if (this.engine.time.frameCount === this.hitFrameCount) { + this._material.baseColor = new Color(1, 0, 0); + } else { + this._material.baseColor = new Color(1, 1, 1); + } + } +} diff --git a/examples/input-wheel.ts b/examples/input-wheel.ts new file mode 100644 index 000000000..c3b990b73 --- /dev/null +++ b/examples/input-wheel.ts @@ -0,0 +1,91 @@ +/** + * @title input-wheel + * @category input + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Twx0TY-OTjIAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + DirectLight, + MathUtil, + MeshRenderer, + PrimitiveMesh, + Script, + TextRenderer, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { WireframeManager } from "@galacean/engine-toolkit"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.canvas._webCanvas.style.touchAction = "none"; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // init main camera + const mainCameraEntity = rootEntity.createChild("camera"); + const mainCamera = mainCameraEntity.addComponent(Camera); + mainCameraEntity.transform.setPosition(0, 0, 20); + mainCamera.fieldOfView = 30; + mainCamera.nearClipPlane = 3; + mainCamera.farClipPlane = 35; + // add wire frame + rootEntity.addComponent(MeshRenderer); + rootEntity.addComponent(WireframeManager).addCameraWireframe(mainCamera); + + const boxEntity = rootEntity.createChild("box"); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + renderer.setMaterial(new UnlitMaterial(engine)); + boxEntity.addComponent( + class extends Script { + onUpdate(deltaTime: number): void { + const rad = engine.time.actualElapsedTime; + boxEntity.transform.rotate(Math.sin(rad), Math.cos(rad), 0); + } + } + ); + + // init side camera + const sideCameraEntity = rootEntity.createChild("sideCamera"); + const sideCamera = sideCameraEntity.addComponent(Camera); + sideCamera.priority = 1; + sideCamera.viewport.set(0, 0.6, 0.4, 0.4); + sideCameraEntity.transform.setPosition(-50, 0, -5); + sideCameraEntity.transform.setRotation(0, -90, 0); + sideCameraEntity.addComponent( + class extends Script { + onBeginRender(camera: Camera): void { + scene.background.solidColor.set(0, 0, 0, 1); + } + onEndRender(camera: Camera): void { + scene.background.solidColor.set(0.25, 0.25, 0.25, 1.0); + } + } + ); + + // add tip + const textEntity = rootEntity.createChild("text"); + textEntity.transform.setPosition(0, 3, 0); + textEntity.transform.setScale(2, 2, 2); + const textRenderer = textEntity.addComponent(TextRenderer); + textRenderer.fontSize = 30; + textRenderer.text = "Use the wheel to control the distance of the camera"; + + rootEntity.addComponent( + class extends Script { + onUpdate(deltaTime: number): void { + const { wheelDelta } = engine.inputManager; + if (wheelDelta) { + const { position } = mainCameraEntity.transform; + position.z = MathUtil.clamp(position.z - wheelDelta.y / 100, 0, 40); + } + } + } + ); + + // Run engine + engine.run(); +}); diff --git a/examples/light-type.ts b/examples/light-type.ts new file mode 100644 index 000000000..87a3626d1 --- /dev/null +++ b/examples/light-type.ts @@ -0,0 +1,131 @@ +/** + * @title Light Type + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*z9MpQagp8WEAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + BlinnPhongMaterial, + Camera, + DirectLight, + MeshRenderer, + PointLight, + PrimitiveMesh, + RenderFace, + SpotLight, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create Ground + const groundEntity = rootEntity.createChild("ground"); + const groundRenderer = groundEntity.addComponent(MeshRenderer); + const mesh = PrimitiveMesh.createPlane(engine, 100, 100); + const material = new BlinnPhongMaterial(engine); + + material.renderFace = RenderFace.Double; + groundRenderer.mesh = mesh; + groundRenderer.setMaterial(material); + + // Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 30, 50); + const camera = cameraNode.addComponent(Camera); + camera.farClipPlane = 200; + + const control = cameraNode.addComponent(OrbitControl); + control.maxDistance = 100; + + // Create spot light + const lightEntity = rootEntity.createChild("light"); + const spotLight = lightEntity.addComponent(SpotLight); + const pointLight = lightEntity.addComponent(PointLight); + const directionalLight = lightEntity.addComponent(DirectLight); + const target = new Vector3(0, 0, 0); + const up = new Vector3(0, 1, 0); + + let light: any = spotLight; + pointLight.enabled = false; + directionalLight.enabled = false; + + lightEntity.transform.setPosition(20, 20, 0); + lightEntity.transform.lookAt(target, up); + + const lightRenderer = lightEntity.addComponent(MeshRenderer); + lightRenderer.mesh = PrimitiveMesh.createSphere(engine, 1); + lightRenderer.setMaterial(new UnlitMaterial(engine)); + + // Debug + const debugInfo = { + type: "SpotLight", + angle: 30, + penumbra: 15, + x: 20, + y: 20, + z: 0, + }; + + gui + .add(debugInfo, "type", ["DirectionalLight", "PointLight", "SpotLight"]) + .onChange((v) => { + light.enabled = false; + spotFolder.closed = true; + switch (v) { + case "SpotLight": + light = spotLight; + light.enabled = true; + spotFolder.closed = false; + break; + case "DirectionalLight": + light = directionalLight; + light.enabled = true; + break; + case "PointLight": + light = pointLight; + light.enabled = true; + break; + } + }); + gui.add(light, "distance", 0, 100, 1); + + const folder = gui.addFolder("位置"); + folder.open(); + folder.add(debugInfo, "x", -100, 100).onChange((v) => { + const last = lightEntity.transform.position; + lightEntity.transform.setPosition(v, last.y, last.z); + lightEntity.transform.lookAt(target, up); + }); + folder.add(debugInfo, "y", 0, 100).onChange((v) => { + const last = lightEntity.transform.position; + lightEntity.transform.setPosition(last.x, v, last.z); + lightEntity.transform.lookAt(target, up); + }); + folder.add(debugInfo, "z", -100, 100).onChange((v) => { + const last = lightEntity.transform.position; + lightEntity.transform.setPosition(last.x, last.y, v); + lightEntity.transform.lookAt(target, up); + }); + + const spotFolder = gui.addFolder("SpotLight"); + spotFolder.open(); + + spotFolder.add(debugInfo, "angle", 1, 90, 1).onChange((v) => { + light.angle = (v * Math.PI) / 180; + }); + spotFolder.add(debugInfo, "penumbra", 1, 90, 1).onChange((v) => { + light.penumbra = (v * Math.PI) / 180; + }); + + // Run + engine.run(); +}); diff --git a/examples/lines.ts b/examples/lines.ts new file mode 100644 index 000000000..6e7475e4c --- /dev/null +++ b/examples/lines.ts @@ -0,0 +1,106 @@ +/** + * @title Lines + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*K4P2TK5c3IoAAAAAAAAAAAAADiR2AQ/original + */ +import { Camera, Color, Vector2, Vector3, WebGLEngine } from "@galacean/engine"; +import { + Line, + DashLine, + LineCap, + LineJoin, +} from "@galacean/engine-toolkit-lines"; +import * as dat from "dat.gui"; + +const lines: Line[] = []; +const controls = { + width: 0.02, + cap: LineCap.Round, + join: LineJoin.Round, +}; +const colors = [ + new Color(91 / 255, 143 / 255, 249 / 255), + new Color(92 / 255, 206 / 255, 161 / 255), + new Color(246 / 255, 189 / 255, 22 / 255), + new Color(170 / 255, 0 / 255, 97 / 255), + new Color(0 / 255, 155 / 255, 119 / 255), +]; +// Create engine and get root entity +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity("Root"); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + const center = new Vector3(0, 0, 0); + cameraEntity.transform.setPosition(center.x, center.y, 50); + cameraEntity.transform.lookAt(center); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + engine.sceneManager.activeScene.background.solidColor.set(0.8, 0.8, 0.8, 1); + engine.run(); + + fetch( + "https://gw.alipayobjects.com/os/basement_prod/0d2f0113-f48b-4db9-8adc-a3937243d5a3.json" + ) + .then((res) => res.json()) + .then((data) => { + const lineEntity = rootEntity.createChild("Line"); + // lineEntity.transform.translate(new Vector3(-15, 0, 0)) + // lineEntity.transform.setScale(30, 30, 1); + for (let index = 0; index < data.features.length; index++) { + const geometry = data.features[index].geometry; + let line; + if (index % 2 === 0) { + line = lineEntity.addComponent(Line); + } else { + line = lineEntity.addComponent(DashLine); + line.dash = new Vector2(0.2, 0.1); + } + line.cap = controls.cap; + line.join = controls.join; + line.width = controls.width; + line.color = colors[index % colors.length]; + line.points = geometry.coordinates[0].map((p) => { + return { x: (p[0] - 116.4) * 30, y: (p[1] - 39.9) * 30 }; + }); + + lines.push(line); + } + }); + + showDebug(); + + function showDebug() { + const gui = new dat.GUI({ + name: "line", + }); + + const widthControl = gui.add(controls, "width", 0.01, 0.1); + const capControl = gui.add(controls, "cap", { + butt: LineCap.Butt, + round: LineCap.Round, + square: LineCap.Square, + }); + const joinControl = gui.add(controls, "join", { + bevel: LineJoin.Bevel, + round: LineJoin.Round, + miter: LineJoin.Miter, + }); + + widthControl.onChange(change); + capControl.onChange(change); + joinControl.onChange(change); + + function change() { + if (lines.length) { + lines.forEach((line) => { + line.cap = controls.cap; + line.join = Number(controls.join); + line.width = Number(controls.width); + }); + } + } + } +}); diff --git a/examples/lite-collision-detection.ts b/examples/lite-collision-detection.ts new file mode 100644 index 000000000..1fe42248d --- /dev/null +++ b/examples/lite-collision-detection.ts @@ -0,0 +1,138 @@ +/** + * @title Lite Collision Detection + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*wqmuRKAr-Z0AAAAAAAAAAAAADiR2AQ/original + */ +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + DynamicCollider, + MeshRenderer, + PBRMaterial, + PointLight, + PrimitiveMesh, + Script, + SphereColliderShape, + StaticCollider, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +import { LitePhysics } from "@galacean/engine-physics-lite"; + +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.addComponent(OrbitControl); + + // init point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 3, 0); + light.addComponent(PointLight); + + // create box test entity + const cubeSize = 2.0; + const boxEntity = rootEntity.createChild("BoxEntity"); + + const boxMtl = new PBRMaterial(engine); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxMtl.baseColor.set(0.6, 0.3, 0.3, 1.0); + boxMtl.metallic = 0.0; + boxMtl.roughness = 0.5; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + engine, + cubeSize, + cubeSize, + cubeSize + ); + boxRenderer.setMaterial(boxMtl); + + const boxCollider = boxEntity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(cubeSize, cubeSize, cubeSize); + boxCollider.addShape(boxColliderShape); + + // create sphere test entity + const radius = 1.25; + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(-5, 0, 0); + + const sphereMtl = new PBRMaterial(engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + sphereMtl.metallic = 0.0; + sphereMtl.roughness = 0.5; + sphereRenderer.mesh = PrimitiveMesh.createSphere(engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + const sphereColliderShape = new SphereColliderShape(); + sphereColliderShape.radius = radius; + sphereCollider.addShape(sphereColliderShape); + + class MoveScript extends Script { + pos: number = -5; + vel: number = 0.05; + velSign: number = -1; + + onPhysicsUpdate() { + if (this.pos >= 5) { + this.velSign = -1; + } + if (this.pos <= -5) { + this.velSign = 1; + } + this.pos += this.vel * this.velSign; + this.entity.transform.worldPosition.set(this.pos, 0, 0); + } + } + + // Collision Detection + class CollisionScript extends Script { + onTriggerExit() { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onTriggerStay() {} + + onTriggerEnter() { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + } + + sphereEntity.addComponent(CollisionScript); + sphereEntity.addComponent(MoveScript); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); + } +); diff --git a/examples/lite-raycast.ts b/examples/lite-raycast.ts new file mode 100644 index 000000000..d0e327dc4 --- /dev/null +++ b/examples/lite-raycast.ts @@ -0,0 +1,160 @@ +/** + * @title Lite Raycast + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*3jekSqyUAFgAAAAAAAAAAAAADiR2AQ/original + */ +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + Color, + Font, + HitResult, + Layer, + MeshRenderer, + PBRMaterial, + PointerPhase, + PointLight, + PrimitiveMesh, + Ray, + Script, + SphereColliderShape, + StaticCollider, + TextRenderer, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +import { LitePhysics } from "@galacean/engine-physics-lite"; + +class Raycast extends Script { + camera: Camera; + originalColor: Color = new Color(); + point = new Vector2(); + ray = new Ray(); + hit = new HitResult(); + + onAwake() { + this.camera = this.entity.getComponent(Camera); + } + + onUpdate(deltaTime: number) { + const { engine, ray, hit, originalColor } = this; + const { inputManager } = engine; + const { pointers } = inputManager; + if (!pointers) { + return; + } + for (let i = pointers.length - 1; i >= 0; i--) { + const pointer = pointers[i]; + this.camera.screenPointToRay(pointer.position, ray); + const result = engine.physicsManager.raycast( + ray, + Number.MAX_VALUE, + Layer.Everything, + hit + ); + if (result) { + const pickedMeshRenderer = hit.entity.getComponent(MeshRenderer); + switch (pointer.phase) { + case PointerPhase.Down: + const material = pickedMeshRenderer.getMaterial(); + originalColor.copyFrom(material.baseColor); + material.baseColor = new Color(0.3, 0.3, 0.3, 1); + break; + case PointerPhase.Up: + case PointerPhase.Leave: + (pickedMeshRenderer.getMaterial()).baseColor = + originalColor; + break; + default: + break; + } + } + } + } +} + +WebGLEngine.create({ canvas: "canvas", physics: new LitePhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.addComponent(OrbitControl); + cameraEntity.addComponent(Raycast); + + const entity = cameraEntity.createChild("text"); + entity.transform.position = new Vector3(0, 3.5, -10); + const renderer = entity.addComponent(TextRenderer); + renderer.color = new Color(); + renderer.text = "Use mouse to click the entity"; + renderer.font = Font.createFromOS(entity.engine, "Arial"); + renderer.fontSize = 40; + + // init point light + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0, 3, 0); + lightEntity.addComponent(PointLight); + + // create sphere test entity + const radius = 1.25; + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(-3, 0, 0); + + const sphereMtl = new PBRMaterial(engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(0.7, 0.1, 0.1, 1.0); + sphereMtl.roughness = 0.5; + sphereMtl.metallic = 0.0; + sphereRenderer.mesh = PrimitiveMesh.createSphere(engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + const sphereCollider = sphereEntity.addComponent(StaticCollider); + const sphereColliderShape = new SphereColliderShape(); + sphereColliderShape.radius = radius; + sphereCollider.addShape(sphereColliderShape); + + // create box test entity + const cubeSize = 2.0; + const boxEntity = rootEntity.createChild("BoxEntity"); + + const boxMtl = new PBRMaterial(engine); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxMtl.baseColor.set(0.1, 0.7, 0.1, 1.0); + boxMtl.roughness = 0.5; + boxMtl.metallic = 0.0; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + engine, + cubeSize, + cubeSize, + cubeSize + ); + boxRenderer.setMaterial(boxMtl); + + const boxCollider = boxEntity.addComponent(StaticCollider); + const boxColliderShape = new BoxColliderShape(); + boxColliderShape.size.set(cubeSize, cubeSize, cubeSize); + boxCollider.addShape(boxColliderShape); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); + } +); diff --git a/examples/lottie-3d-rotation.ts b/examples/lottie-3d-rotation.ts new file mode 100644 index 000000000..39eab66c0 --- /dev/null +++ b/examples/lottie-3d-rotation.ts @@ -0,0 +1,41 @@ +/** + * @title Lottie 3D Rotation + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*zBSbQ6nWtN8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { Camera, Entity, WebGLEngine } from "@galacean/engine"; +import { LottieAnimation } from "@galacean/engine-lottie"; + +async function main() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/bmw-prod/bbf83713-c23f-4981-8b8d-241d905fc3bf.json", + "https://gw.alipayobjects.com/os/bmw-prod/d9b42223-b1ae-4f51-b489-75b2f36a2b2d.atlas", + ], + type: "lottie", + }) + .then((lottieEntity) => { + rootEntity.addChild(lottieEntity); + const lottie = lottieEntity.getComponent(LottieAnimation); + lottie.isLooping = true; + lottieEntity.transform.setScale(0.5, 0.5, 0.5); + lottie.play(); + }); + + engine.run(); +} + +main(); diff --git a/examples/lottie-benchmark.ts b/examples/lottie-benchmark.ts new file mode 100644 index 000000000..abc2fc4f8 --- /dev/null +++ b/examples/lottie-benchmark.ts @@ -0,0 +1,45 @@ +/** + * @title Lottie Benchmark + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*-d70TJt-KaUAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { Camera, Entity, WebGLEngine } from "@galacean/engine"; +import { LottieAnimation } from "@galacean/engine-lottie"; +import { Stats } from "@galacean/engine-toolkit-stats"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const root = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 30); + cameraEntity.addComponent(OrbitControl); + cameraEntity.addComponent(Stats); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/bmw-prod/9ad65a42-9171-47ab-9218-54cf175f6201.json", + "https://gw.alipayobjects.com/os/bmw-prod/58cde292-8675-4299-b400-d98029b48ac7.atlas", + ], + type: "lottie", + }) + .then((lottieEntity) => { + for (let i = -4; i < 5; i++) { + for (let j = -5; j < 6; j++) { + const clone = lottieEntity.clone(); + clone.transform.setPosition(i * 2, j * 2, 0); + root.addChild(clone); + const lottie = clone.getComponent(LottieAnimation); + lottie.isLooping = true; + lottie.speed = 0.2 + Math.random() * 2; + lottie.play(); + } + } + }); + + engine.run(); +}); diff --git a/examples/lottie-clips.ts b/examples/lottie-clips.ts new file mode 100644 index 000000000..450461338 --- /dev/null +++ b/examples/lottie-clips.ts @@ -0,0 +1,41 @@ +/** + * @title Lottie Clips + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*MclFRodjyycAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { Camera, Entity, WebGLEngine } from "@galacean/engine"; +import { LottieAnimation } from "@galacean/engine-lottie"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const root = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/bmw-prod/84c13df1-567c-4a67-aa1e-c378ee698c55.json", + "https://gw.alipayobjects.com/os/bmw-prod/965eb2ca-ee3c-4c54-a502-7fdc0673f1d7.atlas", + ], + type: "lottie", + }) + .then(async (lottieEntity) => { + root.addChild(lottieEntity); + lottieEntity.transform.setPosition(0, -3, 0); + const lottie = lottieEntity.getComponent(LottieAnimation); + + lottie.repeats = 2; + await lottie.play("beforePlay"); + lottie.repeats = 1; + await lottie.play("onPlay"); + lottie.play("afterPlay"); + }); + + engine.run(); +}); diff --git a/examples/lottie.ts b/examples/lottie.ts new file mode 100644 index 000000000..74dde1079 --- /dev/null +++ b/examples/lottie.ts @@ -0,0 +1,36 @@ +/** + * @title Lottie Animation + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*tBbxSq6jdHcAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { Camera, Entity, WebGLEngine } from "@galacean/engine"; +import { LottieAnimation } from "@galacean/engine-lottie"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const root = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = root.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/bmw-prod/b46be138-e48b-4957-8071-7229661aba53.json", + "https://gw.alipayobjects.com/os/bmw-prod/6447fc36-db32-4834-9579-24fe33534f55.atlas", + ], + type: "lottie", + }) + .then((lottieEntity) => { + root.addChild(lottieEntity); + const lottie = lottieEntity.getComponent(LottieAnimation); + lottie.isLooping = true; + lottie.play(); + }); + + engine.run(); +}); diff --git a/examples/material-head-distort.ts b/examples/material-head-distort.ts new file mode 100644 index 000000000..b7511270f --- /dev/null +++ b/examples/material-head-distort.ts @@ -0,0 +1,570 @@ +/** + * @title Material Heat Distortion + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*PIkJQaUO-a4AAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + Animator, + AssetType, + BackgroundMode, + BaseMaterial, + BlendMode, + Burst, + Camera, + Color, + ConeShape, + CurveKey, + DirectLight, + Downsampling, + Engine, + Entity, + GLTFResource, + Logger, + MeshRenderer, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleScaleMode, + ParticleSimulationSpace, + PrimitiveMesh, + Shader, + SkyBoxMaterial, + SphereShape, + Texture2D, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +async function main() { + Logger.enable(); + const engine = await WebGLEngine.create({ + canvas: "canvas", + }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + + // Create sky + const background = scene.background; + background.mode = BackgroundMode.Sky; + + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + const rootEntity = scene.createRootEntity(); + + // Create camera entity + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 1, 4.0); + cameraEntity.transform.lookAt(new Vector3(0, 1, 0)); + // Enable opaque texture and configure downsampling + camera.opaqueTextureEnabled = true; + camera.opaqueTextureDownsampling = Downsampling.TwoX; + + // Create light entity + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0, 0.7, 0.5); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + lightEntity.addComponent(DirectLight); + + // Add ambient light + const ambientLight = await engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/f369110c-0e33-47eb-8296-756e9c80f254.bin", + type: AssetType.Env, + }); + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + + // Add glTF model + const glTFResource = await engine.resourceManager.load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ); + const model = glTFResource.instantiateSceneRoot(); + model.transform.setPosition(0, 0.3, 0); + model.transform.scale = new Vector3(0.012, 0.012, 0.012); + model.getComponent(Animator)!.play("walk", 0); + rootEntity.addChild(model); + + // Add heat distortion + addHeatDistortion(engine, rootEntity); + addFire(engine, rootEntity); + + engine.run(); +} + +main(); + +async function addHeatDistortion(engine: Engine, root: Entity): Promise { + const heatDistortionVS = ` + #include + #include + #include + #include + + varying vec4 v_clipPosition; + + void main() { + #include + #include + #include + #include + #include + + v_clipPosition = gl_Position; + }`; + + const heatDistortionFS = ` + #include + #include + + uniform vec4 scene_ElapsedTime; + + uniform sampler2D camera_OpaqueTexture; + + uniform float material_DistortSpeed; + uniform float material_DistortValue; + uniform float material_FadeRadius; + uniform sampler2D material_NoiseTexture; + + varying vec4 v_clipPosition; + + void main() { + vec2 noiseUV = v_uv + vec2(scene_ElapsedTime.x / 30.0, scene_ElapsedTime.x)* vec2(material_DistortSpeed); + vec4 distortUV = texture2D(material_NoiseTexture, noiseUV); + + float fade = pow(1.0 - length(v_uv - 0.5), material_FadeRadius); + + vec2 viewportUV = (v_clipPosition.xy / v_clipPosition.w) * 0.5 + 0.5; + vec2 opaqueTextureUV = mix(viewportUV, vec2(distortUV.x), material_DistortValue * fade); + + gl_FragColor = texture2D(camera_OpaqueTexture, opaqueTextureUV); + }`; + + Shader.create("HeatDistortion", heatDistortionVS, heatDistortionFS); + + // Load noise texture + const noiseTexture = await engine.resourceManager.load({ + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*H_HOQZUHdKQAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }); + + const entity = root.createChild("NoisePlane"); + entity.transform.setPosition(0, 1.5, 1); + entity.transform.rotate(new Vector3(90, 0, 0)); + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine, 2, 2); + renderer.priority = -1; + + // Create material + const material = new BaseMaterial(engine, Shader.find("HeatDistortion")); + material.isTransparent = true; + + const shaderData = material.shaderData; + shaderData.setFloat("material_DistortSpeed", 0.8); + shaderData.setFloat("material_DistortValue", 0.05); + shaderData.setFloat("material_FadeRadius", 4); + shaderData.setTexture("material_NoiseTexture", noiseTexture); + + renderer.setMaterial(material); +} + +function addFire(engine: Engine, rootEntity: Entity) { + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: " https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JlayRa2WltYAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*cFafRr6WaWUAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*TASTTpESkIIAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((textures) => { + const fireEntity = createFireParticle(engine, textures[0]); + createFireGlowParticle(fireEntity, textures[1]); + createFireSmokeParticle(fireEntity, textures[2]); + createFireEmbersParticle(fireEntity, textures[3]); + + rootEntity.addChild(fireEntity); + }); +} + +function createFireParticle(engine: Engine, texture: Texture2D): Entity { + const particleEntity = new Entity(engine, "Fire"); + particleEntity.transform.setPosition(0, -0.1, 0.5); + particleEntity.transform.scale.set(1.268892, 1.268892, 1.268892); + particleEntity.transform.rotate(90, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 2; + + const generator = particleRenderer.generator; + const { + main, + emission, + textureSheetAnimation, + sizeOverLifetime, + colorOverLifetime, + } = generator; + + // Main module + const { startLifetime, startSpeed, startSize, startRotationZ } = main; + startLifetime.constantMin = 0.2; + startLifetime.constantMax = 0.8; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + startSpeed.constantMin = 0.4; + startSpeed.constantMax = 1.6; + startSpeed.mode = ParticleCurveMode.TwoConstants; + + startSize.constantMin = 0.6; + startSize.constantMax = 0.9; + startSize.mode = ParticleCurveMode.TwoConstants; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.simulationSpace = ParticleSimulationSpace.World; + + // Emission module + emission.rateOverTime.constant = 35; + + const coneShape = new ConeShape(); + coneShape.angle = 0.96; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].color.set(255 / 255, 127 / 255, 4 / 255, 1.0); + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 123 / 255, 0 / 255, 1.0); + gradient.addColorKey(0.157, new Color(1, 1, 1, 1)); + gradient.addColorKey(0.573, new Color(255 / 255, 255 / 255, 137 / 255, 1)); + gradient.alphaKeys[1].time = 0.089; + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 0.529; + curve.addKey(0.074, 0.428 + 0.2); + curve.addKey(0.718, 0.957 + 0.03); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(6, 6); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.mode = ParticleCurveMode.TwoCurves; + frameOverTime.curveMin = new ParticleCurve( + new CurveKey(0, 0.47), + new CurveKey(1, 1) + ); + + return particleEntity; +} + +function createFireGlowParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireGlow"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 1; + + const generator = particleRenderer.generator; + const { main, emission, sizeOverLifetime, colorOverLifetime } = generator; + + // Main module + const { startLifetime, startSpeed, startRotationZ } = main; + startLifetime.constantMin = 0.2; + startLifetime.constantMax = 0.6; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + startSpeed.constantMin = 0.0; + startSpeed.constantMax = 1.4; + startSpeed.mode = ParticleCurveMode.TwoConstants; + + main.startSize.constant = 1.2; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 100 / 255, + 0 / 255, + 168 / 255 + ); + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 20; + + const coneShape = new ConeShape(); + coneShape.angle = 15; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 50 / 255, 0 / 255, 1.0); + + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + + gradient.addAlphaKey(0.057, 247 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 1.0; + curve.addKey(0.057, 0.37); + curve.addKey(0.728, 0.958); +} + +function createFireSmokeParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireSmoke"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 0; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + textureSheetAnimation, + } = generator; + + // Main module + const { startLifetime, startRotationZ } = main; + startLifetime.constantMin = 1; + startLifetime.constantMax = 1.2; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 1.5; + + main.startSize.constant = 1.2; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 255 / 255, + 255 / 255, + 84 / 255 + ); + + main.gravityModifier.constant = -0.05; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 25; + + const coneShape = new ConeShape(); + coneShape.angle = 10; + coneShape.radius = 0.1; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].time = 0; + colorKeys[0].color.set(255 / 255, 98 / 255, 0 / 255, 1.0); + colorKeys[1].time = 0.679; + colorKeys[1].color.set(0, 0, 0, 1.0); + gradient.addColorKey(0.515, new Color(255 / 255, 98 / 255, 0 / 255, 1.0)); + + const alphaKeys = gradient.alphaKeys; + alphaKeys[0].alpha = 0; + alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.121, 1); + gradient.addAlphaKey(0.329, 200 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.28; + keys[1].value = 1.0; + curve.addKey(0.607, 0.909); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(8, 8); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.curveMax.keys[1].value = 0.382; +} + +function createFireEmbersParticle( + fireEntity: Entity, + texture: Texture2D +): void { + const particleEntity = fireEntity.createChild("FireEmbers"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 3; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + velocityOverLifetime, + rotationOverLifetime, + } = generator; + + // Main module + const { startLifetime, startSize, startRotationZ } = main; + main.duration = 3; + + startLifetime.constantMin = 1; + startLifetime.constantMax = 1.5; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 0.4; + + startSize.constantMin = 0.05; + startSize.constantMax = 0.2; + startSize.mode = ParticleCurveMode.TwoConstants; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.gravityModifier.constant = -0.15; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 65; + emission.addBurst(new Burst(0, new ParticleCompositeCurve(15))); + + const sphereShape = new SphereShape(); + sphereShape.radius = 0.01; + emission.shape = sphereShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = -0.1; + velocityOverLifetime.velocityX.constantMax = 0.1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = -0.1; + velocityOverLifetime.velocityY.constantMax = 0.1; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = -0.1; + velocityOverLifetime.velocityZ.constantMax = 0.1; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.TwoGradients; + + const gradientMax = colorOverLifetime.color.gradientMax; + const maxColorKeys = gradientMax.colorKeys; + maxColorKeys[0].time = 0.315; + maxColorKeys[1].time = 0.998; + maxColorKeys[1].color.set(255 / 255, 92 / 255, 0, 1.0); + gradientMax.addColorKey(0.71, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + + const gradientMin = colorOverLifetime.color.gradientMin; + gradientMin.addColorKey(0.0, new Color(1.0, 1.0, 1.0, 1.0)); + gradientMin.addColorKey(0.486, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + gradientMin.addColorKey(1.0, new Color(255 / 255, 94 / 255, 0 / 255, 1.0)); + + gradientMin.addAlphaKey(0.0, 1); + gradientMin.addAlphaKey(0.229, 1); + gradientMin.addAlphaKey(0.621, 0); + gradientMin.addAlphaKey(0.659, 1); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + const curve = sizeOverLifetime.size.curve; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + curve.keys[0].value = 1; + curve.keys[1].value = 0; + + // Rotation over lifetime module + rotationOverLifetime.enabled = true; + rotationOverLifetime.rotationZ.mode = ParticleCurveMode.TwoConstants; + rotationOverLifetime.rotationZ.constantMin = 90; + rotationOverLifetime.rotationZ.constantMax = 360; + + // Renderer + particleRenderer.pivot = new Vector3(0.2, 0.2, 0); +} diff --git a/examples/model-mesh.ts b/examples/model-mesh.ts new file mode 100644 index 000000000..820055488 --- /dev/null +++ b/examples/model-mesh.ts @@ -0,0 +1,176 @@ +/** + * @title Model Mesh + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*-wVwQrYm6b8AAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + Camera, + Color, + Engine, + Entity, + Material, + MeshRenderer, + ModelMesh, + PrimitiveMesh, + Script, + Shader, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +main(); + +async function main() { + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create root entity + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 10); + cameraEntity.transform.lookAt(new Vector3(0, 8, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 2000; + camera.fieldOfView = 55; + + createPlane(engine, rootEntity); + engine.run(); +} + +/** + * Create a plane as a child of entity. + */ +function createPlane(engine: Engine, entity: Entity): void { + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_2e421e/afts/img/A*fRtNTKrsq3YAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + const planeEntity = entity.createChild("plane"); + const meshRenderer = planeEntity.addComponent(MeshRenderer); + const material = new Material(engine, shader); + + // planeEntity.transform.setRotation(-90, 0, 0); + meshRenderer.mesh = PrimitiveMesh.createPlane( + engine, + 1245, + 1245, + 100, + 100, + false + ); + meshRenderer.setMaterial(material); + + planeEntity.addComponent(PlaneAnimation); + + const { shaderData } = material; + shaderData.setTexture("material_BaseTexture", texture); + shaderData.setColor("u_fogColor", new Color(0.25, 0.25, 0.25, 1)); + shaderData.setFloat("u_fogDensity", 0.004); + shaderData.setColor( + "u_color", + new Color(86 / 255, 182 / 255, 194 / 255, 1) + ); + }); +} + +/** + * Plane animation script. + */ +class PlaneAnimation extends Script { + private _planeMesh: ModelMesh; + private _initZ: number[]; + private _counter: number = 0; + + /** + * @override + * Called when be enabled first time, only once. + */ + onAwake(): void { + const renderer = this.entity.getComponent(MeshRenderer); + console.log('render', renderer) + const mesh = renderer.mesh; + const { vertexCount } = mesh; + const positions = mesh.getPositions(); + const initY = new Array(vertexCount); + + for (var i = 0; i < vertexCount; i++) { + const position = positions[i]; + position.y += Math.random() * 10 - 10; + initY[i] = position.y; + } + this._initZ = initY; + this._planeMesh = mesh; + } + + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + const mesh = this._planeMesh; + let { _counter: counter, _initZ: initZ } = this; + const positions = mesh.getPositions(); + for (let i = 0, n = positions.length; i < n; i++) { + const position = positions[i]; + position.y = + Math.sin(i + counter * 0.00002) * (initZ[i] - initZ[i] * 0.6); + counter += 0.1; + } + mesh.setPositions(positions); + mesh.uploadData(false); + this._counter = counter; + } +} + +const shader = Shader.create( + "test-plane", + `uniform mat4 renderer_MVPMat; + attribute vec4 POSITION; + attribute vec2 TEXCOORD_0; + + uniform mat4 renderer_MVMat; + + varying vec2 v_uv; + varying vec3 v_position; + + void main() { + v_uv = TEXCOORD_0; + v_position = (renderer_MVMat * POSITION).xyz; + gl_Position = renderer_MVPMat * POSITION; + }`, + + ` + uniform sampler2D material_BaseTexture; + uniform vec4 u_color; + uniform vec4 u_fogColor; + uniform float u_fogDensity; + + varying vec2 v_uv; + varying vec3 v_position; + + + vec4 linearToGamma(vec4 linearIn){ + return vec4( pow(linearIn.rgb, vec3(1.0 / 2.2)), linearIn.a); + } + + void main() { + vec4 color = texture2D(material_BaseTexture, v_uv) * u_color; + float fogDistance = length(v_position); + float fogAmount = 1. - exp2(-u_fogDensity * u_fogDensity * fogDistance * fogDistance * 1.442695); + fogAmount = clamp(fogAmount, 0., 1.); + gl_FragColor = mix(color, u_fogColor, fogAmount); + + #ifndef ENGINE_IS_COLORSPACE_GAMMA + gl_FragColor = linearToGamma(gl_FragColor); + #endif + } + ` +); diff --git a/examples/mrt.ts b/examples/mrt.ts new file mode 100644 index 000000000..462c6e9b9 --- /dev/null +++ b/examples/mrt.ts @@ -0,0 +1,168 @@ +/** + * @title Multiple Render Targets + * @category Advance + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*mzsETqwXuPIAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + CullMode, + DirectLight, + GLTFResource, + Layer, + Material, + MeshRenderer, + PrimitiveMesh, + RenderTarget, + Script, + Shader, + Texture2D, + TextureFilterMode, + TextureFormat, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + camera.cullingMask = Layer.Layer0; + cameraEntity.transform.setPosition(0, 0, 5); + const control = cameraEntity.addComponent(OrbitControl); + control.minDistance = 3; + camera.scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + const lightEntity = rootEntity.createChild(); + lightEntity.addComponent(DirectLight); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + const width = engine.canvas.width; + const height = engine.canvas.height; + + const positionTexture = new Texture2D(engine, width, height); + const depthTexture = new Texture2D(engine, width, height); + const normalTexture = new Texture2D(engine, width, height); + const autoDepthTexture = new Texture2D( + engine, + width, + height, + TextureFormat.Depth, + false + ); + autoDepthTexture.filterMode = TextureFilterMode.Point; + const renderTarget = new RenderTarget( + engine, + width, + height, + [positionTexture, depthTexture, normalTexture], + autoDepthTexture + ); + + const positionPlaneEntity = createPlane(positionTexture); + positionPlaneEntity.transform.setPosition(0, 3, -6); + const depthPlaneEntity = createPlane(depthTexture); + depthPlaneEntity.transform.setPosition(0, 1, -6); + const normalEntity = createPlane(normalTexture); + normalEntity.transform.setPosition(0, -1, -6); + const autoDepthEntity = createPlane(autoDepthTexture); + autoDepthEntity.transform.setPosition(0, -3, -6); + + const mrtMatrial = getMRTMaterial(); + mrtMatrial.renderState.rasterState.cullMode = CullMode.Off; + + class mrtScript extends Script { + private materialMap: Array<{ renderer: MeshRenderer; material: Material }> = + []; + private rendererList: Array = []; + + onBeginRender(camera: Camera): void { + this.materialMap.length = 0; + this.rendererList.length = 0; + rootEntity.getComponentsIncludeChildren(MeshRenderer, this.rendererList); + for (let i = 0; i < this.rendererList.length; i++) { + const renderer = this.rendererList[i]; + if (renderer.entity.layer === Layer.Layer1) continue; + const material = renderer.getMaterial(); + if (material) { + this.materialMap.push({ renderer: this.rendererList[i], material }); + renderer.setMaterial(mrtMatrial); + } + } + + camera.renderTarget = renderTarget; + camera.render(); + camera.renderTarget = null; + this.materialMap.forEach(({ renderer, material }) => { + renderer.setMaterial(material); + }); + } + } + cameraEntity.addComponent(mrtScript); + createWindowCamera(); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf" + ) + .then((gltf) => { + const { defaultSceneRoot } = gltf; + rootEntity.addChild(defaultSceneRoot); + + engine.run(); + }); + + function getMRTMaterial() { + const vertex = ` + uniform mat4 renderer_MVPMat; + uniform mat4 renderer_ModelMat; + varying vec4 worldPos; + varying vec4 normal; + attribute vec3 NORMAL; + attribute vec3 POSITION; + void main() { + worldPos = renderer_ModelMat * vec4(POSITION, 1.0); + normal = renderer_ModelMat * vec4(NORMAL, 1.0); + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + }`; + const frag = ` + varying vec4 worldPos; + varying vec4 normal; + void main() { + gl_FragData[0] = vec4(worldPos.xyz, 1.0); + gl_FragData[1] = vec4(vec3(worldPos.z), 1.0); + gl_FragData[2] = vec4(normal.xyz, 1.0); + } + `; + + const shader = Shader.create("MRT", vertex, frag); + return new Material(engine, shader); + } + + function createPlane(texture: Texture2D) { + const entity = rootEntity.createChild(); + entity.transform.setRotation(90, 0, 0); + entity.layer = Layer.Layer1; + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine, 2, (2 * height) / width); + const material = new UnlitMaterial(engine); + material.baseTexture = texture; + renderer.setMaterial(material); + + return entity; + } + + function createWindowCamera() { + const windowEntity = scene.createRootEntity(); + windowEntity.layer = Layer.Layer1; + const windowCameraEntity = windowEntity.createChild("window-camera"); + const windowCamera = windowCameraEntity.addComponent(Camera); + windowCamera.cullingMask = Layer.Layer1; + windowCamera.viewport.set(0.7, 0.0, 0.3, 0.7); + windowCameraEntity.transform.setPosition(0, 0, 3); + } +}); diff --git a/examples/multi-camera.ts b/examples/multi-camera.ts new file mode 100644 index 000000000..ea42892c5 --- /dev/null +++ b/examples/multi-camera.ts @@ -0,0 +1,120 @@ +/** + * @title Multi Camera + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*_vyfS5xqVe4AAAAAAAAAAAAADiR2AQ/original + */ + +import { SpineAnimation } from "@galacean/engine-spine"; +import { + AssetType, + BackgroundMode, + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + Entity, + Layer, + MeshRenderer, + PrimitiveMesh, + Script, + SkyBoxMaterial, + TextureCube, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +/** + * Script for rotate. + */ +class RotateScript extends Script { + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + this.entity.transform.rotate(0.0, 0.6, 0); + } +} + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const { background } = scene; + const rootEntity = scene.createRootEntity(); + + // init full screen camera + const cameraEntity = rootEntity.createChild("fullscreen-camera"); + const camera = cameraEntity.addComponent(Camera); + camera.cullingMask = Layer.Layer0; + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-30, 0, 0); + lightEntity.addComponent(DirectLight); + + // init cube + const cubeEntity = rootEntity.createChild("cube"); + cubeEntity.transform.setPosition(-3, 0, 3); + const renderer = cubeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + cubeEntity.addComponent(RotateScript); + + //---------------------------------------------------------------------------------------------------------------------- + // init window camera + const windowEntity = scene.createRootEntity(); + windowEntity.layer = Layer.Layer1; + const windowCameraEntity = windowEntity.createChild("window-camera"); + const windowCamera = windowCameraEntity.addComponent(Camera); + windowCamera.cullingMask = Layer.Layer1; + windowCamera.viewport.set(0.5, 0.2, 0.3, 0.6); + windowCamera.farClipPlane = 200; + windowCameraEntity.transform.setPosition(0, 3, 20); + + engine.run(); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/OasisHub/a66ef194-6bc8-4325-9a59-6ea9097225b1/1620888427489.json", + "https://gw.alipayobjects.com/os/OasisHub/a1e3e67b-a783-4832-ba1b-37a95bd55291/1620888427490.atlas", + "https://gw.alipayobjects.com/zos/OasisHub/a3ca8f62-1068-43a5-bb64-5c9a0f823dde/1620888427490.png", + ], + type: "spine", + }) + .then((spineEntity: Entity) => { + spineEntity.layer = Layer.Layer1; + windowEntity.addChild(spineEntity); + const spineAnimation = spineEntity.getComponent(SpineAnimation); + spineAnimation.state.setAnimation(0, "walk", true); + spineAnimation.scale = 0.01; + }); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*TiT2TbN5cG4AAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8GF6Q4LZefUAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*D5pdRqUHC3IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_FooTIp6pNIAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*CYGZR7ogZfoAAAAAAAAAAAAAARQnAQ", + ], + type: AssetType.TextureCube, + }) + .then((cubeMap1) => { + // Add skybox background + background.mode = BackgroundMode.Sky; + const skyMaterial = (background.sky.material = new SkyBoxMaterial( + engine + )); + skyMaterial.texture = cubeMap1; + background.sky.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); + }); +}); diff --git a/examples/multi-scene.ts b/examples/multi-scene.ts new file mode 100644 index 000000000..6e4c12fad --- /dev/null +++ b/examples/multi-scene.ts @@ -0,0 +1,166 @@ +/** + * @title Multi Scene + * @category Scene + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*C6ggR5ur22UAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AssetType, + BackgroundMode, + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + Engine, + GLTFResource, + Layer, + MeshRenderer, + PrimitiveMesh, + Scene, + SceneManager, + Script, + SkyBoxMaterial, + TextureCube, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const firstScene = initFirstScene(engine); + const secondScene = initSecondScene(engine); + engine.run(); + addGUI(engine.sceneManager, firstScene, secondScene); +}); + +function initFirstScene(engine: Engine): Scene { + const scene = engine.sceneManager.scenes[0]; + const rootEntity = scene.createRootEntity(); + + // Add sky box background + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*TiT2TbN5cG4AAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8GF6Q4LZefUAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*D5pdRqUHC3IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_FooTIp6pNIAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*CYGZR7ogZfoAAAAAAAAAAAAAARQnAQ", + ], + type: AssetType.TextureCube, + }) + .then((cubeMap1) => { + const { background } = scene; + background.mode = BackgroundMode.Sky; + const skyMaterial = new SkyBoxMaterial(engine); + skyMaterial.texture = cubeMap1; + + background.sky.material = skyMaterial; + background.sky.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); + }); + + // Create full screen camera + const cameraEntity = rootEntity.createChild("fullscreen-camera"); + const camera = cameraEntity.addComponent(Camera); + camera.cullingMask = Layer.Layer0; + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-30, 0, 0); + lightEntity.addComponent(DirectLight); + + // Create cube + const cubeEntity = rootEntity.createChild("cube"); + cubeEntity.transform.setPosition(-3, 0, 3); + const renderer = cubeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 2, 24); + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + return scene; +} + +function initSecondScene(engine: Engine): Scene { + // Init window camera + const scene = new Scene(engine); + engine.sceneManager.addScene(scene); + const rootEntity = scene.createRootEntity(); + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, 0, 0); + lightEntity.addComponent(DirectLight); + + const cameraEntity = rootEntity.createChild("window-camera"); + const camera = cameraEntity.addComponent(Camera); + camera.viewport.set(0.6, 0.2, 0.25, 0.6); + camera.farClipPlane = 200; + cameraEntity.transform.setPosition(0, 3, 5); + cameraEntity.transform.lookAt(new Vector3(0, 1, 0)); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf" + ) + .then((gltf) => { + const defaultSceneRoot = gltf.defaultSceneRoot; + rootEntity.addChild(defaultSceneRoot); + defaultSceneRoot.addComponent(RotateScript); + }); + return scene; +} + +/** + * Script for rotate. + */ +class RotateScript extends Script { + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + this.entity.transform.rotate(0.0, 50 * deltaTime, 0); + } +} + +function addGUI( + sceneManager: SceneManager, + firstScene: Scene, + secondScene: Scene +) { + const guiData = { + showFirst: true, + showSecond: true, + }; + + const gui = new dat.GUI(); + const sceneFolder = gui.addFolder("multi scene"); + sceneFolder.open(); + + gui + .add(guiData, "showFirst") + .onChange((value: boolean) => { + if (value) { + sceneManager.addScene(0, firstScene); + } else { + sceneManager.removeScene(firstScene); + } + }) + .listen(); + gui + .add(guiData, "showSecond") + .onChange((value: boolean) => { + if (value) { + sceneManager.addScene(1, secondScene); + } else { + sceneManager.removeScene(secondScene); + } + }) + .listen(); +} diff --git a/examples/multi-viewport.ts b/examples/multi-viewport.ts new file mode 100644 index 000000000..7140c1f31 --- /dev/null +++ b/examples/multi-viewport.ts @@ -0,0 +1,79 @@ +/** + * @title Multi Viewport + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*1c2aQpboBxwAAAAAAAAAAAAADiR2AQ/original + */ + +import { + WebGLEngine, + DirectLight, + AssetType, + Camera, + Vector3, + MeshRenderer, + BlinnPhongMaterial, + Color, + BackgroundMode, + SkyBoxMaterial, + PrimitiveMesh, + TextureCube, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const { background } = scene; + const rootEntity = scene.createRootEntity(); + + // init camera + const leftCameraEntity = rootEntity.createChild("left-camera"); + const leftCamera = leftCameraEntity.addComponent(Camera); + leftCamera.viewport.set(0, 0, 0.5, 1); + leftCameraEntity.transform.setPosition(10, 10, 10); + leftCameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + leftCameraEntity.addComponent(OrbitControl); + + const rightCameraEntity = rootEntity.createChild("right-camera"); + const rightCamera = rightCameraEntity.addComponent(Camera); + rightCamera.viewport.set(0.5, 0, 0.5, 1); + rightCameraEntity.transform.setPosition(10, 10, 10); + rightCameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + rightCameraEntity.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-30, 0, 0); + lightEntity.addComponent(DirectLight); + + // init cube + const cubeEntity = rootEntity.createChild("cube"); + const renderer = cubeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + + engine.run(); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5w6_Rr6ML6IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*TiT2TbN5cG4AAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*8GF6Q4LZefUAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*D5pdRqUHC3IAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_FooTIp6pNIAAAAAAAAAAAAAARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*CYGZR7ogZfoAAAAAAAAAAAAAARQnAQ", + ], + type: AssetType.TextureCube, + }) + .then((cubeMap1) => { + // Add skybox background + background.mode = BackgroundMode.Sky; + const skyMaterial = (background.sky.material = new SkyBoxMaterial( + engine + )); + skyMaterial.texture = cubeMap1; + background.sky.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); + }); +}); diff --git a/examples/obj-loader.ts b/examples/obj-loader.ts new file mode 100644 index 000000000..640b606f0 --- /dev/null +++ b/examples/obj-loader.ts @@ -0,0 +1,78 @@ +/** + * @title OBJ Loader Use Model Mesh + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*ZnBlT4UNh0IAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + MeshRenderer, + MeshTopology, + ModelMesh, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + cameraEntity.transform.setPosition(0.5, 0.5, 0.5); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // init light + rootEntity.addComponent(DirectLight); + + fetch( + "https://gw.alipayobjects.com/os/bmw-prod/b885a803-5315-44f0-af54-6787ec47ed1b.obj" + ) + .then((res) => res.text()) + .then((objText) => { + const lines = objText.split(/\n/); + const positions = []; + const indices: number[] = []; + lines + .map((lineText) => lineText.split(" ")) + .forEach((parseTexts) => { + if (parseTexts[0] === "v") { + positions.push( + new Vector3( + parseFloat(parseTexts[1]), + parseFloat(parseTexts[2]), + parseFloat(parseTexts[3]) + ) + ); + } else if (parseTexts[0] === "f") { + indices.push( + parseInt(parseTexts[1]) - 1, + parseInt(parseTexts[2]) - 1, + parseInt(parseTexts[3]) - 1 + ); + } + }); + + const mesh = new ModelMesh(engine); + mesh.setPositions(positions); + mesh.setIndices(Uint16Array.from(indices)); + mesh.addSubMesh(0, indices.length, MeshTopology.Triangles); + mesh.uploadData(false); + + // init cube + const cubeEntity = rootEntity.createChild("cube"); + const renderer = cubeEntity.addComponent(MeshRenderer); + renderer.mesh = mesh; + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + }); + + engine.run(); +}); diff --git a/examples/ortho-control.ts b/examples/ortho-control.ts new file mode 100644 index 000000000..2d1cd5974 --- /dev/null +++ b/examples/ortho-control.ts @@ -0,0 +1,53 @@ +/** + * @title Ortho Controls + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*I4x0Qrp0mUcAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + Camera, + Sprite, + SpriteRenderer, + TextRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; +import { OrthoControl } from "@galacean/engine-toolkit-controls"; + +// Create engine object + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + + // Add tip + const tipEntity = rootEntity.createChild("Tip"); + tipEntity.transform.setPosition(0, 5, 0); + const textRenderer = tipEntity.addComponent(TextRenderer); + textRenderer.text = "Hold right button and drag"; + textRenderer.fontSize = 50; + + // Add camera control. + cameraEntity.addComponent(OrthoControl); + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*KjnzTpE8LdAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create sprite entity. + const spriteEntity = rootEntity.createChild("sprite"); + const spriteRenderer = spriteEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = new Sprite(engine, texture); + }); + + engine.run(); +}); diff --git a/examples/ortho-switch.ts b/examples/ortho-switch.ts new file mode 100644 index 000000000..7fb35d117 --- /dev/null +++ b/examples/ortho-switch.ts @@ -0,0 +1,60 @@ +/** + * @title Orthographic Camera + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*WK0RSLboFQgAAAAAAAAAAAAADiR2AQ/original + */ +import { GUI } from "dat.gui"; +import { + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + MeshRenderer, + PrimitiveMesh, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + camera.nearClipPlane = 0.1; + cameraEntity.transform.setPosition(10, 2, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-30, 0, 0); + lightEntity.addComponent(DirectLight); + + // init cube + createCube(0, 0, 0); + createCube(6.5, 0, 6.5); + createCube(-6.5, 0, -6.5); + addGUI(); + + engine.run(); + + function createCube(x: number, y: number, z: number) { + const cubeEntity = rootEntity.createChild("cube"); + const renderer = cubeEntity.addComponent(MeshRenderer); + const material = new BlinnPhongMaterial(engine); + cubeEntity.transform.setPosition(x, y, z); + material.baseColor = new Color(1, 0.25, 0.25, 1); + renderer.setMaterial(material); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + } + + function addGUI() { + const gui = new GUI(); + const cameraFolder = gui.addFolder("camera orthogonal switch"); + cameraFolder.open(); + camera.isOrthographic = true; + cameraFolder.add(camera, "isOrthographic"); + } +}); diff --git a/examples/outline-multi-pass.ts b/examples/outline-multi-pass.ts new file mode 100644 index 000000000..105e05770 --- /dev/null +++ b/examples/outline-multi-pass.ts @@ -0,0 +1,323 @@ +/** + * @title Outline multi-pass + * @category Advance + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Vnw3R7c8HbcAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AmbientLight, + AssetType, + Camera, + Color, + CompareFunction, + CullMode, + Engine, + Entity, + GLTFResource, + Material, + MeshRenderer, + Script, + Shader, + StencilOperation, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +const gui = new dat.GUI(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.run(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor.set(1, 1, 1, 1); + + // camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 1.3, 1); + const camera = cameraNode.addComponent(Camera); + camera.enableFrustumCulling = false; + cameraNode.addComponent(OrbitControl).target.set(0, 1.3, 0); + + // ambient light + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + ambientLight.specularIntensity = ambientLight.diffuseIntensity = 2; + }); + + engine.resourceManager + .load({ + type: AssetType.GLTF, + url: "https://gw.alipayobjects.com/os/OasisHub/440000554/3615/%25E5%25BD%2592%25E6%25A1%25A3.gltf", + }) + .then((gltf: GLTFResource) => { + const { defaultSceneRoot } = gltf; + rootEntity.addChild(defaultSceneRoot); + + openDebug(); + }); + + /** ------------------ Border ------------------ */ + // 外描边-模版测试 + class Border extends Script { + material: Material; + borderRenderer: MeshRenderer[] = []; + + private _size: number = 3; + private _color: Color = new Color(0, 0, 0, 1); + + get size(): number { + return this._size; + } + + set size(value: number) { + this.material.shaderData.setFloat("u_width", value * 0.001); + this._size = value; + } + + get color(): Color { + return this._color; + } + + set color(value: Color) { + this.material.shaderData.setColor("u_color", value); + this._color = value; + } + + getBorderMaterial(engine: Engine) { + if (!this.material) { + if (!Shader.find("border-shader")) { + const vertex = ` + attribute vec3 POSITION; + attribute vec3 NORMAL; + + uniform float u_width; + uniform mat4 renderer_MVPMat; + uniform mat4 renderer_ModelMat; + uniform mat4 camera_ViewMat; + uniform mat4 camera_ProjMat; + uniform mat4 renderer_NormalMat; + + void main() { + vec4 mPosition = renderer_ModelMat * vec4(POSITION, 1.0); + vec3 mNormal = normalize( mat3(renderer_NormalMat) * NORMAL ); + mPosition.xyz += mNormal * u_width; + gl_Position = camera_ProjMat * camera_ViewMat * mPosition; + } + `; + const fragment = ` + uniform vec3 u_color; + + void main(){ + gl_FragColor = vec4(u_color, 1); + } + `; + + Shader.create("border-shader", vertex, fragment); + } + const material = new Material(engine, Shader.find("border-shader")); + this.material = material; + material.renderState.rasterState.cullMode = CullMode.Off; + const stencilState = material.renderState.stencilState; + stencilState.enabled = true; + stencilState.referenceValue = 1; + stencilState.compareFunctionFront = CompareFunction.NotEqual; + stencilState.compareFunctionBack = CompareFunction.NotEqual; + stencilState.writeMask = 0x00; + this.size = this._size; + this.color = this._color; + } + + return this.material; + } + + showBorder(renderer: MeshRenderer) { + const entity = renderer.entity; + const material = renderer.getMaterial(); + const stencilState = material.renderState.stencilState; + + stencilState.enabled = true; + stencilState.referenceValue = 1; + stencilState.passOperationFront = StencilOperation.Replace; + + const borderMaterial = this.getBorderMaterial(entity.engine); + + const borderRenderer = entity.addComponent(MeshRenderer); + borderRenderer.mesh = renderer.mesh; + borderRenderer.setMaterial(borderMaterial); + borderRenderer.priority = 1; + this.borderRenderer.push(borderRenderer); + } + + constructor(entity: Entity) { + super(entity); + const meshes: MeshRenderer[] = []; + rootEntity.getComponentsIncludeChildren(MeshRenderer, meshes); + meshes.forEach((mesh) => { + this.showBorder(mesh); + }); + } + + onDestroy() { + this.borderRenderer.forEach((renderer) => { + renderer.destroy(); + }); + this.borderRenderer.length = 0; + } + } + + // 内描边-背面剔除 + class Border2 extends Script { + material: Material; + borderRenderer: MeshRenderer[] = []; + private _size: number = 3; + private _color: Color = new Color(0, 0, 0, 1); + + get size(): number { + return this._size; + } + + set size(value: number) { + this.material.shaderData.setFloat("u_width", value * 0.001); + this._size = value; + } + + get color(): Color { + return this._color; + } + + set color(value: Color) { + this.material.shaderData.setColor("u_color", value); + this._color = value; + } + + getBorderMaterial(engine: Engine) { + if (!this.material) { + if (!Shader.find("border-shader")) { + const vertex = ` + attribute vec3 POSITION; + attribute vec3 NORMAL; + + uniform float u_width; + uniform mat4 renderer_MVPMat; + uniform mat4 renderer_ModelMat; + uniform mat4 camera_ViewMat; + uniform mat4 camera_ProjMat; + uniform mat4 renderer_NormalMat; + + void main() { + vec4 mPosition = renderer_ModelMat * vec4(POSITION, 1.0); + vec3 mNormal = normalize( mat3(renderer_NormalMat) * NORMAL ); + mPosition.xyz += mNormal * u_width; + gl_Position = camera_ProjMat * camera_ViewMat * mPosition; + } + `; + const fragment = ` + uniform vec3 u_color; + + void main(){ + gl_FragColor = vec4(u_color, 1); + } + `; + + Shader.create("border-shader", vertex, fragment); + } + const material = new Material(engine, Shader.find("border-shader")); + this.material = material; + material.renderState.rasterState.cullMode = CullMode.Front; + this.size = this._size; + this.color = this._color; + } + + return this.material; + } + + showBorder(renderer: MeshRenderer) { + const entity = renderer.entity; + + const borderMaterial = this.getBorderMaterial(entity.engine); + const borderRenderer = entity.addComponent(MeshRenderer); + borderRenderer.mesh = renderer.mesh; + borderRenderer.setMaterial(borderMaterial); + borderRenderer.priority = 1; + this.borderRenderer.push(borderRenderer); + } + + constructor(entity: Entity) { + super(entity); + const renderers: MeshRenderer[] = []; + rootEntity.getComponentsIncludeChildren(MeshRenderer, renderers); + renderers.forEach((renderer) => { + this.showBorder(renderer); + }); + } + + onDestroy() { + this.borderRenderer.forEach((renderer) => { + renderer.destroy(); + }); + this.borderRenderer.length = 0; + } + } + + function openDebug() { + const borderEntity = rootEntity.createChild("border"); + const color = new Color(); + let border: Border | Border2 = borderEntity.addComponent(Border); + + const config = { + plan: "外描边", + size: 3, + color: [0, 0, 0], + }; + + gui + .add(config, "plan", ["外描边", "内描边"]) + .onChange((v) => { + color.set( + config.color[0] / 255, + config.color[1] / 255, + config.color[2] / 255, + 1 + ); + + border.destroy(); + if (v === "外描边") { + border = borderEntity.addComponent(Border); + + border.size = config.size; + border.color = color; + showSize(); + } else if (v === "内描边") { + border = borderEntity.addComponent(Border2); + border.size = config.size; + border.color = color; + showSize(); + } + }) + .name("描边方案"); + + let size; + function showSize() { + hideSize(); + size = gui.add(config, "size", 0, 5, 1).onChange((v) => { + border.size = v; + }); + } + function hideSize() { + size && size.remove(); + size = null; + } + + showSize(); + gui.addColor(config, "color").onChange((v) => { + color.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + border.color = color; + }); + } +}); diff --git a/examples/outline-postprocess.ts b/examples/outline-postprocess.ts new file mode 100644 index 000000000..81085668a --- /dev/null +++ b/examples/outline-postprocess.ts @@ -0,0 +1,120 @@ +/** + * @title Outline post-process + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*bSgLQqSgilcAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AmbientLight, + Animator, + AssetType, + Camera, + GLTFResource, + MeshRenderer, + PBRMaterial, + PointerButton, + PrimitiveMesh, + Script, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { FramebufferPicker } from "@galacean/engine-toolkit-framebuffer-picker"; +import { OutlineManager } from "@galacean/engine-toolkit-outline"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.run(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor.set(1, 1, 1, 1); + + const cameraEntity = rootEntity.createChild("camera_entity"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(1, 1.3, 10); + cameraEntity.addComponent(OrbitControl).target.set(1, 1.3, 0); + + const outlineManager = cameraEntity.addComponent(OutlineManager); + outlineManager.isChildrenIncluded = true; + addDebugGUI(outlineManager); + + const framebufferPicker = cameraEntity.addComponent(FramebufferPicker); + + class ClickScript extends Script { + onUpdate(): void { + const inputManager = this.engine.inputManager; + const { pointers } = inputManager; + if (pointers && inputManager.isPointerDown(PointerButton.Primary)) { + const pointerPosition = pointers[0].position; + framebufferPicker + .pick(pointerPosition.x, pointerPosition.y) + .then((renderElement) => { + if (renderElement) { + outlineManager.addEntity(renderElement.entity); + } else { + outlineManager.clear(); + } + }); + } + } + } + + cameraEntity.addComponent(ClickScript); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + ambientLight.specularIntensity = ambientLight.diffuseIntensity = 2; + }); + + engine.resourceManager + .load({ + type: AssetType.GLTF, + url: "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb", + }) + .then((gltf) => { + const parentEntity = rootEntity.createChild(); + const renderer = parentEntity.addComponent(MeshRenderer); + renderer.setMaterial(new PBRMaterial(engine)); + renderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + const otherEntity = rootEntity.createChild(); + otherEntity.transform.setPosition(2, 0, 0); + const otherRenderer = otherEntity.addComponent(MeshRenderer); + otherRenderer.setMaterial(new PBRMaterial(engine)); + otherRenderer.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + const { defaultSceneRoot, animations } = gltf; + parentEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(animations[0].name); + }); + + function addDebugGUI(outlineManager: OutlineManager) { + const debugInfo = { + mainColor: [ + outlineManager.mainColor.r * 255, + outlineManager.mainColor.g * 255, + outlineManager.mainColor.b * 255, + ], + subColor: [ + outlineManager.subColor.r * 255, + outlineManager.subColor.g * 255, + outlineManager.subColor.b * 255, + ], + }; + const gui = new dat.GUI(); + + gui.add(outlineManager, "size", 1, 6, 0.1); + gui.addColor(debugInfo, "mainColor").onChange((v) => { + outlineManager.mainColor.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + gui.addColor(debugInfo, "subColor").onChange((v) => { + outlineManager.subColor.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + } +}); diff --git a/examples/particle-dream.ts b/examples/particle-dream.ts new file mode 100644 index 000000000..f2a095f92 --- /dev/null +++ b/examples/particle-dream.ts @@ -0,0 +1,365 @@ +/** + * @title Particle Dream + * @category Particle + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*IFy1Rr_cbacAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BlendMode, + BoxShape, + Camera, + Color, + Engine, + Entity, + Logger, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderMode, + ParticleRenderer, + Texture2D, + Vector3, + WebGLEngine, + WebGLMode, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas", + graphicDeviceOptions: { webGLMode: WebGLMode.WebGL1 }, +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor = new Color(15 / 255, 38 / 255, 18 / 255, 1); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.position = new Vector3(0, 1, 3); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + + engine.run(); + + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*eWTFRZPqfDMAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*J8uhRoxJtYgAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Ea3qRb1yCQMAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((textures) => { + const fireEntity = createDebrisParticle(engine, textures[0]); + createGlowParticle(fireEntity, textures[1]); + createSparksParticle(fireEntity, textures[2]); + createHighlightsParticle(fireEntity, textures[3]); + + cameraEntity.addChild(fireEntity); + }); +}); + +function createDebrisParticle(engine: Engine, texture: Texture2D): Entity { + const particleEntity = new Entity(engine, "Debris"); + particleEntity.transform.position.set(0, -7.5, -8); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 2; + + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + velocityOverLifetime, + } = particleRenderer.generator; + + // Main module + main.startSpeed.constant = 0; + + main.startSize.constantMin = 0.1; + main.startSize.constantMax = 1; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constantMin.set(255 / 255, 255 / 255, 255 / 255, 1.0); + main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 1.0); + main.startColor.mode = ParticleGradientMode.TwoConstants; + + // Emission module + emission.rateOverTime.constant = 5; + + const boxShape = new BoxShape(); + boxShape.size.set(22, 1, 0); + emission.shape = boxShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.2, 1.0); + gradient.addAlphaKey(0.8, 1.0); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + const keys = sizeOverLifetime.size.curve.keys; + keys[0].value = 1; + keys[1].value = 0; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = 2; + velocityOverLifetime.velocityX.constantMax = 1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = 4; + velocityOverLifetime.velocityY.constantMax = 2; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = 0; + velocityOverLifetime.velocityZ.constantMax = 0; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + return particleEntity; +} + +function createGlowParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("Glow"); + particleEntity.transform.position.set(-1.88, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + particleRenderer.renderMode = ParticleRenderMode.StretchBillboard; + particleRenderer.lengthScale = 2; + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 1; + + const generator = particleRenderer.generator; + const { main, emission, velocityOverLifetime, colorOverLifetime } = generator; + + // Main module + main.startSpeed.constant = 0.0; + + main.startSize.constantMin = 5; + main.startSize.constantMax = 9; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constantMin = new Color( + 0 / 255, + 157 / 255, + 255 / 255, + 64 / 255 + ); + main.startColor.constantMax = new Color( + 13 / 255, + 255 / 255, + 0 / 255, + 128 / 255 + ); + main.startColor.mode = ParticleGradientMode.TwoConstants; + + // Emission module + emission.rateOverTime.constant = 10; + + const boxShape = new BoxShape(); + boxShape.size.set(22, 1, 0); + emission.shape = boxShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = 2; + velocityOverLifetime.velocityX.constantMax = 1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = 4; + velocityOverLifetime.velocityY.constantMax = 2; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = 0; + velocityOverLifetime.velocityZ.constantMax = 0; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.2, 1.0); +} + +function createSparksParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("Sparks"); + particleEntity.transform.position.set(-1.54, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 0; + + const { main, emission, colorOverLifetime, velocityOverLifetime } = + particleRenderer.generator; + + // Main module + main.startLifetime.constant = 5; + main.startSpeed.constant = 0; + + main.startSize.constantMin = 0.05; + main.startSize.constantMax = 0.2; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 37 / 255, + 133 / 255, + 255 / 255, + 255 / 255 + ); + + // Emission module + emission.rateOverTime.constant = 30; + + const boxShape = new BoxShape(); + boxShape.size.set(22, 1, 0); + emission.shape = boxShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = 2; + velocityOverLifetime.velocityX.constantMax = 1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = 4; + velocityOverLifetime.velocityY.constantMax = 2; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = 0; + velocityOverLifetime.velocityZ.constantMax = 0; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.2, 1.0); + gradient.addAlphaKey(0.8, 1.0); +} + +function createHighlightsParticle( + fireEntity: Entity, + texture: Texture2D +): void { + const particleEntity = fireEntity.createChild("Highlights"); + particleEntity.transform.position.set(-5.31, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 3; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + velocityOverLifetime, + } = generator; + + // Main module + main.startSpeed.constant = 0; + + main.startSize.constantMin = 0.1; + main.startSize.constantMax = 7; + main.startSize.mode = ParticleCurveMode.TwoConstants; + + main.startRotationZ.constantMin = 0; + main.startRotationZ.constantMax = 360; + main.startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constantMin.set(105 / 255, 198 / 255, 255 / 255, 64 / 255); + main.startColor.constantMax.set(13 / 255, 255 / 255, 0 / 255, 32 / 255); + main.startColor.mode = ParticleGradientMode.TwoConstants; + + // Emission module + emission.rateOverTime.constant = 40; + + const boxShape = new BoxShape(); + boxShape.size.set(22, 1, 0); + emission.shape = boxShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = 3; + velocityOverLifetime.velocityX.constantMax = 2; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = 4; + velocityOverLifetime.velocityY.constantMax = 2; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = 0; + velocityOverLifetime.velocityZ.constantMax = 0; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.2, 1.0); + gradient.addAlphaKey(0.8, 1.0); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + const curve = sizeOverLifetime.size.curve; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + curve.keys[0].value = 1; + curve.keys[1].value = 0; +} diff --git a/examples/particle-fire.ts b/examples/particle-fire.ts new file mode 100644 index 000000000..c1d252422 --- /dev/null +++ b/examples/particle-fire.ts @@ -0,0 +1,455 @@ +/** + * @title Particle Fire + * @category Particle + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*i1QXTIWTTO4AAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BlendMode, + Burst, + Camera, + Color, + ConeShape, + CurveKey, + Engine, + Entity, + Logger, + ParticleCompositeCurve, + ParticleCurve, + ParticleCurveMode, + ParticleGradientMode, + ParticleMaterial, + ParticleRenderer, + ParticleScaleMode, + ParticleSimulationSpace, + PointerButton, + Script, + SphereShape, + Texture2D, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ + canvas: "canvas", +}).then((engine) => { + Logger.enable(); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.background.solidColor = new Color(0.25, 0.25, 0.25, 1); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.position = new Vector3(0, 1, 3); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 60; + + engine.run(); + + engine.resourceManager + .load([ + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*yu-DSb0surwAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: " https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JlayRa2WltYAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*cFafRr6WaWUAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*TASTTpESkIIAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((textures) => { + const fireEntity = createFireParticle(engine, textures[0]); + createFireGlowParticle(fireEntity, textures[1]); + createFireSmokeParticle(fireEntity, textures[2]); + createFireEmbersParticle(fireEntity, textures[3]); + fireEntity.addComponent(FireMoveScript); + + rootEntity.addChild(fireEntity); + }); +}); + +function createFireParticle(engine: Engine, texture: Texture2D): Entity { + const particleEntity = new Entity(engine, "Fire"); + particleEntity.transform.scale.set(1.268892, 1.268892, 1.268892); + particleEntity.transform.rotate(90, 0, 0); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(engine); + material.baseColor = new Color(1.0, 1.0, 1.0, 1.0); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 2; + + const generator = particleRenderer.generator; + const { + main, + emission, + textureSheetAnimation, + sizeOverLifetime, + colorOverLifetime, + } = generator; + + // Main module + const { startLifetime, startSpeed, startSize, startRotationZ } = main; + startLifetime.constantMin = 0.2; + startLifetime.constantMax = 0.8; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + startSpeed.constantMin = 0.4; + startSpeed.constantMax = 1.6; + startSpeed.mode = ParticleCurveMode.TwoConstants; + + startSize.constantMin = 0.6; + startSize.constantMax = 0.9; + startSize.mode = ParticleCurveMode.TwoConstants; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.simulationSpace = ParticleSimulationSpace.World; + + // Emission module + emission.rateOverTime.constant = 35; + + const coneShape = new ConeShape(); + coneShape.angle = 0.96; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].color.set(255 / 255, 127 / 255, 4 / 255, 1.0); + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 123 / 255, 0 / 255, 1.0); + gradient.addColorKey(0.157, new Color(1, 1, 1, 1)); + gradient.addColorKey(0.573, new Color(255 / 255, 255 / 255, 137 / 255, 1)); + gradient.alphaKeys[1].time = 0.089; + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 0.529; + curve.addKey(0.074, 0.428 + 0.2); + curve.addKey(0.718, 0.957 + 0.03); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(6, 6); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.mode = ParticleCurveMode.TwoCurves; + frameOverTime.curveMin = new ParticleCurve( + new CurveKey(0, 0.47), + new CurveKey(1, 1) + ); + + return particleEntity; +} + +function createFireGlowParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireGlow"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 1; + + const generator = particleRenderer.generator; + const { main, emission, sizeOverLifetime, colorOverLifetime } = generator; + + // Main module + const { startLifetime, startSpeed, startRotationZ } = main; + startLifetime.constantMin = 0.2; + startLifetime.constantMax = 0.6; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + startSpeed.constantMin = 0.0; + startSpeed.constantMax = 1.4; + startSpeed.mode = ParticleCurveMode.TwoConstants; + + main.startSize.constant = 1.2; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 100 / 255, + 0 / 255, + 168 / 255 + ); + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 20; + + const coneShape = new ConeShape(); + coneShape.angle = 15; + coneShape.radius = 0.01; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[1].time = 0.998; + colorKeys[1].color.set(255 / 255, 50 / 255, 0 / 255, 1.0); + + gradient.alphaKeys[0].alpha = 0; + gradient.alphaKeys[1].alpha = 0; + + gradient.addAlphaKey(0.057, 247 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.153; + keys[1].value = 1.0; + curve.addKey(0.057, 0.37); + curve.addKey(0.728, 0.958); +} + +function createFireSmokeParticle(fireEntity: Entity, texture: Texture2D): void { + const particleEntity = fireEntity.createChild("FireSmoke"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 0; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + textureSheetAnimation, + } = generator; + + // Main module + const { startLifetime, startRotationZ } = main; + startLifetime.constantMin = 1; + startLifetime.constantMax = 1.2; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 1.5; + + main.startSize.constant = 1.2; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.startColor.constant = new Color( + 255 / 255, + 255 / 255, + 255 / 255, + 84 / 255 + ); + + main.gravityModifier.constant = -0.05; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 25; + + const coneShape = new ConeShape(); + coneShape.angle = 10; + coneShape.radius = 0.1; + emission.shape = coneShape; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.Gradient; + + const gradient = colorOverLifetime.color.gradient; + const colorKeys = gradient.colorKeys; + colorKeys[0].time = 0; + colorKeys[0].color.set(255 / 255, 98 / 255, 0 / 255, 1.0); + colorKeys[1].time = 0.679; + colorKeys[1].color.set(0, 0, 0, 1.0); + gradient.addColorKey(0.515, new Color(255 / 255, 98 / 255, 0 / 255, 1.0)); + + const alphaKeys = gradient.alphaKeys; + alphaKeys[0].alpha = 0; + alphaKeys[1].alpha = 0; + gradient.addAlphaKey(0.121, 1); + gradient.addAlphaKey(0.329, 200 / 255); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + + const curve = sizeOverLifetime.size.curve; + const keys = curve.keys; + keys[0].value = 0.28; + keys[1].value = 1.0; + curve.addKey(0.607, 0.909); + + // Texture sheet animation module + textureSheetAnimation.enabled = true; + textureSheetAnimation.tiling = new Vector2(8, 8); + const frameOverTime = textureSheetAnimation.frameOverTime; + frameOverTime.curveMax.keys[1].value = 0.382; +} + +function createFireEmbersParticle( + fireEntity: Entity, + texture: Texture2D +): void { + const particleEntity = fireEntity.createChild("FireEmbers"); + + const particleRenderer = particleEntity.addComponent(ParticleRenderer); + + const material = new ParticleMaterial(fireEntity.engine); + material.blendMode = BlendMode.Additive; + material.baseTexture = texture; + particleRenderer.setMaterial(material); + particleRenderer.priority = 3; + + const generator = particleRenderer.generator; + const { + main, + emission, + sizeOverLifetime, + colorOverLifetime, + velocityOverLifetime, + rotationOverLifetime, + } = generator; + + // Main module + const { startLifetime, startSize, startRotationZ } = main; + main.duration = 3; + + startLifetime.constantMin = 1; + startLifetime.constantMax = 1.5; + startLifetime.mode = ParticleCurveMode.TwoConstants; + + main.startSpeed.constant = 0.4; + + startSize.constantMin = 0.05; + startSize.constantMax = 0.2; + startSize.mode = ParticleCurveMode.TwoConstants; + + startRotationZ.constantMin = 0; + startRotationZ.constantMax = 360; + startRotationZ.mode = ParticleCurveMode.TwoConstants; + + main.gravityModifier.constant = -0.15; + + main.simulationSpace = ParticleSimulationSpace.World; + + main.scalingMode = ParticleScaleMode.Hierarchy; + + // Emission module + emission.rateOverTime.constant = 65; + emission.addBurst(new Burst(0, new ParticleCompositeCurve(15))); + + const sphereShape = new SphereShape(); + sphereShape.radius = 0.01; + emission.shape = sphereShape; + + // Velocity over lifetime module + velocityOverLifetime.enabled = true; + velocityOverLifetime.velocityX.constantMin = -0.1; + velocityOverLifetime.velocityX.constantMax = 0.1; + velocityOverLifetime.velocityX.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityY.constantMin = -0.1; + velocityOverLifetime.velocityY.constantMax = 0.1; + velocityOverLifetime.velocityY.mode = ParticleCurveMode.TwoConstants; + + velocityOverLifetime.velocityZ.constantMin = -0.1; + velocityOverLifetime.velocityZ.constantMax = 0.1; + velocityOverLifetime.velocityZ.mode = ParticleCurveMode.TwoConstants; + + // Color over lifetime module + colorOverLifetime.enabled = true; + colorOverLifetime.color.mode = ParticleGradientMode.TwoGradients; + + const gradientMax = colorOverLifetime.color.gradientMax; + const maxColorKeys = gradientMax.colorKeys; + maxColorKeys[0].time = 0.315; + maxColorKeys[1].time = 0.998; + maxColorKeys[1].color.set(255 / 255, 92 / 255, 0, 1.0); + gradientMax.addColorKey(0.71, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + + const gradientMin = colorOverLifetime.color.gradientMin; + gradientMin.addColorKey(0.0, new Color(1.0, 1.0, 1.0, 1.0)); + gradientMin.addColorKey(0.486, new Color(255 / 255, 203 / 255, 0 / 255, 1.0)); + gradientMin.addColorKey(1.0, new Color(255 / 255, 94 / 255, 0 / 255, 1.0)); + + gradientMin.addAlphaKey(0.0, 1); + gradientMin.addAlphaKey(0.229, 1); + gradientMin.addAlphaKey(0.621, 0); + gradientMin.addAlphaKey(0.659, 1); + + // Size over lifetime module + sizeOverLifetime.enabled = true; + const curve = sizeOverLifetime.size.curve; + sizeOverLifetime.size.mode = ParticleCurveMode.Curve; + curve.keys[0].value = 1; + curve.keys[1].value = 0; + + // Rotation over lifetime module + rotationOverLifetime.enabled = true; + rotationOverLifetime.rotationZ.mode = ParticleCurveMode.TwoConstants; + rotationOverLifetime.rotationZ.constantMin = 90; + rotationOverLifetime.rotationZ.constantMax = 360; + + // Renderer + particleRenderer.pivot = new Vector3(0.2, 0.2, 0); +} + +class FireMoveScript extends Script { + radius: number = 0.8; + angle: number = 0; + + onUpdate(deltaTime: number): void { + if (this.engine.inputManager.isPointerHeldDown(PointerButton.Primary)) { + this.angle -= deltaTime * 6.0; + const x = Math.cos(this.angle) * this.radius; + const y = Math.sin(this.angle) * this.radius; + this.entity.transform.setPosition(x, 0, 0); + } + } +} diff --git a/examples/pbr-anisotropy.ts b/examples/pbr-anisotropy.ts new file mode 100644 index 000000000..d7bd96148 --- /dev/null +++ b/examples/pbr-anisotropy.ts @@ -0,0 +1,88 @@ +/** + * @title PBR Anisotropy + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*AGD9SLdc8JIAAAAAAAAAAAAADiR2AQ/original + */ +import { + AmbientLight, + Animator, + AssetType, + Camera, + DirectLight, + GLTFResource, + PBRMaterial, + Texture2D, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +const gui = new dat.GUI(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0.35, 0.5); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(OrbitControl); + + const lightEntity = rootEntity.createChild("light"); + const light = lightEntity.addComponent(DirectLight); + lightEntity.transform.setRotation(-50, 180, 0); + light.color.set(0.1, 0, 0, 1); + + Promise.all([ + engine.resourceManager.load({ + type: AssetType.Env, + url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*4zvFQaMWvOsAAAAAAAAAAAAADkp5AQ/ambient.bin" + }), + engine.resourceManager.load({ + type: AssetType.Texture2D, + url: "https://mdn.alipayobjects.com/huamei_dmxymu/afts/img/A*ZDwpRbRVDVwAAAAAAAAAAAAADuuHAQ/original" + }), + engine.resourceManager.load({ + type: AssetType.GLTF, + url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*Quc2T4zf_5YAAAAAAAAAAAAADkp5AQ/anisotropic_record_test.glb" + }) + ]).then(([ambientLight, texture, glTF]) => { + ambientLight.specularIntensity = 3; + ambientLight.diffuseIntensity = 3; + scene.ambientLight = ambientLight; + + const { defaultSceneRoot, materials, animations } = glTF; + rootEntity.addChild(defaultSceneRoot); + const material = materials![0] as PBRMaterial; + const animator = defaultSceneRoot.getComponent(Animator)!; + const debugInfo = { + rotate: true, + texture: true + }; + + animator.play(animations![0].name); + animator.speed = 0.2; + material.anisotropy = 1; + material.anisotropyRotation = 45; // [1,1] + material.anisotropyTexture = texture; + + gui.add(material, "anisotropy", -1, 1, 0.01); + gui.add(material, "anisotropyRotation", -180, 180, 0.01); + + gui.add(debugInfo, "rotate").onChange((v) => { + animator.speed = v ? 1 : 0; + }); + + gui.add(debugInfo, "texture").onChange((v) => { + if (v) { + material.anisotropyTexture = texture; + } else { + material.anisotropyTexture = null; + } + }); + + engine.run(); + }); +}); diff --git a/examples/pbr-base.ts b/examples/pbr-base.ts new file mode 100644 index 000000000..fcf46047e --- /dev/null +++ b/examples/pbr-base.ts @@ -0,0 +1,80 @@ +/** + * @title PBR Base + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*kXDCQpieYEEAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AmbientLight, + AssetType, + BackgroundMode, + Camera, + DirectLight, + GLTFResource, + PrimitiveMesh, + SkyBoxMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const gui = new dat.GUI(); + + const directLightNode = rootEntity.createChild("dir_light"); + const directLight = directLightNode.addComponent(DirectLight); + const dirFolder = gui.addFolder("DirectionalLight1"); + directLight.intensity = 0.5; + dirFolder.add(directLight, "enabled"); + dirFolder.add(directLight, "intensity", 0, 1); + directLightNode.transform.setPosition(5, 5, 5); + directLightNode.transform.lookAt(new Vector3(0, 0, 0)); + + //Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.position = new Vector3(0.25, 0.5, 1.5); + cameraNode.addComponent(Camera); + const control = cameraNode.addComponent(OrbitControl); + control.target.set(0.25, 0.25, 0); + + // Create sky + const sky = scene.background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + scene.background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + Promise.all([ + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/477b0093-7ee8-41af-a0dd-836608a4f130.gltf" + ) + .then((gltf) => { + const { defaultSceneRoot } = gltf; + rootEntity.addChild(defaultSceneRoot); + defaultSceneRoot.transform.setScale(100, 100, 100); + }), + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + + const envFolder = gui.addFolder("EnvironmentMapLight"); + envFolder.add(ambientLight, "specularIntensity", 0, 1); + envFolder.add(ambientLight, "diffuseIntensity", 0, 1); + }), + ]).then(() => { + engine.run(); + }); +}); diff --git a/examples/pbr-clearcoat.ts b/examples/pbr-clearcoat.ts new file mode 100644 index 000000000..a193e0c16 --- /dev/null +++ b/examples/pbr-clearcoat.ts @@ -0,0 +1,69 @@ +/** + * @title PBR Clearcoat + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*8ws-SZJCuvoAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AmbientLight, + AssetType, + BackgroundMode, + Camera, + DirectLight, + GLTFResource, + Logger, + PrimitiveMesh, + SkyBoxMaterial, + WebGLEngine, +} from "@galacean/engine"; +Logger.enable(); +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const { background } = scene; + const rootEntity = scene.createRootEntity(); + + const directLightNode = rootEntity.createChild("dir_light"); + directLightNode.addComponent(DirectLight); + directLightNode.transform.setRotation(30, 0, 0); + + //Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 0, 15); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + // Create sky + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + background.mode = BackgroundMode.Sky; + + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + Promise.all([ + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/16875768-21cf-481f-b05f-454c17866ba0.glb" + ) + .then((gltf) => { + const { defaultSceneRoot } = gltf; + const entity = rootEntity.createChild(); + entity.addChild(defaultSceneRoot); + }), + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + }), + ]).then(() => { + engine.run(); + }); +}); diff --git a/examples/pbr-helmet.ts b/examples/pbr-helmet.ts new file mode 100644 index 000000000..93a2e87cc --- /dev/null +++ b/examples/pbr-helmet.ts @@ -0,0 +1,71 @@ +/** + * @title PBR Helmet + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*qbWBT62EnaAAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AmbientLight, + AssetType, + BackgroundMode, + Camera, + DirectLight, + GLTFResource, + Logger, + PrimitiveMesh, + SkyBoxMaterial, + WebGLEngine, +} from "@galacean/engine"; +Logger.enable(); +// Create engine object +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const { ambientLight, background } = scene; + const rootEntity = scene.createRootEntity(); + + const directLightNode = rootEntity.createChild("dir_light"); + const directLightNode2 = rootEntity.createChild("dir_light2"); + directLightNode.addComponent(DirectLight); + directLightNode2.addComponent(DirectLight); + directLightNode.transform.setRotation(30, 0, 0); + directLightNode2.transform.setRotation(-30, 180, 0); + + //Create camera + const cameraNode = rootEntity.createChild("camera_node"); + cameraNode.transform.setPosition(0, 0, 5); + cameraNode.addComponent(Camera); + cameraNode.addComponent(OrbitControl); + + // Create sky + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + background.mode = BackgroundMode.Sky; + + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + Promise.all([ + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf" + ) + .then((gltf) => { + const entity = rootEntity.createChild(""); + entity.addChild(gltf.defaultSceneRoot); + }), + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/f369110c-0e33-47eb-8296-756e9c80f254.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + }), + ]).then(() => { + engine.run(); + }); +}); diff --git a/examples/physics-debug-draw.ts b/examples/physics-debug-draw.ts new file mode 100644 index 000000000..77956bcc8 --- /dev/null +++ b/examples/physics-debug-draw.ts @@ -0,0 +1,158 @@ +/** + * @title Physics Debug Draw + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*PtfoQre36loAAAAAAAAAAAAADiR2AQ/original + */ + +import { + WebGLEngine, + SphereColliderShape, + DynamicCollider, + BoxColliderShape, + Vector3, + MeshRenderer, + PointLight, + PrimitiveMesh, + Camera, + Script, + StaticCollider, + ColliderShape, + PBRMaterial, + AmbientLight, + AssetType, + BlinnPhongMaterial, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { WireframeManager } from "@galacean/engine-toolkit-auxiliary-lines"; + +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.addComponent(OrbitControl); + + // init point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 3, 0); + light.addComponent(PointLight); + + // create box test entity + const cubeSize = 2.0; + const boxEntity = rootEntity.createChild("BoxEntity"); + + const boxMtl = new PBRMaterial(engine); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxMtl.baseColor.set(0.6, 0.3, 0.3, 1.0); + boxMtl.roughness = 0.5; + boxMtl.metallic = 0.0; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + engine, + cubeSize, + cubeSize, + cubeSize + ); + boxRenderer.setMaterial(boxMtl); + + const physicsBox = new BoxColliderShape(); + physicsBox.size = new Vector3(cubeSize, cubeSize, cubeSize); + physicsBox.material.staticFriction = 0.1; + physicsBox.material.dynamicFriction = 0.2; + physicsBox.material.bounciness = 1; + physicsBox.isTrigger = true; + + const boxCollider = boxEntity.addComponent(StaticCollider); + boxCollider.addShape(physicsBox); + + // create sphere test entity + const radius = 1.25; + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(-2, 0, 0); + + const sphereMtl = new PBRMaterial(engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + sphereMtl.metallic = 0.0; + sphereMtl.roughness = 0.5; + sphereRenderer.mesh = PrimitiveMesh.createSphere(engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + physicsSphere.material.staticFriction = 0.1; + physicsSphere.material.dynamicFriction = 0.2; + physicsSphere.material.bounciness = 1; + + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.isKinematic = true; + sphereCollider.addShape(physicsSphere); + + rootEntity.addComponent(MeshRenderer); + const wireframe = rootEntity.addComponent(WireframeManager); // debug draw + wireframe.addEntityWireframe(sphereEntity); + wireframe.addEntityWireframe(boxEntity); + + class MoveScript extends Script { + pos: number = -5; + vel: number = 0.05; + velSign: number = -1; + + onPhysicsUpdate() { + if (this.pos >= 5) { + this.velSign = -1; + } + if (this.pos <= -5) { + this.velSign = 1; + } + this.pos += this.vel * this.velSign; + this.entity.transform.worldPosition.set(this.pos, 0, 0); + } + } + + // Collision Detection + class CollisionScript extends Script { + onTriggerExit(other: ColliderShape) { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onTriggerEnter(other: ColliderShape) { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onTriggerStay(other: ColliderShape) {} + } + + sphereEntity.addComponent(CollisionScript); + sphereEntity.addComponent(MoveScript); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); + } +); diff --git a/examples/physx-attractor.ts b/examples/physx-attractor.ts new file mode 100644 index 000000000..c898888f1 --- /dev/null +++ b/examples/physx-attractor.ts @@ -0,0 +1,229 @@ +/** + * @title PhysX Attractor + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*CERrSaAWYaQAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + Camera, + Color, + DirectLight, + DynamicCollider, + Entity, + Font, + Layer, + MathUtil, + MeshRenderer, + PBRMaterial, + PlaneColliderShape, + PrimitiveMesh, + Quaternion, + Ray, + RenderFace, + Script, + ShadowType, + SphereColliderShape, + StaticCollider, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +class Attractor extends Script { + private collider: DynamicCollider; + private force: Vector3 = new Vector3(); + + onAwake() { + this.collider = this.entity.getComponent(DynamicCollider); + } + + onPhysicsUpdate() { + this.force.copyFrom(this.entity.transform.worldPosition); + this.collider.applyForce(this.force.normalize().scale(-10)); + } +} + +class Interactor extends Script { + ray = new Ray(); + position = new Vector3(); + rotation = new Quaternion(); + camera: Camera; + + onAwake() { + this.camera = this.entity.getComponent(Camera); + } + + onUpdate(deltaTime: number) { + const ray = this.ray; + const { pointers } = this.engine.inputManager; + if (pointers && pointers.length > 0) { + const pointer = pointers[0].position; + this.camera.screenPointToRay(pointer, ray); + const position = this.entity.transform.position; + position.copyFrom(ray.origin); + position.add(ray.direction.scale(18)); + } + } +} + +// init scene +function init(rootEntity: Entity) { + addPlane(rootEntity, new Vector3(0, -8, 0), new Quaternion()); + const quat180 = new Quaternion(); + quat180.rotateZ(MathUtil.degreeToRadian(180)); + addPlane(rootEntity, new Vector3(0, 8, 0), quat180); + + const quat90 = new Quaternion(); + quat90.rotateZ(MathUtil.degreeToRadian(90)); + addPlane(rootEntity, new Vector3(10, 0, 0), quat90); + + const quatNega90 = new Quaternion(); + quatNega90.rotateZ(MathUtil.degreeToRadian(-90)); + addPlane(rootEntity, new Vector3(-10, 0, 0), quatNega90); + + const quatFront90 = new Quaternion(); + quatFront90.rotateX(MathUtil.degreeToRadian(-90)); + addPlane(rootEntity, new Vector3(0, 0, 10), quatFront90); + + const quatNegaFront90 = new Quaternion(); + quatNegaFront90.rotateX(MathUtil.degreeToRadian(90)); + addPlane(rootEntity, new Vector3(0, 0, 0), quatNegaFront90); + + const quat = new Quaternion(0, 0, 0.3, 0.7); + quat.normalize(); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + for (let k = 0; k < 4; k++) { + addSphere( + rootEntity, + 1, + new Vector3(-4 + 2 * i, -4 + 2 * j, -4 + 2 * k), + quat + ); + } + } + } +} + +function addPlane( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set( + 0.03179807202597362, + 0.3939682161541871, + 0.41177952549087604, + 1 + ); + mtl.renderFace = RenderFace.Double; + const planeEntity = rootEntity.createChild(); + planeEntity.layer = Layer.Layer1; + + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(rootEntity.engine, 10, 10); + // renderer.setMaterial(mtl); + planeEntity.transform.position = position; + planeEntity.transform.rotationQuaternion = rotation; + + const physicsPlane = new PlaneColliderShape(); + const planeCollider = planeEntity.addComponent(StaticCollider); + planeCollider.addShape(physicsPlane); + + return planeEntity; +} + +function addSphere( + rootEntity: Entity, + radius: number, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(1.0, 168 / 255, 196 / 255, 1.0); + mtl.roughness = 0.8; + mtl.metallic = 0.4; + + const sphereEntity = rootEntity.createChild(); + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius, 60); + renderer.setMaterial(mtl); + sphereEntity.transform.position = position; + sphereEntity.transform.rotationQuaternion = rotation; + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(physicsSphere); + sphereCollider.linearDamping = 0.95; + sphereCollider.angularDamping = 0.2; + sphereEntity.addComponent(Attractor); + return sphereEntity; +} + +//-------------------------------------------------------------------------------------------------------------------- +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + scene.shadowDistance = 20; + const rootEntity = scene.createRootEntity(); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + const pos = cameraEntity.transform.position; + pos.set(0, 0, -15); + cameraEntity.transform.position = pos; + cameraEntity.transform.lookAt(new Vector3()); + + const entity = cameraEntity.createChild("text"); + entity.transform.position = new Vector3(0, 3.5, -10); + const renderer = entity.addComponent(TextRenderer); + renderer.color = new Color(); + renderer.text = "Use mouse to interact with spheres"; + renderer.font = Font.createFromOS(entity.engine, "Arial"); + renderer.fontSize = 40; + + const light = rootEntity.createChild("light"); + light.transform.setPosition(5, 0, -10); + light.transform.lookAt(new Vector3(0, 0, 0)); + const p = light.addComponent(DirectLight); + p.shadowType = ShadowType.SoftLow; + + { + const attractorEntity = rootEntity.createChild(); + attractorEntity.addComponent(Interactor).camera = camera; + const mtl = new PBRMaterial(engine); + mtl.baseColor.set(1, 1, 1, 1.0); + const renderer = attractorEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 2); + // renderer.setMaterial(mtl); + + const attractorSphere = new SphereColliderShape(); + attractorSphere.radius = 2; + const attractorCollider = attractorEntity.addComponent(DynamicCollider); + attractorCollider.isKinematic = true; + attractorCollider.addShape(attractorSphere); + } + + engine.physicsManager.gravity = new Vector3(); + init(rootEntity); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + + engine.run(); + }); + } +); diff --git a/examples/physx-collision-detection.ts b/examples/physx-collision-detection.ts new file mode 100644 index 000000000..a9101a17b --- /dev/null +++ b/examples/physx-collision-detection.ts @@ -0,0 +1,151 @@ +/** + * @title PhysX Collision Detection + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*bLqoQJtljk0AAAAAAAAAAAAADiR2AQ/original + */ + +import { + WebGLEngine, + SphereColliderShape, + DynamicCollider, + BoxColliderShape, + Vector3, + MeshRenderer, + PointLight, + PrimitiveMesh, + Camera, + Script, + StaticCollider, + ColliderShape, + PBRMaterial, + AmbientLight, + AssetType, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.addComponent(OrbitControl); + + // init point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 3, 0); + light.addComponent(PointLight); + + // create box test entity + const cubeSize = 2.0; + const boxEntity = rootEntity.createChild("BoxEntity"); + + const boxMtl = new PBRMaterial(engine); + const boxRenderer = boxEntity.addComponent(MeshRenderer); + boxMtl.baseColor.set(0.6, 0.3, 0.3, 1.0); + boxMtl.metallic = 0.0; + boxMtl.roughness = 0.5; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + engine, + cubeSize, + cubeSize, + cubeSize + ); + boxRenderer.setMaterial(boxMtl); + + const physicsBox = new BoxColliderShape(); + physicsBox.size = new Vector3(cubeSize, cubeSize, cubeSize); + physicsBox.material.staticFriction = 0.1; + physicsBox.material.dynamicFriction = 0.2; + physicsBox.material.bounciness = 1; + physicsBox.isTrigger = true; + + const boxCollider = boxEntity.addComponent(StaticCollider); + boxCollider.addShape(physicsBox); + + // create sphere test entity + const radius = 1.25; + const sphereEntity = rootEntity.createChild("SphereEntity"); + sphereEntity.transform.setPosition(-2, 0, 0); + + const sphereMtl = new PBRMaterial(engine); + const sphereRenderer = sphereEntity.addComponent(MeshRenderer); + sphereMtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + sphereMtl.metallic = 0.0; + sphereMtl.roughness = 0.5; + sphereRenderer.mesh = PrimitiveMesh.createSphere(engine, radius); + sphereRenderer.setMaterial(sphereMtl); + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + physicsSphere.material.staticFriction = 0.1; + physicsSphere.material.dynamicFriction = 0.2; + physicsSphere.material.bounciness = 1; + // sphereEntity.transform.setScale(3,3,3); + + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.isKinematic = true; + sphereCollider.addShape(physicsSphere); + + class MoveScript extends Script { + pos: number = -5; + vel: number = 0.05; + velSign: number = -1; + + onPhysicsUpdate() { + if (this.pos >= 5) { + this.velSign = -1; + } + if (this.pos <= -5) { + this.velSign = 1; + } + this.pos += this.vel * this.velSign; + this.entity.transform.worldPosition.set(this.pos, 0, 0); + } + } + + // Collision Detection + class CollisionScript extends Script { + onTriggerExit(other: ColliderShape) { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onTriggerEnter(other: ColliderShape) { + (sphereRenderer.getMaterial()).baseColor.set( + Math.random(), + Math.random(), + Math.random(), + 1.0 + ); + } + + onTriggerStay(other: ColliderShape) {} + } + + sphereEntity.addComponent(CollisionScript); + sphereEntity.addComponent(MoveScript); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); + } +); diff --git a/examples/physx-compound.ts b/examples/physx-compound.ts new file mode 100644 index 000000000..9925400b0 --- /dev/null +++ b/examples/physx-compound.ts @@ -0,0 +1,200 @@ +/** + * @title PhysX Compound + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*CnfERJy_GEgAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { PhysXPhysics, PhysXRuntimeMode } from "@galacean/engine-physics-physx"; +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + DirectLight, + DynamicCollider, + Entity, + MeshRenderer, + PBRMaterial, + PlaneColliderShape, + PrimitiveMesh, + Quaternion, + Script, + ShadowType, + StaticCollider, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +class TableGenerator extends Script { + private _totalTime: number = 0; + + onUpdate(deltaTime: number): void { + this._totalTime += deltaTime; + if (this._totalTime > 0.3) { + this._addTable(); + this._totalTime = 0; + } + } + + private _addTable(): void { + const entity = this.entity.createChild("entity"); + entity.transform.setPosition( + Math.random() * 16 - 8, + 10, + Math.random() * 16 - 8 + ); + entity.transform.setRotation( + Math.random() * 360, + Math.random() * 360, + Math.random() * 360 + ); + entity.transform.setScale(3, 3, 3); + const boxCollider = entity.addComponent(DynamicCollider); + boxCollider.mass = 10.0; + + const boxMaterial = new PBRMaterial(this.engine); + boxMaterial.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + boxMaterial.metallic = 0; + boxMaterial.roughness = 0.5; + { + const physicsBox = new BoxColliderShape(); + physicsBox.size = new Vector3(0.5, 0.4, 0.045); + physicsBox.position.set(0, 0, 0.125); + boxCollider.addShape(physicsBox); + const child = entity.createChild(); + child.transform.setPosition(0, 0, 0.125); + const boxRenderer = child.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid( + this.engine, + 0.5, + 0.4, + 0.045 + ); + boxRenderer.setMaterial(boxMaterial); + } + + { + const physicsBox1 = new BoxColliderShape(); + physicsBox1.size = new Vector3(0.1, 0.1, 0.3); + physicsBox1.position.set(-0.2, -0.15, -0.045); + boxCollider.addShape(physicsBox1); + const child = entity.createChild(); + child.transform.setPosition(-0.2, -0.15, -0.045); + const boxRenderer = child.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid(this.engine, 0.1, 0.1, 0.3); + boxRenderer.setMaterial(boxMaterial); + } + + { + const physicsBox2 = new BoxColliderShape(); + physicsBox2.size = new Vector3(0.1, 0.1, 0.3); + physicsBox2.position.set(0.2, -0.15, -0.045); + boxCollider.addShape(physicsBox2); + const child = entity.createChild(); + child.transform.setPosition(0.2, -0.15, -0.045); + const boxRenderer = child.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid(this.engine, 0.1, 0.1, 0.3); + boxRenderer.setMaterial(boxMaterial); + } + + { + const physicsBox3 = new BoxColliderShape(); + physicsBox3.size = new Vector3(0.1, 0.1, 0.3); + physicsBox3.position.set(-0.2, 0.15, -0.045); + boxCollider.addShape(physicsBox3); + const child = entity.createChild(); + child.transform.setPosition(-0.2, 0.15, -0.045); + const boxRenderer = child.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid(this.engine, 0.1, 0.1, 0.3); + boxRenderer.setMaterial(boxMaterial); + } + + { + const physicsBox4 = new BoxColliderShape(); + physicsBox4.size = new Vector3(0.1, 0.1, 0.3); + physicsBox4.position.set(0.2, 0.15, -0.045); + boxCollider.addShape(physicsBox4); + const child = entity.createChild(); + child.transform.setPosition(0.2, 0.15, -0.045); + const boxRenderer = child.addComponent(MeshRenderer); + boxRenderer.mesh = PrimitiveMesh.createCuboid(this.engine, 0.1, 0.1, 0.3); + boxRenderer.setMaterial(boxMaterial); + } + } +} + +function addPlane( + rootEntity: Entity, + size: Vector2, + position: Vector3, + rotation: Quaternion +): Entity { + const engine = rootEntity.engine; + const material = new PBRMaterial(engine); + material.baseColor.set( + 0.2179807202597362, + 0.2939682161541871, + 0.31177952549087604, + 1 + ); + material.roughness = 0.0; + material.metallic = 0.0; + + const entity = rootEntity.createChild(); + const renderer = entity.addComponent(MeshRenderer); + entity.transform.position = position; + entity.transform.rotationQuaternion = rotation; + renderer.mesh = PrimitiveMesh.createPlane(engine, size.x, size.y); + renderer.setMaterial(material); + + const physicsPlane = new PlaneColliderShape(); + const planeCollider = entity.addComponent(StaticCollider); + planeCollider.addShape(physicsPlane); + + return entity; +} + +//-------------------------------------------------------------------------------------------------------------------- + +async function main() { + const engine = await WebGLEngine.create({ + canvas: "canvas", + physics: new PhysXPhysics(PhysXRuntimeMode.Auto), + }); + + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + scene.ambientLight.diffuseSolidColor.set(0.5, 0.5, 0.5, 1); + scene.shadowDistance = 30; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(10, 10, 10); + cameraEntity.addComponent(OrbitControl); + + // init directional light + const light = rootEntity.createChild("light"); + light.transform.setPosition(-0.3, 1, 0.4); + light.transform.lookAt(new Vector3(0, 0, 0)); + const directLight = light.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + addPlane(rootEntity, new Vector2(30, 30), new Vector3(), new Quaternion()); + rootEntity.addComponent(TableGenerator); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); +} + +main(); diff --git a/examples/physx-controller.ts b/examples/physx-controller.ts new file mode 100644 index 000000000..2ed210a41 --- /dev/null +++ b/examples/physx-controller.ts @@ -0,0 +1,562 @@ +/** + * @title PhysX Character Controller + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*guHUSbk6THIAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { + AmbientLight, + AnimationClip, + Animator, + AnimatorStateMachine, + AssetType, + BackgroundMode, + BoxColliderShape, + Camera, + CapsuleColliderShape, + CharacterController, + Color, + ControllerCollisionFlag, + DirectLight, + Engine, + Entity, + Font, + GLTFResource, + Keys, + Logger, + Material, + Matrix, + MeshRenderer, + PBRMaterial, + PlaneColliderShape, + PrimitiveMesh, + Quaternion, + RenderFace, + Script, + ShadowType, + SkyBoxMaterial, + StaticCollider, + TextRenderer, + Texture2D, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +Logger.enable(); + +enum State { + Run = "Run", + Idle = "Idle", + Jump = "Jump_In", + Fall = "Fall", + Landing = "Landing", +} + +class AnimationState { + private _state: State = State.Idle; + private _lastKey: Keys = null; + + get state(): State { + return this._state; + } + + setMoveKey(value: Keys) { + this._lastKey = value; + if (this._state === State.Fall || this._state === State.Jump) { + return; + } + + if ( + this._lastKey === null && + (this._state === State.Run || this._state === State.Idle) + ) { + this._state = State.Idle; + } else { + this._state = State.Run; + } + } + + setJumpKey() { + this._state = State.Jump; + } + + setFallKey() { + this._state = State.Fall; + } + + setIdleKey() { + if (this._state == State.Jump) { + return; + } + + if (this._state === State.Fall) { + this._state = State.Landing; + } + + if (this._state === State.Landing) { + this._state = State.Idle; + } + } +} + +class ControllerScript extends Script { + _camera: Entity; + _character: Entity; + _controller: CharacterController; + _animator: Animator; + + _displacement = new Vector3(); + _forward = new Vector3(); + _cross = new Vector3(); + _lastKey = true; + + _predictPosition = new Vector3(); + _rotMat = new Matrix(); + _rotation = new Quaternion(); + _newRotation = new Quaternion(); + _yAxisMove = new Vector3(); + _up = new Vector3(0, 1, 0); + + _animationState = new AnimationState(); + _animationName: State; + _fallAccumulateTime = 0; + + onAwake() { + this._controller = this.entity.getComponent(CharacterController); + } + + targetCamera(camera: Entity) { + this._camera = camera; + } + + targetCharacter(character: Entity) { + this._character = character; + this._animator = character.getComponent(Animator); + } + + onUpdate(deltaTime: number) { + const inputManager = this.engine.inputManager; + if (inputManager.isKeyHeldDown()) { + this._forward.copyFrom(this._camera.transform.worldForward); + this._forward.y = 0; + this._forward.normalize(); + this._cross.set(this._forward.z, 0, -this._forward.x); + + const animationSpeed = 0.02; + const animationState = this._animationState; + const displacement = this._displacement; + if (inputManager.isKeyHeldDown(Keys.KeyW)) { + animationState.setMoveKey(Keys.KeyW); + Vector3.scale(this._forward, animationSpeed, displacement); + } + if (inputManager.isKeyHeldDown(Keys.KeyS)) { + animationState.setMoveKey(Keys.KeyS); + Vector3.scale(this._forward, -animationSpeed, displacement); + } + if (inputManager.isKeyHeldDown(Keys.KeyA)) { + animationState.setMoveKey(Keys.KeyA); + Vector3.scale(this._cross, animationSpeed, displacement); + } + if (inputManager.isKeyHeldDown(Keys.KeyD)) { + animationState.setMoveKey(Keys.KeyD); + Vector3.scale(this._cross, -animationSpeed, displacement); + } + if (inputManager.isKeyDown(Keys.Space)) { + animationState.setJumpKey(); + displacement.set(0, 0.05, 0); + } + } else { + this._animationState.setMoveKey(null); + this._displacement.set(0, 0, 0); + } + this._playAnimation(); + } + + onPhysicsUpdate() { + const physicsManager = this.engine.physicsManager; + const gravity = physicsManager.gravity; + const fixedTimeStep = physicsManager.fixedTimeStep; + this._fallAccumulateTime += fixedTimeStep; + const character = this._controller; + character.move(this._displacement, 0.0001, fixedTimeStep); + const transform = this._character.transform; + const yAxisMove = this._yAxisMove; + + yAxisMove.set(0, gravity.y * fixedTimeStep * this._fallAccumulateTime, 0); + const flag = character.move(yAxisMove, 0.0001, fixedTimeStep); + if (flag & ControllerCollisionFlag.Down) { + this._fallAccumulateTime = 0; + this._animationState.setIdleKey(); + } else { + this._animationState.setFallKey(); + } + this._playAnimation(); + + if (this._displacement.x != 0 || this._displacement.z != 0) { + this._predictPosition.copyFrom(transform.worldPosition); + this._predictPosition.subtract(this._displacement); + Matrix.lookAt( + transform.worldPosition, + this._predictPosition, + this._up, + this._rotMat + ); + this._rotMat.getRotation(this._rotation).invert(); + const currentRot = transform.rotationQuaternion; + Quaternion.slerp(currentRot, this._rotation, 0.1, this._newRotation); + transform.rotationQuaternion = this._newRotation; + } + } + + private _playAnimation() { + if (this._animationName !== this._animationState.state) { + this._animator.crossFade(this._animationState.state, 0.1); + this._animationName = this._animationState.state; + } + } +} + +function addPlane( + rootEntity: Entity, + size: Vector2, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set( + 0.2179807202597362, + 0.2939682161541871, + 0.31177952549087604, + 1 + ); + mtl.roughness = 0.0; + mtl.metallic = 0.0; + mtl.renderFace = RenderFace.Double; + const planeEntity = rootEntity.createChild(); + + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(rootEntity.engine, size.x, size.y); + renderer.setMaterial(mtl); + planeEntity.transform.position = position; + planeEntity.transform.rotationQuaternion = rotation; + + const physicsPlane = new PlaneColliderShape(); + physicsPlane.isTrigger = false; + const planeCollider = planeEntity.addComponent(StaticCollider); + planeCollider.addShape(physicsPlane); + + return planeEntity; +} + +function addBox( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.roughness = 0.2; + mtl.metallic = 0.8; + mtl.baseColor.set(1, 1, 0, 1.0); + const boxEntity = rootEntity.createChild(); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + renderer.setMaterial(mtl); + boxEntity.transform.position = position; + boxEntity.transform.rotationQuaternion = rotation; + + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + physicsBox.isTrigger = false; + const boxCollider = boxEntity.addComponent(StaticCollider); + boxCollider.addShape(physicsBox); + + return boxEntity; +} + +function addStair( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.roughness = 0.5; + mtl.baseColor.set(0.9, 0.9, 0.9, 1.0); + const mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + + const stairEntity = rootEntity.createChild(); + stairEntity.transform.position = position; + stairEntity.transform.rotationQuaternion = rotation; + const boxCollider = stairEntity.addComponent(StaticCollider); + { + const level = stairEntity.createChild(); + const renderer = level.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(mtl); + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + boxCollider.addShape(physicsBox); + } + + { + const level = stairEntity.createChild(); + level.transform.setPosition(0, 0.3, 0.5); + const renderer = level.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(mtl); + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + physicsBox.position.set(0, 0.3, 0.5); + boxCollider.addShape(physicsBox); + } + + { + const level = stairEntity.createChild(); + level.transform.setPosition(0, 0.6, 1); + const renderer = level.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(mtl); + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + physicsBox.position.set(0, 0.6, 1); + boxCollider.addShape(physicsBox); + } + + { + const level = stairEntity.createChild(); + level.transform.setPosition(0, 0.9, 1.5); + const renderer = level.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(mtl); + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + physicsBox.position.set(0, 0.9, 1.5); + boxCollider.addShape(physicsBox); + } + return stairEntity; +} + +function textureAndAnimationLoader( + engine: Engine, + materials: Material[], + animator: Animator, + animatorStateMachine: AnimatorStateMachine +) { + engine.resourceManager + .load( + "https://gw.alipayobjects.com/zos/OasisHub/440001585/6990/T_Doggy_1_diffuse.png" + ) + .then((res) => { + for (let i = 0, n = materials.length; i < n; i++) { + const material = materials[i]; + (material).baseTexture = res; + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/zos/OasisHub/440001585/3072/T_Doggy_normal.png" + ) + .then((res) => { + for (let i = 0, n = materials.length; i < n; i++) { + const material = materials[i]; + (material).normalTexture = res; + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/zos/OasisHub/440001585/5917/T_Doggy_roughness.png" + ) + .then((res) => { + for (let i = 0, n = materials.length; i < n; i++) { + const material = materials[i]; + (material).roughnessMetallicTexture = res; + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/zos/OasisHub/440001585/2547/T_Doggy_1_ao.png" + ) + .then((res) => { + for (let i = 0, n = materials.length; i < n; i++) { + const material = materials[i]; + (material).occlusionTexture = res; + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/7205/Anim_Run.gltf" + ) + .then((res) => { + const animations = res.animations; + if (animations) { + animations.forEach((clip: AnimationClip) => { + const animatorState = animatorStateMachine.addState(clip.name); + animatorState.clip = clip; + }); + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/3380/Anim_Idle.gltf" + ) + .then((res) => { + const animations = res.animations; + if (animations) { + animations.forEach((clip: AnimationClip) => { + const animatorState = animatorStateMachine.addState(clip.name); + animatorState.clip = clip; + }); + animator.play(State.Idle); + engine.run(); + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/5703/Anim_Landing.gltf" + ) + .then((res) => { + const animations = res.animations; + if (animations) { + animations.forEach((clip: AnimationClip) => { + const animatorState = animatorStateMachine.addState(clip.name); + animatorState.clip = clip; + }); + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/3275/Anim_Fall.gltf" + ) + .then((res) => { + const animations = res.animations; + if (animations) { + animations.forEach((clip: AnimationClip) => { + const animatorState = animatorStateMachine.addState(clip.name); + animatorState.clip = clip; + }); + } + }); + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/2749/Anim_Jump_In.gltf" + ) + .then((res) => { + const animations = res.animations; + if (animations) { + animations.forEach((clip: AnimationClip) => { + const animatorState = animatorStateMachine.addState(clip.name); + animatorState.clip = clip; + }); + } + }); +} + +//---------------------------------------------------------------------------------------------------------------------- +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.shadowDistance = 10; + const { background } = scene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position.set(4, 4, -4); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.transform.setPosition(8, 10, 10); + lightNode.transform.lookAt(new Vector3(0, 0, 0)); + const directLight = lightNode.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + const entity = cameraEntity.createChild("text"); + entity.transform.position = new Vector3(0, 3.5, -10); + const renderer = entity.addComponent(TextRenderer); + renderer.color = new Color(); + renderer.text = "Use `WASD` to move character and `Space` to jump"; + renderer.font = Font.createFromOS(entity.engine, "Arial"); + renderer.fontSize = 40; + + // Create sky + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + addPlane(rootEntity, new Vector2(10, 6), new Vector3(), new Quaternion()); + const slope = new Quaternion(); + Quaternion.rotationEuler(45, 0, 0, slope); + addBox( + rootEntity, + new Vector3(4, 4, 0.01), + new Vector3(0, 0, 1), + slope.normalize() + ); + addStair( + rootEntity, + new Vector3(1, 0.3, 0.5), + new Vector3(3, 0, 1), + new Quaternion() + ); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/09904c03-0d23-4834-aa73-64e11e2287b0.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + }); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/440001585/5407/Doggy_Demo.gltf" + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + const controllerEntity = rootEntity.createChild("controller"); + controllerEntity.addChild(defaultSceneRoot); + + // animator + defaultSceneRoot.transform.setPosition(0, -0.35, 0); + const animator = defaultSceneRoot.getComponent(Animator); + + // controller + const physicsCapsule = new CapsuleColliderShape(); + physicsCapsule.radius = 0.15; + physicsCapsule.height = 0.2; + const characterController = + controllerEntity.addComponent(CharacterController); + characterController.addShape(physicsCapsule); + const userController = controllerEntity.addComponent(ControllerScript); + userController.targetCamera(cameraEntity); + userController.targetCharacter(defaultSceneRoot); + + textureAndAnimationLoader( + engine, + asset.materials, + animator, + animator.animatorController.layers[0].stateMachine + ); + }); +}); diff --git a/examples/physx-joint-basic.ts b/examples/physx-joint-basic.ts new file mode 100644 index 000000000..cef952eda --- /dev/null +++ b/examples/physx-joint-basic.ts @@ -0,0 +1,263 @@ +/** + * @title PhysX Joint Basic + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*7vrZQpbkIDEAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + Collider, + Color, + DirectLight, + DynamicCollider, + Entity, + FixedJoint, + Font, + HingeJoint, + MeshRenderer, + PBRMaterial, + PointerButton, + PrimitiveMesh, + Quaternion, + Ray, + Script, + ShadowType, + SphereColliderShape, + SpringJoint, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +function createText(rootEntity: Entity, pos: Vector3, fontSize: number): void { + // Create text entity + const entity = rootEntity.createChild("text"); + entity.transform.position = pos; + // Add text renderer for text entity + const renderer = entity.addComponent(TextRenderer); + // Set text color + renderer.color = new Color(); + // Set text to render + renderer.text = "Click Mouse to Shoot balls"; + // Set font with font family + renderer.font = Font.createFromOS(entity.engine, "Arial"); + // Set font size + renderer.fontSize = fontSize; +} + +function addBox( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.roughness = 0.5; + mtl.metallic = 0.0; + const boxEntity = rootEntity.createChild(); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + renderer.setMaterial(mtl); + boxEntity.transform.position = position; + boxEntity.transform.rotationQuaternion = rotation; + + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + const boxCollider = boxEntity.addComponent(DynamicCollider); + boxCollider.addShape(physicsBox); + + return boxEntity; +} + +function transform( + position: Vector3, + rotation: Quaternion, + outPosition: Vector3, + outRotation: Quaternion +) { + Quaternion.multiply(rotation, outRotation, outRotation); + Vector3.transformByQuat(outPosition, rotation, outPosition); + outPosition.add(position); +} + +function createChain( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion, + length: number, + separation: number +) { + const offset = new Vector3(); + let prevCollider: Collider = null; + for (let i = 0; i < length; i++) { + const localPosition = new Vector3(0, (-separation / 2) * (2 * i + 1), 0); + const localQuaternion = new Quaternion(); + transform(position, rotation, localPosition, localQuaternion); + const currentEntity = addBox( + rootEntity, + new Vector3(2.0, 2.0, 0.5), + localPosition, + localQuaternion + ); + + const currentCollider = currentEntity.getComponent(DynamicCollider); + const fixedJoint = currentEntity.addComponent(FixedJoint); + if (prevCollider !== null) { + Vector3.subtract( + currentEntity.transform.worldPosition, + prevCollider.entity.transform.worldPosition, + offset + ); + fixedJoint.connectedAnchor = offset; + fixedJoint.connectedCollider = prevCollider; + } else { + fixedJoint.connectedAnchor = position; + } + prevCollider = currentCollider; + } +} + +function createSpring( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion +) { + const currentEntity = addBox( + rootEntity, + new Vector3(2, 2, 1), + position, + rotation + ); + const springJoint = currentEntity.addComponent(SpringJoint); + springJoint.connectedAnchor = position; + springJoint.swingOffset = new Vector3(0, 1, 0); + springJoint.maxDistance = 2; + springJoint.stiffness = 10; + springJoint.damping = 1; +} + +function createHinge( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion +) { + const currentEntity = addBox( + rootEntity, + new Vector3(4.0, 4.0, 0.5), + position, + rotation + ); + const hingeJoint = currentEntity.addComponent(HingeJoint); + hingeJoint.connectedAnchor = position; + hingeJoint.swingOffset = new Vector3(0, 1, 0); + hingeJoint.axis = new Vector3(0, 1, 0); +} + +function addSphere( + rootEntity: Entity, + radius: number, + position: Vector3, + rotation: Quaternion, + velocity: Vector3 +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.roughness = 0.5; + mtl.metallic = 0.0; + const sphereEntity = rootEntity.createChild(); + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius); + renderer.setMaterial(mtl); + sphereEntity.transform.position = position; + sphereEntity.transform.rotationQuaternion = rotation; + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(physicsSphere); + sphereCollider.linearVelocity = velocity; + sphereCollider.angularDamping = 0.5; + + return sphereEntity; +} + +class ShootScript extends Script { + ray = new Ray(); + position = new Vector3(); + rotation = new Quaternion(); + camera: Camera; + + onAwake() { + this.camera = this.entity.getComponent(Camera); + } + + onUpdate(deltaTime: number) { + const ray = this.ray; + const inputManager = this.engine.inputManager; + if ( + inputManager.pointers && + inputManager.isPointerDown(PointerButton.Primary) + ) { + const pointerPosition = inputManager.pointers[0].position; + this.camera.screenPointToRay(pointerPosition, ray); + ray.direction.scale(50); + addSphere(this.entity, 0.5, this.position, this.rotation, ray.direction); + } + } +} + +//---------------------------------------------------------------------------------------------------------------------- +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => { + + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(0.5, 0.5, 0.5, 1); + + createText(rootEntity, new Vector3(-5, 5), 50); + createChain( + rootEntity, + new Vector3(8.0, 10.0, 0.0), + new Quaternion(), + 10, + 2.0 + ); + createSpring(rootEntity, new Vector3(-4.0, 4.0, 1.0), new Quaternion()); + createHinge(rootEntity, new Vector3(0, 0, 0), new Quaternion()); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(3, 1, 22); + cameraEntity.transform.lookAt(new Vector3(3, 1, 0)); + cameraEntity.addComponent(ShootScript); + + // init direct light + const light = rootEntity.createChild("light"); + light.transform.setPosition(-10, 10, 10); + light.transform.lookAt(new Vector3()); + const directLight = light.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); +}); diff --git a/examples/physx-raycast.ts b/examples/physx-raycast.ts new file mode 100644 index 000000000..fd2d24e63 --- /dev/null +++ b/examples/physx-raycast.ts @@ -0,0 +1,347 @@ +/** + * @title PhysX Raycast + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*8as1SLJgJ6EAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + CapsuleColliderShape, + Color, + DirectLight, + DynamicCollider, + Entity, + Font, + HitResult, + Layer, + MeshRenderer, + PBRMaterial, + PlaneColliderShape, + PointerButton, + PrimitiveMesh, + Quaternion, + Ray, + Script, + ShadowType, + SphereColliderShape, + StaticCollider, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +class GeometryGenerator extends Script { + quat: Quaternion; + + onAwake() { + this.quat = new Quaternion(0, 0, 0.3, 0.7); + this.quat.normalize(); + } + + onUpdate(deltaTime: number) { + const quat = this.quat; + const inputManager = this.engine.inputManager; + if (inputManager.isPointerDown(PointerButton.Secondary)) { + if (Math.random() > 0.5) { + addSphere( + this.entity, + 0.5, + new Vector3( + Math.floor(Math.random() * 6) - 2.5, + 5, + Math.floor(Math.random() * 6) - 2.5 + ), + quat + ); + } else { + addCapsule( + this.entity, + 0.5, + 2.0, + new Vector3( + Math.floor(Math.random() * 6) - 2.5, + 5, + Math.floor(Math.random() * 6) - 2.5 + ), + quat + ); + } + } + } +} + +class Raycast extends Script { + camera: Camera; + ray = new Ray(); + hit = new HitResult(); + + onAwake() { + this.camera = this.entity.getComponent(Camera); + } + + onUpdate(deltaTime: number) { + const engine = this.engine; + const ray = this.ray; + const hit = this.hit; + const inputManager = this.engine.inputManager; + const pointers = inputManager.pointers; + if (pointers && inputManager.isPointerDown(PointerButton.Primary)) { + const pointerPosition = pointers[0].position; + this.camera.screenPointToRay(pointerPosition, ray); + + const result = engine.physicsManager.raycast( + ray, + Number.MAX_VALUE, + Layer.Layer0, + hit + ); + if (result) { + const mtl = new PBRMaterial(engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.metallic = 0.0; + mtl.roughness = 0.5; + + const meshes: MeshRenderer[] = []; + hit.entity.getComponentsIncludeChildren(MeshRenderer, meshes); + meshes.forEach((mesh: MeshRenderer) => { + mesh.setMaterial(mtl); + }); + } + } + } +} + +// init scene +function init(rootEntity: Entity) { + const quat = new Quaternion(0, 0, 0.3, 0.7); + quat.normalize(); + addPlane( + rootEntity, + new Vector3(30, 0.0, 30), + new Vector3(), + new Quaternion() + ); + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 8; i++) { + // eslint-disable-next-line no-plusplus + for (let j = 0; j < 8; j++) { + const random = Math.floor(Math.random() * 3) % 3; + switch (random) { + case 0: + addBox( + rootEntity, + new Vector3(1, 1, 1), + new Vector3(-4 + i, Math.floor(Math.random() * 6) + 1, -4 + j), + quat + ); + break; + case 1: + addSphere( + rootEntity, + 0.5, + new Vector3( + Math.floor(Math.random() * 16) - 4, + 5, + Math.floor(Math.random() * 16) - 4 + ), + quat + ); + break; + case 2: + addCapsule( + rootEntity, + 0.5, + 2.0, + new Vector3( + Math.floor(Math.random() * 16) - 4, + 5, + Math.floor(Math.random() * 16) - 4 + ), + quat + ); + break; + default: + break; + } + } + } +} + +function addPlane( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set( + 0.2179807202597362, + 0.2939682161541871, + 0.31177952549087604, + 1 + ); + mtl.roughness = 0.0; + mtl.metallic = 0.0; + const planeEntity = rootEntity.createChild(); + planeEntity.layer = Layer.Layer1; + + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + renderer.setMaterial(mtl); + planeEntity.transform.position = position; + planeEntity.transform.rotationQuaternion = rotation; + + const physicsPlane = new PlaneColliderShape(); + physicsPlane.position.set(0, size.y, 0); + const planeCollider = planeEntity.addComponent(StaticCollider); + planeCollider.addShape(physicsPlane); + + return planeEntity; +} + +function addBox( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.metallic = 0.0; + mtl.roughness = 0.5; + const boxEntity = rootEntity.createChild(); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + renderer.setMaterial(mtl); + boxEntity.transform.position = position; + boxEntity.transform.rotationQuaternion = rotation; + + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + physicsBox.isTrigger = false; + const boxCollider = boxEntity.addComponent(DynamicCollider); + boxCollider.addShape(physicsBox); + + return boxEntity; +} + +function addSphere( + rootEntity: Entity, + radius: number, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.metallic = 0.0; + mtl.roughness = 0.5; + const sphereEntity = rootEntity.createChild(); + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(rootEntity.engine, radius); + renderer.setMaterial(mtl); + sphereEntity.transform.position = position; + sphereEntity.transform.rotationQuaternion = rotation; + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(physicsSphere); + + return sphereEntity; +} + +function addCapsule( + rootEntity: Entity, + radius: number, + height: number, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.metallic = 0.0; + mtl.roughness = 0.5; + const capsuleEntity = rootEntity.createChild(); + const renderer = capsuleEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCapsule( + rootEntity.engine, + radius, + height, + 20 + ); + renderer.setMaterial(mtl); + capsuleEntity.transform.position = position; + capsuleEntity.transform.rotationQuaternion = rotation; + + const physicsCapsule = new CapsuleColliderShape(); + physicsCapsule.radius = radius; + physicsCapsule.height = height; + const capsuleCollider = capsuleEntity.addComponent(DynamicCollider); + capsuleCollider.addShape(physicsCapsule); + + return capsuleEntity; +} + +//---------------------------------------------------------------------------------------------------------------------- +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then((engine) => { + + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + scene.shadowDistance = 50; + const rootEntity = scene.createRootEntity(); + rootEntity.addComponent(GeometryGenerator); + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + const pos = cameraEntity.transform.position; + pos.set(20, 20, 20); + cameraEntity.transform.lookAt(new Vector3()); + cameraEntity.addComponent(OrbitControl); + cameraEntity.addComponent(Raycast); + + const entity = cameraEntity.createChild("text"); + entity.transform.position = new Vector3(0, 3.5, -10); + const renderer = entity.addComponent(TextRenderer); + renderer.color = new Color(); + renderer.text = "Use mouse to click the entity"; + renderer.font = Font.createFromOS(entity.engine, "Arial"); + renderer.fontSize = 40; + + // init directional light + const light = rootEntity.createChild("light"); + light.transform.setPosition(-0.3, 1, 0.4); + light.transform.lookAt(new Vector3(0, 0, 0)); + const directLight = light.addComponent(DirectLight); + directLight.intensity = 1; + directLight.shadowType = ShadowType.SoftLow; + directLight.shadowStrength = 1; + + init(rootEntity); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); +}); diff --git a/examples/physx-select.ts b/examples/physx-select.ts new file mode 100644 index 000000000..0144e5955 --- /dev/null +++ b/examples/physx-select.ts @@ -0,0 +1,349 @@ +/** + * @title PhysX Select + * @category Physics + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*_vPnS5vMHrcAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + CollisionDetectionMode, + Color, + DirectLight, + DynamicCollider, + Entity, + Font, + Layer, + MeshRenderer, + PBRMaterial, + PlaneColliderShape, + Pointer, + PrimitiveMesh, + Quaternion, + Script, + ShadowType, + StaticCollider, + TextRenderer, + Texture2D, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; + +class PanScript extends Script { + private startPointerPos = new Vector3(); + private tempVec3: Vector3 = new Vector3(); + private zValue: number = 0; + + private collider: DynamicCollider; + camera: Camera; + invCanvasWidth: number; + invCanvasHeight: number; + + onStart() { + this.collider = this.entity.getComponent(DynamicCollider); + } + + onPointerDown(pointer: Pointer) { + // get depth + this.camera.worldToViewportPoint( + this.entity.transform.worldPosition, + this.tempVec3 + ); + this.zValue = this.camera.worldToViewportPoint( + this.entity.transform.worldPosition, + this.tempVec3 + ).z; + const { tempVec3 } = this; + tempVec3.set( + pointer.position.x * this.invCanvasWidth, + pointer.position.y * this.invCanvasHeight, + this.zValue + ); + this.camera.viewportToWorldPoint(tempVec3, this.startPointerPos); + this.collider.linearVelocity = new Vector3(); + this.collider.angularVelocity = new Vector3(); + } + + onPointerDrag(pointer: Pointer) { + const { tempVec3, startPointerPos } = this; + const { transform } = this.entity; + this.tempVec3.set( + pointer.position.x * this.invCanvasWidth, + pointer.position.y * this.invCanvasHeight, + this.zValue + ); + this.camera.viewportToWorldPoint(tempVec3, tempVec3); + Vector3.subtract(tempVec3, startPointerPos, startPointerPos); + transform.worldPosition = transform.worldPosition.add(startPointerPos); + startPointerPos.copyFrom(tempVec3); + } +} + +function addPlane( + rootEntity: Entity, + size: Vector2, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set( + 0.2179807202597362, + 0.2939682161541871, + 0.31177952549087604, + 1 + ); + mtl.roughness = 0.0; + mtl.metallic = 0.0; + + const planeEntity = rootEntity.createChild(); + planeEntity.layer = Layer.Layer1; + + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(rootEntity.engine, size.x, size.y); + renderer.setMaterial(mtl); + planeEntity.transform.position = position; + planeEntity.transform.rotationQuaternion = rotation; + + const physicsPlane = new PlaneColliderShape(); + physicsPlane.position.set(0, 0, 0); + const planeCollider = planeEntity.addComponent(StaticCollider); + planeCollider.addShape(physicsPlane); + + return planeEntity; +} + +function addVerticalBox( + rootEntity: Entity, + texture: Texture2D, + x: number, + y: number, + z: number, + camera: Camera, + invCanvasWidth: number, + invCanvasHeight: number +): void { + const entity = rootEntity.createChild("entity"); + entity.transform.setPosition(x, y, z); + + const boxMtl = new PBRMaterial(rootEntity.engine); + boxMtl.roughness = 0.5; + boxMtl.metallic = 0.0; + const boxRenderer = entity.addComponent(MeshRenderer); + boxMtl.baseTexture = texture; + boxMtl.baseTexture.anisoLevel = 12; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + 0.5, + 0.33, + 2, + false + ); + boxRenderer.setMaterial(boxMtl); + + const physicsBox = new BoxColliderShape(); + physicsBox.size = new Vector3(0.5, 0.33, 2); + physicsBox.material.staticFriction = 1; + physicsBox.material.dynamicFriction = 1; + physicsBox.material.bounciness = 0.0; + + const boxCollider = entity.addComponent(DynamicCollider); + boxCollider.addShape(physicsBox); + boxCollider.mass = 1; + boxCollider.collisionDetectionMode = + CollisionDetectionMode.ContinuousSpeculative; + + const pan = entity.addComponent(PanScript); + pan.camera = camera; + pan.invCanvasWidth = invCanvasWidth; + pan.invCanvasHeight = invCanvasHeight; +} + +function addHorizontalBox( + rootEntity: Entity, + texture: Texture2D, + x: number, + y: number, + z: number, + camera: Camera, + invCanvasWidth: number, + invCanvasHeight: number +): void { + const entity = rootEntity.createChild("entity"); + entity.transform.setPosition(x, y, z); + + const boxMtl = new PBRMaterial(rootEntity.engine); + boxMtl.roughness = 0.5; + boxMtl.metallic = 0.0; + const boxRenderer = entity.addComponent(MeshRenderer); + boxMtl.baseTexture = texture; + boxMtl.baseTexture.anisoLevel = 12; + boxRenderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + 2, + 0.33, + 0.5 + ); + boxRenderer.setMaterial(boxMtl); + + const physicsBox = new BoxColliderShape(); + physicsBox.size = new Vector3(2, 0.33, 0.5); + physicsBox.material.staticFriction = 1; + physicsBox.material.dynamicFriction = 1; + physicsBox.material.bounciness = 0.0; + + const boxCollider = entity.addComponent(DynamicCollider); + boxCollider.addShape(physicsBox); + boxCollider.mass = 0.5; + boxCollider.collisionDetectionMode = + CollisionDetectionMode.ContinuousSpeculative; + + const pan = entity.addComponent(PanScript); + pan.camera = camera; + pan.invCanvasWidth = invCanvasWidth; + pan.invCanvasHeight = invCanvasHeight; +} + +function addBox( + rootEntity: Entity, + texture1: Texture2D, + texture2: Texture2D, + camera: Camera, + invCanvasWidth: number, + invCanvasHeight: number +): void { + for (let i: number = 0; i < 8; i++) { + addVerticalBox( + rootEntity, + texture1, + -0.65, + 0.165 + i * 0.33 * 2, + 0, + camera, + invCanvasWidth, + invCanvasHeight + ); + addVerticalBox( + rootEntity, + texture1, + 0, + 0.165 + i * 0.33 * 2, + 0, + camera, + invCanvasWidth, + invCanvasHeight + ); + addVerticalBox( + rootEntity, + texture1, + 0.65, + 0.165 + i * 0.33 * 2, + 0, + camera, + invCanvasWidth, + invCanvasHeight + ); + + addHorizontalBox( + rootEntity, + texture2, + 0, + 0.165 + 0.33 + i * 0.33 * 2, + -0.65, + camera, + invCanvasWidth, + invCanvasHeight + ); + addHorizontalBox( + rootEntity, + texture2, + 0, + 0.165 + 0.33 + i * 0.33 * 2, + 0, + camera, + invCanvasWidth, + invCanvasHeight + ); + addHorizontalBox( + rootEntity, + texture2, + 0, + 0.165 + 0.33 + i * 0.33 * 2, + 0.65, + camera, + invCanvasWidth, + invCanvasHeight + ); + } +} + +//-------------------------------------------------------------------------------------------------------------------- +WebGLEngine.create({ canvas: "canvas", physics: new PhysXPhysics() }).then( + (engine) => { + engine.canvas.resizeByClientSize(); + const invCanvasWidth = 1 / engine.canvas.width; + const invCanvasHeight = 1 / engine.canvas.height; + const scene = engine.sceneManager.activeScene; + scene.shadowDistance = 20; + const rootEntity = scene.createRootEntity("root"); + + scene.ambientLight.diffuseSolidColor.set(0.5, 0.5, 0.5, 1); + scene.ambientLight.diffuseIntensity = 1.2; + + // init camera + const cameraEntity = rootEntity.createChild("camera"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(7, 7, 7); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0), new Vector3(0, 1, 0)); + + const entity = cameraEntity.createChild("text"); + entity.transform.position = new Vector3(0, 3.5, -10); + const renderer = entity.addComponent(TextRenderer); + renderer.color = new Color(); + renderer.text = "Use mouse to move the bricks"; + renderer.font = Font.createFromOS(entity.engine, "Arial"); + renderer.fontSize = 40; + + // init point light + const light = rootEntity.createChild("light"); + light.transform.setPosition(0, 5, 8); + light.transform.lookAt(new Vector3(0, 2, 0), new Vector3(0, 1, 0)); + const directLight = light.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + addPlane(rootEntity, new Vector2(30, 30), new Vector3(), new Quaternion()); + + Promise.all([ + engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Wkn5QY0tpbcAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }), + engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*W5azT5DjDAEAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }), + ]).then((asset: Texture2D[]) => { + addBox( + rootEntity, + asset[0], + asset[1], + camera, + invCanvasWidth, + invCanvasHeight + ); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + engine.run(); + }); + }); + } +); diff --git a/examples/planar-shadow.ts b/examples/planar-shadow.ts new file mode 100644 index 000000000..25dd4ee64 --- /dev/null +++ b/examples/planar-shadow.ts @@ -0,0 +1,80 @@ +/** + * @title Planar Shadow + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*F7EqQpFQuzAAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Animator, + BlinnPhongMaterial, + Camera, + DirectLight, + GLTFResource, + Logger, + MeshRenderer, + PrimitiveMesh, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +import { Color } from "@galacean/engine"; +import { PlanarShadowShaderFactory } from "@galacean/engine-toolkit"; + +/** + * Planar Shadow + */ + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.setPosition(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightEntity = rootEntity.createChild("light_node"); + lightEntity.addComponent(DirectLight); + lightEntity.transform.setPosition(-10, 10, 10); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + const planeEntity = rootEntity.createChild("plane_node"); + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine, 10, 10); + const planeMaterial = new BlinnPhongMaterial(engine); + planeMaterial.baseColor.set(1, 1.0, 0, 1.0); + renderer.setMaterial(planeMaterial); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(asset.animations[0].name); + + const lightDirection = lightEntity.transform.worldForward; + + const renderers = new Array(); + defaultSceneRoot.getComponentsIncludeChildren(MeshRenderer, renderers); + + for (let i = 0, n = renderers.length; i < n; i++) { + const material = renderers[i].getMaterial(); + PlanarShadowShaderFactory.replaceShader(material); + PlanarShadowShaderFactory.setShadowFalloff(material, 0.2); + PlanarShadowShaderFactory.setPlanarHeight(material, 0.01); + PlanarShadowShaderFactory.setLightDirection(material, lightDirection); + PlanarShadowShaderFactory.setShadowColor( + material, + new Color(0, 0, 0, 1.0) + ); + } + }); + + engine.run(); +}); diff --git a/examples/primitive-mesh.ts b/examples/primitive-mesh.ts new file mode 100644 index 000000000..8289c6e33 --- /dev/null +++ b/examples/primitive-mesh.ts @@ -0,0 +1,174 @@ +/** + * @title Primitive Mesh + * @category Mesh + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*0MTbR5vkdFQAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + Entity, + Material, + MeshRenderer, + ModelMesh, + PrimitiveMesh, + RenderFace, + Script, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +main(); + +async function main(): void { + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create root entity + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.ambientLight.diffuseSolidColor = new Color(0.6, 0.6, 0.6); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 20); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create direct light + const lightEntity = rootEntity.createChild("DirectLight"); + const light = lightEntity.addComponent(DirectLight); + light.intensity = 0.6; + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ArCHTbfVPXUAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + const distanceX = 2.5; + const distanceY = 2.4; + const position = new Vector3(); + + // Create material + const material = new BlinnPhongMaterial(engine); + material.renderFace = RenderFace.Double; + material.baseTexture = texture; + + for (let i = 0; i < 3; i++) { + const posX = (i - 1) * distanceX; + + // Create cuboid + position.set(posX, distanceY * 3, 0); + generatePrimitiveEntity( + rootEntity, + "cuboid", + position, + material, + PrimitiveMesh.createCuboid(engine) + ); + + // Create sphere + position.set(posX, distanceY * 2, 0); + generatePrimitiveEntity( + rootEntity, + "sphere", + position, + material, + PrimitiveMesh.createSphere(engine) + ); + + // Create plane + position.set(posX, distanceY * 1, 0); + generatePrimitiveEntity( + rootEntity, + "plane", + position, + material, + PrimitiveMesh.createPlane(engine) + ); + + // Create cylinder + position.set(posX, -distanceY * 0, 0); + generatePrimitiveEntity( + rootEntity, + "cylinder", + position, + material, + PrimitiveMesh.createCylinder(engine) + ); + + // Create cone + position.set(posX, -distanceY * 1, 0); + generatePrimitiveEntity( + rootEntity, + "cone", + position, + material, + PrimitiveMesh.createCone(engine) + ); + + // Create turos + position.set(posX, -distanceY * 2, 0); + generatePrimitiveEntity( + rootEntity, + "torus", + position, + material, + PrimitiveMesh.createTorus(engine) + ); + + // Create capsule + position.set(posX, -distanceY * 3, 0); + generatePrimitiveEntity( + rootEntity, + "capsule", + position, + material, + PrimitiveMesh.createCapsule(engine, 0.5, 1, 24, 1) + ); + } + }); + + // Run engine + engine.run(); +} + +/** + * generate primitive mesh entity. + */ +function generatePrimitiveEntity( + rootEntity: Entity, + name: string, + position: Vector3, + material: Material, + mesh: ModelMesh +): Entity { + const entity = rootEntity.createChild(name); + entity.transform.setPosition(position.x, position.y, position.z); + entity.addComponent(RotateScript); + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(material); + + return entity; +} + +/** + * Script for rotate. + */ +class RotateScript extends Script { + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + this.entity.transform.rotate(0.5, 0.6, 0); + } +} diff --git a/examples/render-target.ts b/examples/render-target.ts new file mode 100644 index 000000000..ee636d9f1 --- /dev/null +++ b/examples/render-target.ts @@ -0,0 +1,125 @@ +/** + * @title Render Target + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*VLd3QYdpb1MAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Animator, + AssetType, + BackgroundMode, + Camera, + Entity, + GLTFResource, + Layer, + MeshRenderer, + PrimitiveMesh, + RenderFace, + RenderTarget, + Script, + SkyBoxMaterial, + Texture2D, + TextureCube, + UnlitMaterial, + WebGLEngine, +} from "@galacean/engine"; + +// Create scene + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + const control = cameraEntity.addComponent(OrbitControl); + control.minDistance = 3; + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + // Create planes to mock mirror + const planeEntity = rootEntity.createChild("mirror"); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + const mesh = PrimitiveMesh.createPlane(engine, 2, 2); + const material = new UnlitMaterial(engine); + + planeEntity.transform.setRotation(90, 0, 0); + material.renderFace = RenderFace.Double; + planeRenderer.mesh = mesh; + planeRenderer.setMaterial(material); + for (let i = 0; i < 8; i++) { + const clone = planeEntity.clone(); + planeEntity.parent.addChild(clone); + + clone.layer = Layer.Layer1; + clone.transform.setPosition((i - 4) * 2, 0, i % 2 ? -5 : -8); + } + planeEntity.isActive = false; + + // Create sky + const background = scene.background; + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + background.mode = BackgroundMode.Sky; + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + // Add script to switch `camera.renderTarget` + class switchRTScript extends Script { + renderColorTexture = new Texture2D(engine, 1024, 1024); + renderTarget = new RenderTarget( + engine, + 1024, + 1024, + this.renderColorTexture + ); + + constructor(entity: Entity) { + super(entity); + material.baseTexture = this.renderColorTexture; + } + + onBeginRender(camera: Camera) { + camera.renderTarget = this.renderTarget; + camera.cullingMask = Layer.Layer0; + camera.render(); + camera.renderTarget = null; + camera.cullingMask = Layer.Everything; + } + } + + cameraEntity.addComponent(switchRTScript); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*Gi7CTZqKuacAAAAAAAAAAABkARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*iRRMQIExwKMAAAAAAAAAAABkARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*ZIcPQZo20sAAAAAAAAAAAABkARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*SPYuTbHT-KgAAAAAAAAAAABkARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*mGUERbY77roAAAAAAAAAAABkARQnAQ", + "https://gw.alipayobjects.com/mdn/rms_475770/afts/img/A*ilkPS7A1_JsAAAAAAAAAAABkARQnAQ", + ], + type: AssetType.TextureCube, + }) + .then((cubeMap) => { + // Load glTF + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/8cc524dd-2481-438d-8374-3c933adea3b6.gltf" + ) + .then((gltf) => { + const { animations, defaultSceneRoot } = gltf; + + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(animations[0].name); + }); + + scene.ambientLight.specularTexture = cubeMap; + skyMaterial.texture = cubeMap; + engine.run(); + }); +}); diff --git a/examples/renderer-cull.ts b/examples/renderer-cull.ts new file mode 100644 index 000000000..1cef5a1e3 --- /dev/null +++ b/examples/renderer-cull.ts @@ -0,0 +1,91 @@ +/** + * @title Renderer Cull + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*6LDmR5sbLu4AAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + MeshRenderer, + PrimitiveMesh, + Script, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { FreeControl } from "@galacean/engine-toolkit-controls"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + //-- create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.position = new Vector3(0, 0, 50); + cameraEntity.addComponent(Camera); + const control = cameraEntity.addComponent(FreeControl); + control.movementSpeed = 50; + + engine.run(); + + // create two renderer + const cube = rootEntity.createChild("cube1"); + const cube2 = rootEntity.createChild("cube2"); + cube.transform.position = new Vector3(-10, 0, 0); + cube2.transform.position = new Vector3(10, 0, 0); + + const lightNode = rootEntity.createChild("Light"); + lightNode.transform.setRotation(-45, 0, 0); + lightNode.addComponent(DirectLight); + + const material = new BlinnPhongMaterial(engine); + material.baseColor = new Color(1, 0, 0, 1); + const material2 = new BlinnPhongMaterial(engine); + material2.baseColor = new Color(0, 0, 1, 1); + const geometry = PrimitiveMesh.createCuboid(engine, 5, 5, 5); + const sphereGeometry = PrimitiveMesh.createSphere(engine, 5); + + const cubeRenderer = cube.addComponent(MeshRenderer); + const cubeRenderer2 = cube2.addComponent(MeshRenderer); + + cubeRenderer.mesh = geometry; + cubeRenderer.setMaterial(material); + + cubeRenderer2.mesh = sphereGeometry; + cubeRenderer2.setMaterial(material2); + + // rotate + class RotationScript extends Script { + onUpdate() { + this.entity.transform.rotate(1, 1, 1); + } + } + cube.addComponent(RotationScript); + cube2.addComponent(RotationScript); + + // observe renderer-cull + const state = { + cube1: "正常渲染", + cube2: "正常渲染", + }; + + class ObserverScript extends Script { + onUpdate() { + state.cube1 = cubeRenderer.isCulled ? "视锥体裁剪" : "正常渲染"; + state.cube2 = cubeRenderer2.isCulled ? "视锥体裁剪" : "正常渲染"; + } + } + + rootEntity.addComponent(ObserverScript); + + const folder = gui.addFolder("移动视角,观察视锥体裁剪情况"); + folder.add(state, "cube1").name("红色立方体").listen(); + folder.add(state, "cube2").name("蓝色球体").listen(); + folder.open(); +}); diff --git a/examples/scene-basic.ts b/examples/scene-basic.ts new file mode 100644 index 000000000..7e5d586a4 --- /dev/null +++ b/examples/scene-basic.ts @@ -0,0 +1,53 @@ +/** + * @title Scene Basic + * @category Basic + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*qnCQRJ_-fwQAAAAAAAAAAAAADiR2AQ/original + */ +// Import Modules +import { + BlinnPhongMaterial, + Camera, + Color, + DirectLight, + MeshRenderer, + PrimitiveMesh, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Init Engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + // Adapter to screen + engine.canvas.resizeByClientSize(); + + // Get root entity of current scene + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("root"); + + // Init Camera + let cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.position = new Vector3(0, 5, 10); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(Camera); + scene.background.solidColor.set(1, 1, 1, 1); + + // Create a entity to add light component + let lightEntity = rootEntity.createChild("light"); + + // Create light component + let directLight = lightEntity.addComponent(DirectLight); + directLight.color = new Color(1.0, 1.0, 1.0); + directLight.intensity = 0.5; + + // Control light direction by entity's transform + lightEntity.transform.rotation = new Vector3(45, 45, 45); + + // Create Cube + let cubeEntity = rootEntity.createChild("cube"); + let cube = cubeEntity.addComponent(MeshRenderer); + cube.mesh = PrimitiveMesh.createCuboid(engine, 2, 2, 2); + cube.setMaterial(new BlinnPhongMaterial(engine)); + + // Run Engine + engine.run(); +}); diff --git a/examples/scene-fog.ts b/examples/scene-fog.ts new file mode 100644 index 000000000..08d78d947 --- /dev/null +++ b/examples/scene-fog.ts @@ -0,0 +1,151 @@ +/** + * @title Scene Fog + * @category Scene + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*FEbDQLYuRNkAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + Camera, + Color, + DirectLight, + FogMode, + GLTFResource, + Scene, + ShadowType, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { FreeControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +async function main() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + + // Set background color to corn flower blue + const cornFlowerBlue = new Color(130 / 255, 163 / 255, 255 / 255); + scene.background.solidColor = cornFlowerBlue; + + // Set fog + scene.fogMode = FogMode.ExponentialSquared; + scene.fogDensity = 0.015; + scene.fogEnd = 200; + scene.fogColor = cornFlowerBlue; + + const rootEntity = scene.createRootEntity(); + + // Create camera entity and components + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(-6, 2, -22); + cameraEntity.transform.rotate(new Vector3(0, -110, 0)); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(FreeControl).floorMock = false; + + // Create light entity and component + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0, 0.7, 0.5); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + // Enable light cast shadow + const directLight = lightEntity.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + // Add ambient light + const ambientLight = await engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/09904c03-0d23-4834-aa73-64e11e2287b0.bin", + type: AssetType.Env, + }); + scene.ambientLight = ambientLight; + + // Add model + const glTFResource = await engine.resourceManager.load( + "https://gw.alipayobjects.com/os/OasisHub/19748279-7b9b-4c17-abdf-2c84f93c54c8/oasis-file/1670226408346/low_poly_scene_forest_waterfall.gltf" + ); + rootEntity.addChild(glTFResource.defaultSceneRoot); + + engine.run(); + + // Add debug GUI for fog + addDebugGUI(scene); +} + +function addDebugGUI(scene: Scene): void { + let fogStartItem; + let fogEndItem; + let fogDensityItem; + const gui = new dat.GUI(); + + let switchMode = (mode: FogMode) => { + switch (mode) { + case FogMode.None: + clearModeItems(); + break; + case FogMode.Linear: + clearModeItems(); + fogStartItem = gui.add(debugInfos, "fogStart", 0, 300).onChange((v) => { + scene.fogStart = v; + }); + + fogEndItem = gui.add(debugInfos, "fogEnd", 0, 300).onChange((v) => { + scene.fogStart = v; + }); + break; + case FogMode.Exponential: + case FogMode.ExponentialSquared: + clearModeItems(); + fogDensityItem = gui + .add(debugInfos, "fogDensity", 0.01, 0.1) + .onChange((v) => { + scene.fogDensity = v; + }); + break; + } + scene.fogMode = mode; + }; + + let clearModeItems = () => { + if (fogStartItem) { + fogStartItem.remove(); + fogStartItem = null; + } + if (fogEndItem) { + fogEndItem.remove(); + fogEndItem = null; + } + if (fogDensityItem) { + fogDensityItem.remove(); + fogDensityItem = null; + } + }; + + const fogColor = scene.fogColor; + const debugInfos = { + fogMode: scene.fogMode, + fogColor: [fogColor.r * 255, fogColor.g * 255, fogColor.b * 255], + fogStart: scene.fogStart, + fogEnd: scene.fogEnd, + fogDensity: scene.fogDensity, + }; + + gui + .add(debugInfos, "fogMode", { + None: FogMode.None, + Linear: FogMode.Linear, + Exponential: FogMode.Exponential, + ExponentialSquared: FogMode.ExponentialSquared, + }) + .onChange((v) => { + switchMode(parseInt(v)); + }); + + gui.addColor(debugInfos, "fogColor").onChange((v) => { + scene.fogColor.set(v[0] / 255, v[1] / 255, v[2] / 255, 1.0); + }); + + switchMode(scene.fogMode); +} +main(); diff --git a/examples/screenshot.ts b/examples/screenshot.ts new file mode 100644 index 000000000..79f16e1c4 --- /dev/null +++ b/examples/screenshot.ts @@ -0,0 +1,175 @@ +/** + * @title Screenshot + * @category Camera + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*0OXcQYHlwzQAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AmbientLight, + Animator, + AssetType, + Camera, + GLTFResource, + RenderTarget, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +const gui = new dat.GUI(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + scene.background.solidColor.set(0, 0, 0, 0); + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + // add gltf model + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play("run"); + }); + + // add ambient light + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + }); + engine.run(); + + /** ---------------------------- Capture ---------------------------- */ + let screenshotCanvas: HTMLCanvasElement = null; + let flipYCanvas: HTMLCanvasElement = null; + function screenshot( + camera: Camera, + width: number, + height: number, + flipY = false, + isPNG = true, + jpgQuality = 1 + ) { + if (!screenshotCanvas) { + screenshotCanvas = document.createElement("canvas"); + } + let canvas = screenshotCanvas; + + screenshotCanvas.width = width; + screenshotCanvas.height = height; + + const context = screenshotCanvas.getContext("2d"); + const isPaused = engine.isPaused; + engine.pause(); + + const originalTarget = camera.renderTarget; + const renderColorTexture = new Texture2D(engine, width, height); + const renderTargetData = new Uint8Array(width * height * 4); + const renderTarget = new RenderTarget( + engine, + width, + height, + renderColorTexture, + undefined, + 8 + ); + + // render to off-screen + camera.renderTarget = renderTarget; + camera.aspectRatio = width / height; + camera.render(); + + renderColorTexture.getPixelBuffer(0, 0, width, height, 0, renderTargetData); + + const imageData = context.createImageData(width, height); + imageData.data.set(renderTargetData); + context.putImageData(imageData, 0, 0); + + // flip Y + if (flipY) { + if (!flipYCanvas) { + flipYCanvas = document.createElement("canvas"); + } + canvas = flipYCanvas; + + flipYCanvas.width = width; + flipYCanvas.height = height; + + const ctx2 = flipYCanvas.getContext("2d"); + + ctx2.translate(0, height); + ctx2.scale(1, -1); + ctx2.drawImage(screenshotCanvas, 0, 0); + } + + // download + canvas.toBlob( + (blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + + document.body.appendChild(a); + a.style.display = "none"; + a.href = url; + a.download = "screenshot"; + + a.addEventListener("click", () => { + if (a.parentElement) { + a.parentElement.removeChild(a); + } + }); + + a.click(); + + window.URL.revokeObjectURL(url); + + // revert + camera.renderTarget = originalTarget; + camera.resetAspectRatio(); + !isPaused && engine.resume(); + }, + isPNG ? "image/png" : "image/jpeg", + !isPNG && jpgQuality + ); + } + + function openDebug() { + const config = { + width: 1024, + height: 1024, + flipY: false, + isPNG: true, + jpgQuality: 1, + screenshot: () => { + const { width, height, flipY, isPNG, jpgQuality } = config; + screenshot(camera, width, height, flipY, isPNG, jpgQuality); + }, + }; + + const configFolder = gui.addFolder("config"); + configFolder.add(config, "width", 1, 2048, 1); + configFolder.add(config, "height", 1, 2048, 1); + configFolder.add(config, "flipY"); + configFolder.add(config, "isPNG"); + configFolder.add(config, "jpgQuality", 0, 1, 0.01); + gui.add(config, "screenshot"); + } + + openDebug(); +}); diff --git a/examples/script-basic.ts b/examples/script-basic.ts new file mode 100644 index 000000000..1614d4971 --- /dev/null +++ b/examples/script-basic.ts @@ -0,0 +1,55 @@ +/** + * @title Script Basic + * @category Basic + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*cHnfTJxLtpkAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Camera, + GLTFResource, + Script, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(3, 3, 3); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + cameraEntity.addComponent(OrbitControl); + + engine.sceneManager.activeScene.ambientLight.diffuseSolidColor.set( + 1, + 1, + 1, + 1 + ); + + // Create Rotate Script + class Rotate extends Script { + private _tempVector = new Vector3(0, 1, 0); + onUpdate() { + this.entity.transform.rotate(this._tempVector); + } + } + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf" + ) + .then((gltf) => { + const duck = gltf.defaultSceneRoot; + + rootEntity.addChild(duck); + + // Add Script + duck.addComponent(Rotate); + }); + + engine.run(); +}); diff --git a/examples/shader-lab-multi-pass.ts b/examples/shader-lab-multi-pass.ts new file mode 100644 index 000000000..5b87f6164 --- /dev/null +++ b/examples/shader-lab-multi-pass.ts @@ -0,0 +1,265 @@ +/** + * @title ShaderLab Multi Pass + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*f7GST6W14aUAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BaseMaterial, + Camera, + Logger, + MeshRenderer, + PrimitiveMesh, + Script, + Shader, + ShaderData, + Texture2D, + Vector3, + Vector4, + WebGLEngine +} from "@galacean/engine"; +import { ShaderLab } from "@galacean/engine-shader-lab"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +const LAYER = 40; + +Logger.enable(); +const gui = new dat.GUI(); +const shaderLab = new ShaderLab(); +const loopPassSource = Array.from({ length: LAYER }) + .map((_, index) => { + const step = (1 / LAYER) * index; + const u_furOffset = step % 1 === 0 ? step + ".0" : step; + const renderStateSource = + index > 0 + ? ` + BlendState = transparentBlendState; + DepthState = transparentDepthState; + RenderQueueType = RenderQueueType.Transparent; + ` + : ``; + + return ` + Pass "${index}" { + ${renderStateSource} + + mat4 renderer_MVPMat; + mat4 renderer_NormalMat; + mat4 renderer_ModelMat; + float u_furLength; + vec4 u_uvTilingOffset; + vec3 u_gravity; + float u_gravityIntensity; + sampler2D u_mainTex; + sampler2D u_layerTex; + + VertexShader = vert; + FragmentShader = frag; + + struct EnvMapLight { + vec3 diffuse; + float mipMapLevel; + float diffuseIntensity; + float specularIntensity; + } + EnvMapLight scene_EnvMapLight; + + #ifdef SCENE_USE_SH + vec3 scene_EnvSH[9]; + + vec3 getLightProbeIrradiance(vec3 sh[9], vec3 normal){ + normal.x = -normal.x; + vec3 result = sh[0] + + sh[1] * (normal.y) + + sh[2] * (normal.z) + + sh[3] * (normal.x) + + sh[4] * (normal.y * normal.x) + + sh[5] * (normal.y * normal.z) + + sh[6] * (3.0 * normal.z * normal.z - 1.0) + + sh[7] * (normal.z * normal.x) + + sh[8] * (normal.x * normal.x - normal.y * normal.y); + + return max(result, vec3(0.0)); + } + #endif + + v2f vert(a2v v) { + v2f o; + + float u_furOffset = ${u_furOffset}; + vec4 position = v.POSITION; + vec3 direction = mix(v.NORMAL, u_gravity * u_gravityIntensity + v.NORMAL * (1.0 - u_gravityIntensity), u_furOffset); + position.xyz += direction * u_furLength * u_furOffset; + + gl_Position = renderer_MVPMat * position; + + vec2 uvOffset = u_uvTilingOffset.zw * u_furOffset; + o.v_uv = v.TEXCOORD_0 + uvOffset * vec2(1.0, 1.0) / u_uvTilingOffset.xy; + o.v_uv2 = v.TEXCOORD_0 * u_uvTilingOffset.xy + uvOffset; + o.v_normal = normalize( mat3(renderer_NormalMat) * NORMAL ); + vec4 temp_pos = renderer_ModelMat * position; + o.v_pos = temp_pos.xyz / temp_pos.w; + + return o; + } + + void frag(v2f i) { + float u_furOffset = ${u_furOffset}; + vec2 v_uv = i.v_uv; + vec2 v_uv2 = i.v_uv2; + vec3 v_normal = i.v_normal; + vec3 v_pos = i.v_pos; + + vec4 baseColor = texture2D(u_mainTex, v_uv); + float alpha2 = u_furOffset * u_furOffset; + + float mask = (texture2D(u_layerTex, v_uv2)).r; + mask = step(alpha2, mask); + + + #ifdef SCENE_USE_SH + vec3 irradiance = getLightProbeIrradiance(scene_EnvSH, v_normal); + irradiance *= scene_EnvMapLight.diffuseIntensity; + #else + vec3 irradiance = scene_EnvMapLight.diffuse * scene_EnvMapLight.diffuseIntensity; + irradiance *= PI; + #endif + + baseColor.rgb += irradiance * RECIPROCAL_PI * alpha2; + gl_FragColor.rgb = baseColor.rgb; + gl_FragColor.a = 1.0 - alpha2; + gl_FragColor.a *= mask; + } + } + `; + }) + .join("\n"); + +const furShaderSource = `Shader "fur-unlit" { + SubShader "Default" { + BlendState transparentBlendState { + Enabled = true; + SourceColorBlendFactor = BlendFactor.SourceAlpha; + DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + SourceAlphaBlendFactor = BlendFactor.One; + DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + } + + DepthState transparentDepthState { + WriteEnabled = false; + } + + struct a2v { + vec4 POSITION; + vec3 NORMAL; + vec2 TEXCOORD_0; + } + + struct v2f { + vec2 v_uv; + vec2 v_uv2; + vec3 v_normal; + vec3 v_pos; + } + + #define PI 3.14159265359 + #define RECIPROCAL_PI 0.31830988618 + + ${loopPassSource} + } +}`; + +class RandomGravityScript extends Script { + shaderData: ShaderData; + progress = 0; + onUpdate(deltaTime: number) { + const progress = 0.5 + Math.cos((this.progress = this.progress + deltaTime * 2)) / 2; + this.shaderData.setFloat("u_gravityIntensity", progress); + } +} + +WebGLEngine.create({ canvas: "canvas", shaderLab }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const furShader = Shader.create(furShaderSource); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("cameraNode"); + cameraEntity.transform.setPosition(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load([ + { + type: AssetType.Texture2D, + url: "https://mdn.alipayobjects.com/huamei_dmxymu/afts/img/A*R75iTZlbVfgAAAAAAAAAAAAADuuHAQ/original" + }, + { + type: AssetType.Texture2D, + url: "https://mdn.alipayobjects.com/huamei_dmxymu/afts/img/A*t1s4T7h_1OQAAAAAAAAAAAAADuuHAQ/original" + }, + { + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/6470ea5e-094b-4a77-a05f-4945bf81e318.bin" + } + ]) + .then((res) => { + const layerTexture = res[0] as Texture2D; + const baseTexture = res[1] as Texture2D; + scene.ambientLight = res[2] as AmbientLight; + + // create sphere + const entity = rootEntity.createChild("sphere"); + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 0.5, 16); + + const material = new BaseMaterial(engine, furShader); + renderer.setMaterial(material); + + const shaderData = material.shaderData; + + shaderData.setTexture("u_mainTex", baseTexture); + shaderData.setTexture("u_layerTex", layerTexture); + + shaderData.setFloat("u_furLength", 0.5); + shaderData.setVector4("u_uvTilingOffset", new Vector4(5, 5, 0.5, 0.5)); + shaderData.setVector3("u_gravity", new Vector3(0, 0, 0)); + shaderData.setFloat("u_gravityIntensity", 0); + + const randomGravityScript = entity.addComponent(RandomGravityScript); + randomGravityScript.shaderData = shaderData; + + const debugInfo = { + u_furLength: 0.5, + uvScale: 5, + uvOffset: 0.5, + enable: () => { + randomGravityScript.enabled = !randomGravityScript.enabled; + shaderData.setFloat("u_gravityIntensity", 0); + randomGravityScript.progress = 0; + } + }; + + gui.add(debugInfo, "u_furLength", 0, 1, 0.01).onChange((v) => { + shaderData.setFloat("u_furLength", v); + }); + gui.add(debugInfo, "uvScale", 1, 20, 1).onChange((v) => { + const value = shaderData.getVector4("u_uvTilingOffset"); + value.x = value.y = v; + }); + gui.add(debugInfo, "uvOffset", -1, 1, 0.01).onChange((v) => { + const value = shaderData.getVector4("u_uvTilingOffset"); + value.z = value.w = v; + }); + + gui.add(scene.ambientLight, "diffuseIntensity", 0, 1, 0.01); + gui.add(debugInfo, "enable").name("pause/resume"); + engine.run(); + }); +}); diff --git a/examples/shader-lab-simple.ts b/examples/shader-lab-simple.ts new file mode 100644 index 000000000..2ddc5b7f8 --- /dev/null +++ b/examples/shader-lab-simple.ts @@ -0,0 +1,170 @@ +/** + * @title ShaderLab Basic + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*C7Y1RaI_ZJEAAAAAAAAAAAAADiR2AQ/original + */ + +import { GUI } from 'dat.gui'; +import { OrbitControl } from '@galacean/engine-toolkit-controls'; +import { + BufferMesh, + Camera, + Logger, + WebGLEngine, + Buffer, + BufferBindFlag, + BufferUsage, + VertexElement, + VertexElementFormat, + MeshRenderer, + Shader, + Material, + Color, +} from '@galacean/engine'; +import { ShaderLab } from '@galacean/engine-shader-lab'; + +const shaderLab = new ShaderLab(); + +const normalShaderSource = `Shader "Triangle" { + SubShader "Default" { + Pass "0" { + mat4 renderer_MVPMat; + vec3 u_color; + + struct a2v { + vec4 POSITION; + } + + struct v2f { + vec3 v_color; + } + + VertexShader = vert; + FragmentShader = frag; + + v2f vert(a2v v) { + v2f o; + + gl_Position = renderer_MVPMat * v.POSITION; + o.v_color = u_color; + return o; + } + + void frag(v2f i) { + gl_FragColor = vec4(i.v_color, 1.0); + } + } + } +}`; + +const linesShaderSource = ` +Shader "Lines" { + SubShader "Default" { + Pass "0" { + mat4 renderer_MVPMat; + vec3 u_color; + + struct a2v { + vec4 POSITION; + } + + struct v2f { + vec4 v_pos; + vec3 v_color; + } + + VertexShader = vert; + FragmentShader = frag; + + v2f vert(a2v v) { + v2f o; + + gl_Position = renderer_MVPMat * v.POSITION; + o.v_color = u_color; + o.v_pos = v.POSITION; + return o; + } + + #define S smoothstep + vec4 scene_ElapsedTime; + + vec4 Line(vec2 uv, float speed, float height, vec3 col) { + uv.y += S(1.0, 0.0, abs(uv.x)) * sin(scene_ElapsedTime.x * speed + uv.x * height) * 0.2; + return vec4(S(0.06 * S(0.2, 0.9, abs(uv.x)), 0.0, abs(uv.y) - 0.004) * col, 1.0) * S(1.0, 0.3, abs(uv.x)); + } + + void frag(v2f i) { + vec2 iResolution = vec2(1.0, 1.0); + vec2 uv = i.v_pos.xy / iResolution.y; + vec4 color = vec4(0.0); + for (float i = 0.0; i <= 7.0; i += 1.0) { + float t = i / 5.0; + color += Line(uv, 1.0 + t, 4.0 + t, vec3(0.2 + t * 0.7, 0.2 + t * 0.4, 0.3)); + } + gl_FragColor = color; + } + } + } +}`; + +function createPlaneMesh(engine: WebGLEngine) { + const mesh = new BufferMesh(engine); + const vertices = new Float32Array([ + -1, -1, 1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, + ]); + const vertexBuffer = new Buffer( + engine, + BufferBindFlag.VertexBuffer, + vertices, + BufferUsage.Static + ); + mesh.setVertexBufferBinding(vertexBuffer, 12); + mesh.setVertexElements([ + new VertexElement('POSITION', 0, VertexElementFormat.Vector3, 0), + ]); + mesh.addSubMesh(0, 6); + return mesh; +} + +Logger.enable(); +WebGLEngine.create({ canvas: 'canvas', shaderLab }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const shaderMap = { + normal: Shader.create(normalShaderSource), + lines: Shader.create(linesShaderSource), + }; + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild('cameraNode'); + cameraEntity.transform.setPosition(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // create plane + const triangle = rootEntity.createChild('plane'); + const renderer = triangle.addComponent(MeshRenderer); + renderer.mesh = createPlaneMesh(engine); + const shader = shaderMap.lines; + const material = new Material(engine, shader); + material.shaderData.setColor('u_color', new Color(1.0, 1.0, 0)); + renderer.setMaterial(material); + + engine.run(); + + const state = { + shader: 'lines', + }; + + function addGUI() { + const gui = new GUI({ name: 'Switch Shader' }); + gui.add(state, 'shader', Object.keys(shaderMap)).onChange((v) => { + material.shader = shaderMap[v]; + }); + } + + addGUI(); +}); diff --git a/examples/shader-lab.ts b/examples/shader-lab.ts new file mode 100644 index 000000000..0bfb8035b --- /dev/null +++ b/examples/shader-lab.ts @@ -0,0 +1,246 @@ +/** + * @title Shader Lab Planar Shadow + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*09QlQ7IQGecAAAAAAAAAAAAADiR2AQ/original + */ +import { + Color, + Shader, + Vector3, + BlinnPhongMaterial, + Camera, + DirectLight, + GLTFResource, + Logger, + MeshRenderer, + PrimitiveMesh, + WebGLEngine, + AmbientLight, + AssetType, + SkyBoxMaterial, + BackgroundMode, +} from '@galacean/engine'; +import { OrbitControl } from '@galacean/engine-toolkit-controls'; +import { ShaderLab } from '@galacean/engine-shader-lab'; + +// Create ShaderLab +const shaderLab = new ShaderLab(); + +Logger.enable(); +WebGLEngine.create({ canvas: 'canvas', shaderLab }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const { background } = scene; + const rootEntity = scene.createRootEntity(); + + const cameraEntity = rootEntity.createChild('camera_node'); + cameraEntity.transform.setPosition(5, 5, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightEntity = rootEntity.createChild('light_node'); + lightEntity.addComponent(DirectLight); + lightEntity.transform.setPosition(-10, 10, 10); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + const planeEntity = rootEntity.createChild('plane_node'); + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine, 10, 10); + const planeMaterial = new BlinnPhongMaterial(engine); + planeMaterial.baseColor.set(1.0, 1.0, 1.0, 1.0); + renderer.setMaterial(planeMaterial); + + // Create sky + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + background.mode = BackgroundMode.Sky; + + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + Promise.all([ + engine.resourceManager + .load( + 'https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf' + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + + defaultSceneRoot.transform.setPosition(0, 1.5, 0); + + const lightDirection = lightEntity.transform.worldForward; + + const renderers = new Array(); + defaultSceneRoot.getComponentsIncludeChildren(MeshRenderer, renderers); + + const shadowShader = Shader.find('PlanarShadow'); + + for (let i = 0, n = renderers.length; i < n; i++) { + const material = renderers[i].getMaterial(); + if (!material) continue; + material.shader = shadowShader; + const shaderData = material.shaderData; + + shaderData.setFloat('u_planarShadowFalloff', 0.2); + shaderData.setFloat('u_planarHeight', 0.01); + shaderData.setColor('u_planarShadowColor', new Color(0, 0, 0, 1)); + shaderData.setVector3('u_lightDir', lightDirection); + } + }), + engine.resourceManager + .load({ + type: AssetType.Env, + url: 'https://gw.alipayobjects.com/os/bmw-prod/f369110c-0e33-47eb-8296-756e9c80f254.bin', + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + }), + ]); + + engine.run(); +}); + +const PlanarShadowShaderSource = `Shader "PlanarShadow" { + + SubShader "Default" { + + UsePass "pbr/Default/Forward" + + Pass "planarShadow" { + // render states + DepthState { + WriteEnabled = true; + } + + BlendState blendState { + Enabled = true; + SourceColorBlendFactor = BlendFactor.SourceAlpha; + DestinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + SourceAlphaBlendFactor = BlendFactor.One; + DestinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + } + + StencilState { + Enabled = true; + ReferenceValue = 0; + CompareFunctionFront = CompareFunction.Equal; + CompareFunctionBack = CompareFunction.Equal; + FailOperationFront = StencilOperation.Keep; + FailOperationBack = StencilOperation.Keep; + ZFailOperationFront = StencilOperation.Keep; + ZFailOperationBack = StencilOperation.Keep; + PassOperationFront = StencilOperation.IncrementWrap; + PassOperationBack = StencilOperation.IncrementWrap; + } + + BlendState = blendState; + + RenderQueueType = RenderQueueType.Transparent; + + vec3 u_lightDir; + float u_planarHeight; + vec4 u_planarShadowColor; + float u_planarShadowFalloff; + + sampler2D renderer_JointSampler; + float renderer_JointCount; + + mat4 renderer_ModelMat; + mat4 camera_VPMat; + + #ifdef RENDERER_HAS_SKIN + + #ifdef RENDERER_USE_JOINT_TEXTURE + mat4 getJointMatrix(sampler2D smp, float index) { + float base = index / renderer_JointCount; + float hf = 0.5 / renderer_JointCount; + float v = base + hf; + + vec4 m0 = texture2D(smp, vec2(0.125, v )); + vec4 m1 = texture2D(smp, vec2(0.375, v )); + vec4 m2 = texture2D(smp, vec2(0.625, v )); + vec4 m3 = texture2D(smp, vec2(0.875, v )); + + return mat4(m0, m1, m2, m3); + } + #elif defined(RENDERER_BLENDSHAPE_COUNT) + mat4 renderer_JointMatrix[ RENDERER_JOINTS_NUM ]; + #endif + #endif + + vec3 ShadowProjectPos(vec4 vertPos) { + vec3 shadowPos; + + // get the world space coordinates of the vertex + + vec3 worldPos = (renderer_ModelMat * vertPos).xyz; + + // world space coordinates of the shadow (the part below the ground is unchanged) + shadowPos.y = min(worldPos.y , u_planarHeight); + shadowPos.xz = worldPos.xz - u_lightDir.xz * max(0.0, worldPos.y - u_planarHeight) / u_lightDir.y; + + return shadowPos; + } + + struct a2v { + vec4 POSITION; + vec4 JOINTS_0; + vec4 WEIGHTS_0; + } + + struct v2f { + vec4 color; + } + + v2f vert(a2v v) { + v2f o; + + vec4 position = vec4(v.POSITION.xyz, 1.0 ); + #ifdef RENDERER_HAS_SKIN + #ifdef RENDERER_USE_JOINT_TEXTURE + mat4 skinMatrix = + v.WEIGHTS_0.x * getJointMatrix(renderer_JointSampler, v.JOINTS_0.x ) + + v.WEIGHTS_0.y * getJointMatrix(renderer_JointSampler, v.JOINTS_0.y ) + + v.WEIGHTS_0.z * getJointMatrix(renderer_JointSampler, v.JOINTS_0.z ) + + v.WEIGHTS_0.w * getJointMatrix(renderer_JointSampler, v.JOINTS_0.w ); + #else + mat4 skinMatrix = + v.WEIGHTS_0.x * renderer_JointMatrix[ int( v.JOINTS_0.x ) ] + + v.WEIGHTS_0.y * renderer_JointMatrix[ int( v.JOINTS_0.y ) ] + + v.WEIGHTS_0.z * renderer_JointMatrix[ int( v.JOINTS_0.z ) ] + + v.WEIGHTS_0.w * renderer_JointMatrix[ int( v.JOINTS_0.w ) ]; + #endif + position = skinMatrix * position; + #endif + + // get the shadow's world space coordinates + vec3 shadowPos = ShadowProjectPos(position); + + // convert to clip space + gl_Position = camera_VPMat * vec4(shadowPos, 1.0); + + // get the world coordinates of the center point + vec3 center = vec3(renderer_ModelMat[3].x, u_planarHeight, renderer_ModelMat[3].z); + // calculate shadow falloff + float falloff = 0.5 - clamp(distance(shadowPos , center) * u_planarShadowFalloff, 0.0, 1.0); + + // shadow color + o.color = u_planarShadowColor; + o.color.a *= falloff; + return o; + } + + VertexShader = vert; + FragmentShader = frag; + + void frag(v2f i) { + gl_FragColor = i.color; + } + } + } +}`; + +Shader.create(PlanarShadowShaderSource); diff --git a/examples/shader-replacement.ts b/examples/shader-replacement.ts new file mode 100644 index 000000000..7c2c02a5c --- /dev/null +++ b/examples/shader-replacement.ts @@ -0,0 +1,218 @@ +/** + * @title Shader Replacement + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*p9uTTKU3vtwAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AmbientLight, + AssetType, + BackgroundMode, + Camera, + DirectLight, + GLTFResource, + Logger, + PrimitiveMesh, + Shader, + SkyBoxMaterial, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +/** + * Main function. + */ +async function main() { + Logger.enable(); + + initShader(); + + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Get scene and create root entity + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create directional light + const directLightEntity = rootEntity.createChild("Directional Light"); + directLightEntity.addComponent(DirectLight); + directLightEntity.transform.setRotation(30, 0, 0); + + const directLightEntity2 = rootEntity.createChild("Directional Light2"); + directLightEntity2.addComponent(DirectLight); + directLightEntity2.transform.setRotation(-30, 180, 0); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 5); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create sky + const background = scene.background; + background.mode = BackgroundMode.Sky; + + const sky = background.sky; + const skyMaterial = new SkyBoxMaterial(engine); + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + + engine.resourceManager + .load([ + { + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/f369110c-0e33-47eb-8296-756e9c80f254.bin", + }, + { + url: "https://gw.alipayobjects.com/os/bmw-prod/150e44f6-7810-4c45-8029-3575d36aff30.gltf", + type: AssetType.GLTF, + }, + { + url: "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf", + type: AssetType.GLTF, + }, + { + url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*Q60vQ40ZERsAAAAAAAAAAAAADil6AQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((resources) => { + // Add ambient light + const ambientLight = resources[0]; + scene.ambientLight = ambientLight; + skyMaterial.texture = ambientLight.specularTexture; + skyMaterial.textureDecodeRGBM = true; + + // Add helmet model + const glTFResourceHelmet = resources[1]; + const helmetEntity = glTFResourceHelmet.defaultSceneRoot; + helmetEntity.transform.position.set(-1, 0, 0); + rootEntity.addChild(helmetEntity); + + // Add duck model + const glTFResourceDuck = resources[2]; + const duckEntity = glTFResourceDuck.defaultSceneRoot; + duckEntity.transform.position.set(1, -1, 0); + rootEntity.addChild(duckEntity); + + // Apply uv check texture + const uvCheckTexture = resources[3]; + for (const material of glTFResourceHelmet.materials!) { + material.shaderData.setTexture("u_UVCheckTexture", uvCheckTexture); + } + for (const material of glTFResourceDuck.materials!) { + material.shaderData.setTexture("u_UVCheckTexture", uvCheckTexture); + } + + // Run engine + engine.run(); + + // Add debug GUI to switch replacement shader + addDebugGUI(camera); + }); +} +main(); + +/** + * Init replacement shader. + */ +function initShader() { + const normalVS = ` + #include + #include + #include + #include + + void main() { + + #include + #include + #include + #include + #include + #include + }`; + + const normalFS = ` + #include + #include + + void main() { + gl_FragColor = vec4(v_normal,1.0); + } + `; + + const uvCheckVS = ` + #include + #include + #include + #include + + void main() { + + #include + #include + #include + #include + #include + }`; + + const uvCheckFS = ` + #include + #include + + uniform sampler2D u_UVCheckTexture; + + void main() { + vec4 textureColor = texture2D(u_UVCheckTexture, v_uv); + gl_FragColor = textureColor; + } + `; + + // Create normal shader + Shader.create("NormalShader", normalVS, normalFS); + // Create uv check shader + Shader.create("UVCheckShader", uvCheckVS, uvCheckFS); +} + +/** + * Add debug GUI to switch replacement shader. + * @param camera - Camera to render scene + */ +function addDebugGUI(camera: Camera): void { + enum RenderMode { + PBR = "PBR Shader(No replacement)", + Normal = "Replacement normal Shader", + UVCheck = "Replacement UVCheck Shader", + } + + const debugGUI = new dat.GUI({ + width: 350, + }); + + const state = { + "Render Mode": RenderMode.PBR, + }; + debugGUI + .add(state, "Render Mode", [ + RenderMode.PBR, + RenderMode.Normal, + RenderMode.UVCheck, + ]) + .onChange((v: string) => { + switch (v) { + case RenderMode.PBR: + camera.resetReplacementShader(); + break; + case RenderMode.Normal: + camera.setReplacementShader(Shader.find("NormalShader")); + break; + case RenderMode.UVCheck: + camera.setReplacementShader(Shader.find("UVCheckShader")); + break; + } + }); +} diff --git a/examples/shader-water.ts b/examples/shader-water.ts new file mode 100644 index 000000000..3b0457189 --- /dev/null +++ b/examples/shader-water.ts @@ -0,0 +1,261 @@ +/** + * @title Shader Water + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*eGEwSZhJsoYAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from '@galacean/engine-toolkit-controls'; +import * as dat from 'dat.gui'; +import { + AssetType, + Camera, + Color, + Engine, + Material, + MeshRenderer, + PrimitiveMesh, + Shader, + Texture2D, + Vector3, + WebGLEngine, + Logger, +} from '@galacean/engine'; +import { ShaderLab } from '@galacean/engine-shader-lab'; + +const shaderLab = new ShaderLab(); +Logger.enable(); + +const shaderSource = `Shader "customWater" { + SubShader "Default" { + Pass "0" { + VertexShader = vert; + FragmentShader = frag; + + mat4 renderer_MVPMat; + + struct a2v { + vec3 POSITION; + vec2 TEXCOORD_0; + vec3 NORMAL; + } + + struct v2f { + vec2 v_uv; + vec3 v_position; + vec3 v_normal; + } + + v2f vert(a2v v) { + v2f o; + + gl_Position = renderer_MVPMat * vec4( v.POSITION, 1.0 ); + o.v_uv = v.TEXCOORD_0; + o.v_normal = v.NORMAL; + o.v_position = v.POSITION; + + return o; + } + + vec4 scene_ElapsedTime; + sampler2D u_texture; + vec3 camera_Position; + float u_water_scale; + float u_water_speed; + + vec3 u_sea_base; + vec3 u_water_color; + float u_sea_height; + + #define EPS 0.001 + #define MAX_ITR 100 + #define MAX_DIS 100.0 + #define PI 3.141592 + + // Distance Functions + float sd_sph(vec3 p, float r) { return length(p) - r; } + + // Distance Map + float map(vec3 p, vec2 sc) + { + float l = cos(length(p * 2.0)); + vec2 u = vec2(l, sc.y); + vec2 um = u * 0.3; + um.x += scene_ElapsedTime.x * 0.1 * u_water_speed; + um.y += -scene_ElapsedTime.x * 0.025 * u_water_speed; + um.x += (um.y) * 2.0; + float a1 = (texture2D(u_texture, (p.yz * 0.4 + um) * u_water_scale)).x; + float a2 = (texture2D(u_texture, (p.zx * 0.4 + um) * u_water_scale)).x; + float a3 = (texture2D(u_texture, (p.xy * 0.4 + um) * u_water_scale)).x; + + float t1 = a1 + a2 + a3; + t1 /= 15.0 * u_water_scale; + + float b1 = (texture2D(u_texture, (p.yz * 1.0+ u) * u_water_scale)).x; + float b2 = (texture2D(u_texture, (p.zx * 1.0+ u) * u_water_scale)).x; + float b3 = (texture2D(u_texture, (p.xy * 1.0+ u) * u_water_scale)).x; + + float t2 = b1 + b2 + b3; + t2 /= 15.0 * u_water_scale; + + float comb = t1 * 0.4 + t2 * 0.1 * (1.0 - t1); + + return comb + sd_sph(p, 3.0); // sd_box(p, vec3(1., 1., 1.)) + sdPlane(p, vec4(0., 0., 1.0, 0.));// + } + + float diffuse(vec3 n,vec3 l,float p) { + return pow(dot(n,l) * 0.4 + 0.6,p); + } + + float specular(vec3 n,vec3 l,vec3 e,float s) { + float nrm = (s + 8.0) / (PI * 8.0); + return pow(max(dot(reflect(e,n),l),0.0),s) * nrm; + } + + // sky + vec3 getSkyColor(vec3 e) { + e.y = max(e.y,0.0); + return vec3(pow(1.0-e.y,2.0), 1.0-e.y, 0.6+(1.0-e.y)*0.4); + } + + vec3 getSeaColor(vec3 p, vec3 n, vec3 l, vec3 eye, vec3 dist) { + float fresnel = clamp(1.0 - dot(n,-eye), 0.0, 1.0); + fresnel = pow(fresnel,3.0) * 0.65; + + vec3 reflected = getSkyColor(reflect(eye,n)); + vec3 refracted = u_sea_base + diffuse(normalize(n),l,80.0) * u_water_color * 0.12; + + vec3 color = mix(refracted,reflected,fresnel); + + float atten = max(1.0 - dot(dist,dist) * 0.001, 0.0); + color += u_water_color * (length(p) - u_sea_height) * 0.18 * atten; // + + color += vec3(specular(n,l,eye,20.0)); + + return color; + } + + void frag(v2f i) { + vec2 uv = vec2(i.v_uv.x * 0.5, i.v_uv.y * 0.5);// / iResolution.xy; + + vec3 pos = i.v_position; + vec3 dist = pos - camera_Position; + + float dis = EPS; + vec3 rayDir = normalize(dist); + + // Ray marching + for(int i = 0; i < MAX_ITR; i++) + { + if(dis < EPS || dis > MAX_DIS) { + break; + } + dis = map(pos, uv); + pos += dis * rayDir; + } + + if (dis >= EPS) + { + discard; + } + + vec3 lig = normalize(vec3(-1.0, -3, -4.5)); + vec2 eps = vec2(0.0, EPS); + vec3 normal = normalize(vec3( + map(pos + eps.yxx, uv) - map(pos - eps.yxx, uv), + map(pos + eps.xyx, uv) - map(pos - eps.xyx, uv), + map(pos + eps.xxy, uv) - map(pos - eps.xxy, uv) + )); + + vec3 col = getSeaColor(pos, normal, lig, rayDir, dist); + + gl_FragColor = vec4(col,1.0); + } + } + } +} +`; + +const gui = new dat.GUI(); +// create engine +WebGLEngine.create({ canvas: 'canvas', shaderLab }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // create camera + const cameraEntity = rootEntity.createChild('camera_entity'); + cameraEntity.transform.position = new Vector3(0, 0, 15); + cameraEntity.addComponent(Camera); + const orbitControl = cameraEntity.addComponent(OrbitControl); + orbitControl.minDistance = 15; + orbitControl.maxDistance = 15; + + // 初始化 shader + Shader.create(shaderSource); + + class ShaderMaterial extends Material { + constructor(engine: Engine) { + super(engine, Shader.find('customWater')); + + this.shaderData.setFloat('u_sea_height', 0.6); + this.shaderData.setFloat('u_water_scale', 0.2); + this.shaderData.setFloat('u_water_speed', 3.5); + this.shaderData.setColor('u_sea_base', new Color(0.1, 0.2, 0.22)); + this.shaderData.setColor('u_water_color', new Color(0.8, 0.9, 0.6)); + } + } + const material = new ShaderMaterial(engine); + + // 创建球体形的海面 + const sphereEntity = rootEntity.createChild('sphere'); + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 3, 50); + renderer.setMaterial(material); + + // 加载噪声纹理 + engine.resourceManager + .load({ + type: AssetType.Texture2D, + url: 'https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*AC4IQZ6mfCIAAAAAAAAAAAAAARQnAQ', + }) + .then((texture: Texture2D) => { + material.shaderData.setTexture('u_texture', texture); + engine.run(); + }); + + // debug + function openDebug() { + const shaderData = material.shaderData; + const baseColor = shaderData.getColor('u_sea_base'); + const waterColor = shaderData.getColor('u_water_color'); + const debug = { + sea_height: shaderData.getFloat('u_sea_height'), + water_scale: shaderData.getFloat('u_water_scale'), + water_speed: shaderData.getFloat('u_water_speed'), + sea_base: [baseColor.r * 255, baseColor.g * 255, baseColor.b * 255], + water_color: [waterColor.r * 255, waterColor.g * 255, waterColor.b * 255], + }; + + gui.add(debug, 'sea_height', 0, 3).onChange((v) => { + shaderData.setFloat('u_sea_height', v); + }); + gui.add(debug, 'water_scale', 0, 4).onChange((v) => { + shaderData.setFloat('u_water_scale', v); + }); + gui.add(debug, 'water_speed', 0, 4).onChange((v) => { + shaderData.setFloat('u_water_speed', v); + }); + gui.addColor(debug, 'sea_base').onChange((v) => { + baseColor.r = v[0] / 255; + baseColor.g = v[1] / 255; + baseColor.b = v[2] / 255; + }); + gui.addColor(debug, 'water_color').onChange((v) => { + waterColor.r = v[0] / 255; + waterColor.g = v[1] / 255; + waterColor.b = v[2] / 255; + }); + } + + openDebug(); +}); diff --git a/examples/shadow-basic.ts b/examples/shadow-basic.ts new file mode 100644 index 000000000..627b8bef0 --- /dev/null +++ b/examples/shadow-basic.ts @@ -0,0 +1,58 @@ +/** + * @title Shadow Basic + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*EXbeTbDNEvoAAAAAAAAAAAAADiR2AQ/original + */ + +import { FreeControl } from "@galacean/engine-toolkit-controls"; +import { + AmbientLight, + AssetType, + Camera, + DirectLight, + GLTFResource, + ShadowType, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +async function init() { + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + // Set shadow distance + scene.shadowDistance = 20; + + // Create root entity + const rootEntity = scene.createRootEntity(); + + // Create camera entity and component + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(3, 2.5, 0); + cameraEntity.transform.lookAt(new Vector3(0, 2, 0)); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(FreeControl).floorMock = false; + + // Create light entity and component + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0.5, 0.9, 0); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + const directLight = lightEntity.addComponent(DirectLight); + + // Enable shadow + directLight.shadowType = ShadowType.SoftLow; + + const glTFResource = await engine.resourceManager.load( + "https://gw.alipayobjects.com/os/bmw-prod/ca50859b-d736-4a3e-9fc3-241b0bd2afef.gltf" + ); + rootEntity.addChild(glTFResource.defaultSceneRoot); + + const ambientLight = await engine.resourceManager.load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/09904c03-0d23-4834-aa73-64e11e2287b0.bin", + }); + scene.ambientLight = ambientLight; + + engine.run(); +} +init(); diff --git a/examples/shadow-fade.ts b/examples/shadow-fade.ts new file mode 100644 index 000000000..8b8e463b2 --- /dev/null +++ b/examples/shadow-fade.ts @@ -0,0 +1,72 @@ +/** + * @title Shadow Fade + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*YIXASLTf0CkAAAAAAAAAAAAADiR2AQ/original + */ +import { + Animator, + Camera, + DirectLight, + GLTFResource, + Logger, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + ShadowResolution, + ShadowType, + Vector3, + WebGLEngine, + WebGLMode +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +const gui = new dat.GUI(); +/** + * Planar Shadow + */ + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + scene.shadowResolution = ShadowResolution.VeryHigh; + scene.shadowDistance = 10; + + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.setPosition(0, 5, 7); + const camera = cameraEntity.addComponent(Camera); + + const control = cameraEntity.addComponent(OrbitControl); + control.target = new Vector3(0, 0, 0); + control.zoomSpeed = 0.2; + const lightEntity = rootEntity.createChild("light_node"); + const light = lightEntity.addComponent(DirectLight); + lightEntity.transform.setPosition(-10, 10, 10); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + light.shadowType = ShadowType.SoftHigh; + + const planeEntity = rootEntity.createChild("plane_node"); + const renderer = planeEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createPlane(engine, 10, 10); + const planeMaterial = new PBRMaterial(engine); + renderer.setMaterial(planeMaterial); + + engine.resourceManager + .load("https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb") + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(asset.animations[0].name); + }); + + engine.run(); + + gui.add(scene, "shadowFadeBorder", 0, 1, 0.01); + gui.add(scene, "shadowDistance", 0, 100, 1); + gui.add(light, "shadowStrength", 0, 1, 0.01); +}); diff --git a/examples/skeleton-animation-additive.ts b/examples/skeleton-animation-additive.ts new file mode 100644 index 000000000..6ade8d98e --- /dev/null +++ b/examples/skeleton-animation-additive.ts @@ -0,0 +1,110 @@ +/** + * @title Animation Additive + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*2pFrR6RUdrYAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + Animator, + AnimatorControllerLayer, + AnimatorLayerBlendingMode, + AnimatorStateMachine, + Camera, + DirectLight, + GLTFResource, + Logger, + SystemInfo, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; + +const gui = new dat.GUI(); + +Logger.enable(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((gltfResource) => { + const { animations = [], defaultSceneRoot } = gltfResource; + const animator = defaultSceneRoot.getComponent(Animator); + const { animatorController } = animator; + + const animatorStateMachine = new AnimatorStateMachine(); + const additiveLayer = new AnimatorControllerLayer("additiveLayer"); + additiveLayer.stateMachine = animatorStateMachine; + additiveLayer.blendingMode = AnimatorLayerBlendingMode.Additive; + animatorController.addLayer(additiveLayer); + + const additivePoseNames = animations + .filter((clip) => clip.name.includes("pose")) + .map((clip) => clip.name); + + additivePoseNames.forEach((name) => { + const clip = animator.findAnimatorState(name).clip; + const newState = animatorStateMachine.addState(name); + newState.clipStartTime = 1; + newState.clip = clip; + }); + + rootEntity.addChild(defaultSceneRoot); + + animator.play("walk", 0); + animator.play("sad_pose", 1); + + initDatGUI(animator, animations, additiveLayer); + }); + + engine.run(); + + const initDatGUI = (animator, animations, additiveLayer) => { + const animationNames = animations + .filter((clip) => !clip.name.includes("pose")) + .map((clip) => clip.name); + const additivePoseNames = animations + .filter((clip) => clip.name.includes("pose")) + .map((clip) => clip.name); + + const debugInfo = { + animation: animationNames[4], + additive_pose: additivePoseNames[0], + additive_weight: 1, + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + animator.play(v, 0); + }); + + gui.add(debugInfo, "additive_pose", additivePoseNames).onChange((v) => { + animator.play(v, 1); + }); + + gui.add(debugInfo, "additive_weight", 0, 1).onChange((v) => { + additiveLayer.weight = v; + }); + + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +}); diff --git a/examples/skeleton-animation-blendShape.ts b/examples/skeleton-animation-blendShape.ts new file mode 100644 index 000000000..2c502c53e --- /dev/null +++ b/examples/skeleton-animation-blendShape.ts @@ -0,0 +1,54 @@ +/** + * @title Animation BlendShape + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*wn9UQ5ystoYAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Animator, + Camera, + DirectLight, + Logger, + SkinnedMeshRenderer, + SystemInfo, + Vector3, + WebGLEngine, + GLTFResource, +} from "@galacean/engine"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 1.0; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(-45, -135, 0)); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/746da3e3-fdc9-4155-8fee-0e2a97de4e72.glb" + ) + .then((asset) => { + const { defaultSceneRoot } = asset; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + const skinMeshRenderer = + defaultSceneRoot.getComponent(SkinnedMeshRenderer); + + skinMeshRenderer.blendShapeWeights[0] = 1.0; + + animator.play("TheWave"); + }); + + engine.run(); +}); diff --git a/examples/skeleton-animation-crossfade.ts b/examples/skeleton-animation-crossfade.ts new file mode 100644 index 000000000..e30677a58 --- /dev/null +++ b/examples/skeleton-animation-crossfade.ts @@ -0,0 +1,89 @@ +/** + * @title Animation CrossFade + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*sgp8SI6ZiusAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Animator, + Camera, + DirectLight, + Logger, + SystemInfo, + Vector3, + WebGLEngine, + GLTFResource, +} from "@galacean/engine"; +import * as dat from "dat.gui"; + +const gui = new dat.GUI(); + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((gltfResource) => { + const { animations = [], defaultSceneRoot } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play("agree"); + + initDatGUI(animator, animations); + }); + + engine.run(); + + const initDatGUI = (animator, animations) => { + const animationNames = animations + .filter((clip) => !clip.name.includes("pose")) + .map((clip) => clip.name); + + const debugInfo = { + animation: animationNames[0], + crossFade: true, + normalizedTransitionDuration: 0.5, + normalizedTimeOffset: 0, + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + const { crossFade, normalizedTransitionDuration, normalizedTimeOffset } = + debugInfo; + if (crossFade) { + animator.crossFade( + v, + normalizedTransitionDuration, + 0, + normalizedTimeOffset + ); + } else { + animator.play(v); + } + }); + + gui.add(debugInfo, "crossFade"); + gui.add(debugInfo, "normalizedTransitionDuration", 0, 1).name("过渡时间"); + gui.add(debugInfo, "normalizedTimeOffset", 0, 1).name("偏移时间"); + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +}); diff --git a/examples/skeleton-animation-customBlendShape.ts b/examples/skeleton-animation-customBlendShape.ts new file mode 100644 index 000000000..39adff844 --- /dev/null +++ b/examples/skeleton-animation-customBlendShape.ts @@ -0,0 +1,91 @@ +/** + * @title Animation CustomBlendShape + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*kapsTY3USw8AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + BlendShape, + Camera, + Logger, + ModelMesh, + SkinnedMeshRenderer, + SystemInfo, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("cameraNode"); + cameraEntity.transform.position = new Vector3(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + const meshEntity = rootEntity.createChild("meshEntity"); + const skinnedMeshRenderer = meshEntity.addComponent(SkinnedMeshRenderer); + const modelMesh = new ModelMesh(engine); + + // Set vertices data. + const positions = [ + new Vector3(-1.0, -1.0, 1.0), + new Vector3(1.0, -1.0, 1.0), + new Vector3(1.0, 1.0, 1.0), + new Vector3(1.0, 1.0, 1.0), + new Vector3(-1.0, 1.0, 1.0), + new Vector3(-1.0, -1.0, 1.0), + ]; + modelMesh.setPositions(positions); + + // Add SubMesh. + modelMesh.addSubMesh(0, 6); + + // Add BlendShape. + const deltaPositions = [ + new Vector3(0.0, 0.0, 0.0), + new Vector3(0.0, 0.0, 0.0), + new Vector3(-1.0, 0.0, 0.0), + new Vector3(-1.0, 0.0, 0.0), + new Vector3(1.0, 0.0, 0.0), + new Vector3(0.0, 0.0, 0.0), + ]; + const blendShape = new BlendShape("BlendShapeA"); + blendShape.addFrame(1.0, deltaPositions); + modelMesh.addBlendShape(blendShape); + + skinnedMeshRenderer.mesh = modelMesh; + skinnedMeshRenderer.setMaterial(new UnlitMaterial(engine)); + + // Upload data. + modelMesh.uploadData(false); + + engine.run(); + + // Use `blendShapeWeights` property to adjust the mesh to the target BlendShape + skinnedMeshRenderer.blendShapeWeights = new Float32Array([1.0]); + + // Add data GUI + addDataGUI(skinnedMeshRenderer); + + /** + * Add data GUI. + */ + function addDataGUI(skinnedMeshRenderer: SkinnedMeshRenderer): void { + const gui = new dat.GUI(); + const guiData = { + blendShapeWeights: 1.0, + }; + + gui.add(guiData, "blendShapeWeights", 0, 1).onChange((value: number) => { + skinnedMeshRenderer.blendShapeWeights[0] = value; + }); + } +}); diff --git a/examples/skeleton-animation-play.ts b/examples/skeleton-animation-play.ts new file mode 100644 index 000000000..e75fba003 --- /dev/null +++ b/examples/skeleton-animation-play.ts @@ -0,0 +1,70 @@ +/** + * @title Animation Play + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*4WYJTJefACQAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + Animator, + Camera, + DirectLight, + GLTFResource, + Logger, + SystemInfo, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +const gui = new dat.GUI(); + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/5e3c1e4e-496e-45f8-8e05-f89f2bd5e4a4.glb" + ) + .then((gltfResource) => { + const { animations = [], defaultSceneRoot } = gltfResource; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + animator.play("agree"); + + initDatGUI(animator, animations); + }); + + engine.run(); + + const initDatGUI = (animator, animations) => { + const animationNames = animations + .filter((clip) => !clip.name.includes("pose")) + .map((clip) => clip.name); + const debugInfo = { + animation: animationNames[0], + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + animator.play(v); + }); + + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +}); diff --git a/examples/skeleton-animation-reuse.ts b/examples/skeleton-animation-reuse.ts new file mode 100644 index 000000000..8be6e1a4d --- /dev/null +++ b/examples/skeleton-animation-reuse.ts @@ -0,0 +1,89 @@ +/** + * @title Animation Reuse + * @category Animation + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*mnVSQJk8jXgAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + Animator, + AssetPromise, + Camera, + DirectLight, + GLTFResource, + Logger, + SystemInfo, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +const gui = new dat.GUI(); + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.width = window.innerWidth * SystemInfo.devicePixelRatio; + engine.canvas.height = window.innerHeight * SystemInfo.devicePixelRatio; + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(0, 1, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl).target = new Vector3(0, 1, 0); + + const lightNode = rootEntity.createChild("light_node"); + lightNode.addComponent(DirectLight).intensity = 0.6; + lightNode.transform.lookAt(new Vector3(0, 0, 1)); + lightNode.transform.rotate(new Vector3(0, 90, 0)); + + const promises: AssetPromise[] = []; + // origin model + promises.push( + engine.resourceManager.load( + "https://gw.alipayobjects.com/os/OasisHub/6f5b1918-1380-4641-a57a-7507503a524c/data.gltf" + ) + ); + // animation + promises.push( + engine.resourceManager.load( + "https://gw.alipayobjects.com/os/OasisHub/9ef53086-67d4-4be6-bff8-449a8074a5bd/data.gltf" + ) + ); + + Promise.all(promises).then((resArr) => { + const modelGLTF = resArr[0]; + const animationGLTF = resArr[1]; + const { animations: originAnimations = [] } = modelGLTF; + const { animations = [] } = animationGLTF; + const { defaultSceneRoot } = modelGLTF; + rootEntity.addChild(defaultSceneRoot); + const animator = defaultSceneRoot.getComponent(Animator); + + const danceState = + animator.animatorController.layers[0].stateMachine.addState("dance"); + danceState.clip = animations[0]; + + animator.play("dance"); + + const animationNames = originAnimations.map((clip) => clip.name); + animationNames.push("dance"); + initDatGUI(animator, animationNames); + }); + + engine.run(); + + const initDatGUI = (animator, animationNames) => { + const debugInfo = { + animation: animationNames[1], + speed: 1, + }; + + gui.add(debugInfo, "animation", animationNames).onChange((v) => { + animator.play(v); + }); + + gui.add(debugInfo, "speed", -1, 1).onChange((v) => { + animator.speed = v; + }); + }; +}); diff --git a/examples/skeleton-viewer.ts b/examples/skeleton-viewer.ts new file mode 100644 index 000000000..1d6ee8db3 --- /dev/null +++ b/examples/skeleton-viewer.ts @@ -0,0 +1,46 @@ +/** + * @title Skeleton Viewer + * @category Toolkit + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*p-stTqq5NkEAAAAAAAAAAAAADiR2AQ/original + */ +import { + Animator, + Camera, + GLTFResource, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { SkeletonViewer } from "@galacean/engine-toolkit-skeleton-viewer"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootNode = scene.createRootEntity(); + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + // Create camera + const cameraEntity = rootNode.createChild("camera_node"); + cameraEntity.transform.position = new Vector3(10, 10, 30); + cameraEntity.addComponent(Camera); + const control = cameraEntity.addComponent(OrbitControl); + control.target.set(0, 3, 0); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/f40ef8dd-4c94-41d4-8fac-c1d2301b6e47.glb" + ) + .then((gltf) => { + const { defaultSceneRoot, animations } = gltf; + const animator = defaultSceneRoot.getComponent(Animator); + defaultSceneRoot.transform.setScale(0.1, 0.1, 0.1); + rootNode.addChild(defaultSceneRoot); + animator.play(animations[1].name); + + defaultSceneRoot.addComponent(SkeletonViewer); + }); + + // Run + engine.run(); +}); diff --git a/examples/sky-procedural.ts b/examples/sky-procedural.ts new file mode 100644 index 000000000..b7047a696 --- /dev/null +++ b/examples/sky-procedural.ts @@ -0,0 +1,110 @@ +/** + * @title Procedural Sky + * @category Sky + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*Zb3FRYQi9sIAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + BackgroundMode, + Camera, + DirectLight, + Entity, + Logger, + PrimitiveMesh, + SkyProceduralMaterial, + SunMode, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.addComponent(Camera).fieldOfView = 60; + cameraEntity.transform.setPosition(0, 0, 10); + + scene.ambientLight.diffuseSolidColor.set(1, 1, 1, 1); + + // Create sky + const background = scene.background; + background.mode = BackgroundMode.Sky; + + const sky = background.sky; + const skyMaterial = new SkyProceduralMaterial(engine); + sky.material = skyMaterial; + sky.mesh = PrimitiveMesh.createSphere(engine, 1, 64); + + // Create light + const lightEntity = rootEntity.createChild("light"); + lightEntity.transform.setPosition(0, 0.5, -1.0); + lightEntity.transform.lookAt(new Vector3(0, 0, 0)); + + lightEntity.addComponent(DirectLight); + + engine.run(); + + // Add debug GUI + addDebugGUI(lightEntity, skyMaterial); +}); + +function addDebugGUI( + lightEntity: Entity, + skyMaterial: SkyProceduralMaterial +): void { + const gui = new dat.GUI({ width: 360 }); + const skyTint = skyMaterial.skyTint; + const groundTint = skyMaterial.groundTint; + const debugInfos = { + "light rotation": lightEntity.transform.rotation.x, + "sun mode": skyMaterial.sunMode, + "sun size": skyMaterial.sunSize, + "sun size convergence": skyMaterial.sunSizeConvergence, + "sky tint": [skyTint.r * 255, skyTint.g * 255, skyTint.b * 255], + "ground tint": [groundTint.r * 255, groundTint.g * 255, groundTint.b * 255], + "atmosphere thickness": skyMaterial.atmosphereThickness, + exposure: skyMaterial.exposure, + }; + + gui + .add(debugInfos, "sun mode", { + None: SunMode.None, + Simple: SunMode.Simple, + HighQuality: SunMode.HighQuality, + }) + .onChange((v) => { + const sunMode = parseInt(v); + skyMaterial.sunMode = sunMode; + }); + + gui.add(debugInfos, "sun size", 0, 1).onChange((v) => { + skyMaterial.sunSize = v; + }); + + gui.add(debugInfos, "sun size convergence", 0, 20).onChange((v) => { + skyMaterial.sunSizeConvergence = v; + }); + + gui.add(debugInfos, "light rotation", -180, 180).onChange((v) => { + lightEntity.transform.rotation.x = v; + }); + + gui.addColor(debugInfos, "sky tint").onChange((v) => { + skyMaterial.skyTint.set(v[0] / 255, v[1] / 255, v[2] / 255, 1.0); + }); + + gui.addColor(debugInfos, "ground tint").onChange((v) => { + skyMaterial.groundTint.set(v[0] / 255, v[1] / 255, v[2] / 255, 1.0); + }); + + gui.add(debugInfos, "atmosphere thickness", 0, 5).onChange((v) => { + skyMaterial.atmosphereThickness = v; + }); + + gui.add(debugInfos, "exposure", 0, 8).onChange((v) => { + skyMaterial.exposure = v; + }); +} diff --git a/examples/spine-animation.ts b/examples/spine-animation.ts new file mode 100644 index 000000000..bc44ad91a --- /dev/null +++ b/examples/spine-animation.ts @@ -0,0 +1,38 @@ +/** + * @title Spine Animation + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*IALeTYOXMXwAAAAAAAAAAAAADiR2AQ/original + */ +import { Camera, Logger, Vector3, WebGLEngine, Entity } from "@galacean/engine"; +import { SpineRenderer } from "@galacean/engine-spine"; + +Logger.enable(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 60); + + engine.resourceManager + .load({ + url: "https://mmtcdp.stable.alipay.net/oasis_be/afts/file/A*jceoSrUXbUYAAAAAAAAAAAAADnN-AQ/spineboy.json", + type: "spine", + }) + .then((spineResource: any) => { + const spineEntity = rootEntity.createChild("spine"); + spineEntity.transform.setPosition(0, -18, 0); + const spineRenderer = spineEntity.addComponent(SpineRenderer); + spineRenderer.scale = 0.05; + spineRenderer.animationName = "walk"; + spineRenderer.resource = spineResource; + }); + + engine.run(); +}); diff --git a/examples/spine-change-attachment.ts b/examples/spine-change-attachment.ts new file mode 100644 index 000000000..cea58fb2f --- /dev/null +++ b/examples/spine-change-attachment.ts @@ -0,0 +1,79 @@ +/** + * @title Spine Change Attachment + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*VUeSSbViZe8AAAAAAAAAAAAADiR2AQ/original + */ +import { Camera, Logger, Vector3, WebGLEngine, Entity } from "@galacean/engine"; +import { SpineRenderer } from "@galacean/engine-spine"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 60); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/OasisHub/01c23386-ae6d-41b3-ab51-08b023a0dc3f/1629864253199.json", + "https://gw.alipayobjects.com/os/OasisHub/27b76dd2-01b3-4282-83e8-17be20b910ae/1629864253200.atlas", + "https://gw.alipayobjects.com/zos/OasisHub/99bc4468-02c6-4f35-8fef-ac5a711fc641/1629864253200.png", + ], + type: "spine", + }) + .then((spineResource: any) => { + const spineEntity = rootEntity.createChild("spine"); + rootEntity.addChild(spineEntity); + const spineRenderer = spineEntity.addComponent(SpineRenderer); + spineRenderer.scale = 0.05; + spineRenderer.animationName = "walk"; + spineRenderer.resource = spineResource; + const { skeleton, state, skeletonData } = spineRenderer.spineAnimation; + spineEntity.transform.setPosition(0, -10, 0); + state.setAnimation(0, "sneering", true); + skeleton.setSkinByName("fullskin/0101"); // 1. Set the active skin + skeleton.setSlotsToSetupPose(); // 2. Use setup pose to set base attachments. + state.apply(skeleton); + const slotName = "fBody"; + const info = { + 更换衣服部件: "fullskin/0101", + }; + gui + .add(info, "更换衣服部件", [ + "fullskin/0101", + "fullskin/autumn", + "fullskin/carnival", + "fullskin/fishing", + "fullskin/football", + "fullskin/newyear", + "fullskin/painter", + "fullskin/snowman", + ]) + .onChange((skinName) => { + const currentSkin = skeleton.skin; + const slotIndex = skeleton.findSlotIndex(slotName); + const changeSkin = skeletonData.findSkin(skinName); + const changeAttachment = changeSkin.getAttachment( + slotIndex, + slotName + ); + if (changeAttachment) { + currentSkin.removeAttachment(slotIndex, slotName); + currentSkin.setAttachment(slotIndex, slotName, changeAttachment); + } + }); + }); + + engine.run(); +}); diff --git a/examples/spine-hack-slot-texture.ts b/examples/spine-hack-slot-texture.ts new file mode 100644 index 000000000..7bcd7b133 --- /dev/null +++ b/examples/spine-hack-slot-texture.ts @@ -0,0 +1,141 @@ +/** + * @title Spine Hack Slot Texture + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*_wfjSYhksRYAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + Logger, + Vector3, + WebGLEngine, + Entity, + AssetType, + LoadItem, +} from "@galacean/engine"; +import { SpineRenderer } from "@galacean/engine-spine"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 60); + + const resource = generateSkinResource(); + const spineResource = { + urls: [ + "https://gw.alipayobjects.com/os/OasisHub/e675c9e1-2b19-4940-b8ed-474792e613d7/1629603245094.json", + "https://gw.alipayobjects.com/os/OasisHub/994dfadc-c498-4210-b9ba-0c3deed61fc5/1629603245095.atlas", + "https://gw.alipayobjects.com/zos/OasisHub/b52768b0-0374-4c64-a1bd-763b1a37ee5f/1629603245095.png", + ], + type: "spine", + }; + resource.unshift(spineResource); + engine.resourceManager.load(resource).then((res: any) => { + const spineResource = res[0]; + const spineEntity = rootEntity.createChild("spine"); + spineEntity.transform.setPosition(0, -12, 0); + rootEntity.addChild(spineEntity); + const spineRenderer = spineEntity.addComponent(SpineRenderer); + spineRenderer.resource = spineResource + spineRenderer.skinName = "skin1"; + spineRenderer.animationName = "02_walk"; + const { spineAnimation } = spineRenderer; + spineAnimation.scale = 0.07; + spineAnimation.addSeparateSlot("defult/head_hair"); + spineAnimation.addSeparateSlot("defult/arm_rigth_weapon"); + spineAnimation.addSeparateSlot("defult/Sleeveless_01"); + + const textures = []; + for (let i = 1; i < res.length; i += 1) { + textures.push(res[i]); + } + const info = { + 换头饰: "hair_0", + 换衣服: "clothes_0", + 换武器: "weapon_0", + }; + + const hatConfig = []; + const clothConfig = []; + const weaponConfig = []; + for (let i = 0; i < resource.length; i++) { + hatConfig.push(`hair_${i}`); + clothConfig.push(`clothes_${i}`); + weaponConfig.push(`weapon_${i}`); + } + gui.add(info, "换头饰", hatConfig).onChange((v) => { + changeSlotTexture(v, textures, spineAnimation); + }); + gui.add(info, "换衣服", clothConfig).onChange((v) => { + changeSlotTexture(v, textures, spineAnimation); + }); + gui.add(info, "换武器", weaponConfig).onChange((v) => { + changeSlotTexture(v, textures, spineAnimation); + }); + }); + + engine.run(); + + function generateSkinResource(): LoadItem[] { + const skinImgs = [ + "https://gw.alicdn.com/imgextra/i4/O1CN01NVzIQ61Hf7DT0jDWS_!!6000000000784-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01g3HnB21FPQPnjavP3_!!6000000000479-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01CvmDQl1gRFcWeh3Na_!!6000000004138-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01BviZcq1Rc2iTh127L_!!6000000002131-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01mkkLpR1ihrDHyYr1H_!!6000000004445-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i2/O1CN019ENsCO2992jTG9RGD_!!6000000008024-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i4/O1CN01fzyJFg1cNoBGRLSCI_!!6000000003589-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i4/O1CN01duImZL1J8iQk2YzEj_!!6000000000984-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i2/O1CN01b23DDj1QD1SoNL7ua_!!6000000001941-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01powK3y29HHrZCBnbg_!!6000000008042-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01n7R3dE1IRfCVUgvhE_!!6000000000890-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01t0nsyV24AoBFhIfyZ_!!6000000007351-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i4/O1CN01mYwBUD1eBYp2rE0qV_!!6000000003833-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01ks7zZs1mbgKwBjlFS_!!6000000004973-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01mgFHl5262gO0L0JeR_!!6000000007604-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01SJbFkU1udWrRhXPbd_!!6000000006060-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01VGL8pe26qbYegHClp_!!6000000007713-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i2/O1CN01EeZs6N1auCy4QbXiY_!!6000000003389-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01DOfF5J1UTkOMHSnwV_!!6000000002519-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01iWGD1h1G0ytSTLs67_!!6000000000561-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01xjhSTG245JQVrtEhL_!!6000000007339-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01NJAp7c22RdV8PC1Dq_!!6000000007117-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i1/O1CN01A2Mdh01INXdP46W6B_!!6000000000881-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01AqHn4524RIRMTuuNH_!!6000000007387-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i4/O1CN01yU8Z771SPVUUS0Die_!!6000000002239-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01orLkIg1JOkIFur5Fj_!!6000000001019-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i3/O1CN01jRRXrV1b4HgOXGqov_!!6000000003411-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i2/O1CN01XOchrA1Mh0wFgddGl_!!6000000001465-2-tps-802-256.png", + "https://gw.alicdn.com/imgextra/i2/O1CN01zPPHrD1pIOVHtvDqD_!!6000000005337-2-tps-802-256.png", + ]; + return skinImgs.map((item) => { + return { + type: AssetType.Texture2D, + url: item, + }; + }); + } + + function changeSlotTexture(selectItem, textures, spineAnimation) { + const slotNameMap = { + hair: "defult/head_hair", + weapon: "defult/arm_rigth_weapon", + clothes: "defult/Sleeveless_01", + }; + const slotKey = selectItem.split("_")[0]; + const slotName = slotNameMap[slotKey]; + const index = selectItem.split("_")[1]; + spineAnimation.hackSeparateSlotTexture(slotName, textures[index]); + } +}); diff --git a/examples/spine-performance.ts b/examples/spine-performance.ts new file mode 100644 index 000000000..f1a61c459 --- /dev/null +++ b/examples/spine-performance.ts @@ -0,0 +1,47 @@ +/** + * @title Spine + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*6xrGR6nr1c0AAAAAAAAAAAAADiR2AQ/original + */ +import { Camera, Vector3, WebGLEngine } from "@galacean/engine"; +import { SpineRenderer } from "@galacean/engine-spine"; +import { Stats } from "@galacean/engine-toolkit-stats"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 110); + camera.farClipPlane = 200; + cameraEntity.addComponent(Stats); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/OasisHub/a66ef194-6bc8-4325-9a59-6ea9097225b1/1620888427489.json", + "https://gw.alipayobjects.com/os/OasisHub/a1e3e67b-a783-4832-ba1b-37a95bd55291/1620888427490.atlas", + "https://gw.alipayobjects.com/zos/OasisHub/a3ca8f62-1068-43a5-bb64-5c9a0f823dde/1620888427490.png", + ], + type: "spine", + }) + .then((spineResouce: any) => { + const spineEntity = rootEntity.createChild("spine"); + const spineRenderer = spineEntity.addComponent(SpineRenderer); + spineRenderer.resource = spineResouce; + spineRenderer.scale = 0.01; + spineRenderer.animationName = "walk"; + for (let i = -5; i < 5; i++) { + for (let j = -5; j < 5; j++) { + const clone = spineEntity.clone(); + clone.transform.setPosition(8 * i, 8 * j, 0); + rootEntity.addChild(clone); + } + } + }); + + engine.run(); +}); diff --git a/examples/spine-skin-change.ts b/examples/spine-skin-change.ts new file mode 100644 index 000000000..e071c0ed9 --- /dev/null +++ b/examples/spine-skin-change.ts @@ -0,0 +1,66 @@ +/** + * @title Spine Change Skin + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*6RVDRrZkOlgAAAAAAAAAAAAADiR2AQ/original + */ +import { Camera, Logger, Vector3, WebGLEngine, Entity } from "@galacean/engine"; +import { SpineRenderer } from "@galacean/engine-spine"; +import * as dat from "dat.gui"; + +Logger.enable(); + +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // camera + const cameraEntity = rootEntity.createChild("camera_node"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.position = new Vector3(0, 0, 60); + + engine.resourceManager + .load({ + urls: [ + "https://gw.alipayobjects.com/os/OasisHub/c51a45ef-f248-4835-b601-6d31a901f298/1629713824525.json", + "https://gw.alipayobjects.com/os/OasisHub/b016738d-173a-4506-9112-045ebba84d82/1629713824527.atlas", + "https://gw.alipayobjects.com/zos/OasisHub/747a94f3-8734-47b3-92b3-2d7fe2d36e58/1629713824527.png", + ], + type: "spine", + }) + .then((spineResource: any) => { + const spineEntity = rootEntity.createChild("spine"); + rootEntity.addChild(spineEntity); + const spineRenderer = spineEntity.addComponent(SpineRenderer); + spineRenderer.resource = spineResource; + const spineAnimation = spineRenderer.spineAnimation; + const { skeleton, state } = spineAnimation; + spineEntity.transform.setPosition(0, -18, 0); + state.setAnimation(0, "dance", true); + skeleton.setSkinByName("girl"); // 1. Set the active skin + skeleton.setSlotsToSetupPose(); // 2. Use setup pose to set base attachments. + state.apply(skeleton); + spineAnimation.scale = 0.05; + const info = { + skin: "girl", + }; + gui + .add(info, "skin", [ + "girl", + "girl-blue-cape", + "girl-spring-dress", + "boy", + ]) + .onChange((skinName) => { + skeleton.setSkinByName(skinName); // 1. Set the active skin + skeleton.setSlotsToSetupPose(); // 2. Use setup pose to set base attachments. + state.apply(skeleton); + }); + }); + + engine.run(); +}); diff --git a/examples/sprite-atlas.ts b/examples/sprite-atlas.ts new file mode 100644 index 000000000..7a7993392 --- /dev/null +++ b/examples/sprite-atlas.ts @@ -0,0 +1,117 @@ +/** + * @title Sprite Atlas + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*NLhMT5aeKt4AAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + Camera, + Sprite, + SpriteAtlas, + SpriteRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 4); + cameraEntity.addComponent(Camera).isOrthographic = true; + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/da0bccd4-020a-41d5-82e0-a04f4413d9a6.atlas", + type: AssetType.SpriteAtlas, + }) + .then((atlas) => { + const from = new Vector3(); + const to = new Vector3(); + // Draw the fence. + let sprite = atlas.getSprite("terrains-5"); + addGroupSpriteRenderer(sprite, from.set(-6, -6, 0), to.set(6, -6, 0)); + addGroupSpriteRenderer(sprite, from.set(-6, 6, 0), to.set(6, 6, 0)); + addGroupSpriteRenderer(sprite, from.set(-6, -5, 0), to.set(-6, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(6, -5, 0), to.set(6, 5, 0)); + + // Draw the walls. + sprite = atlas.getSprite("terrains-3"); + addGroupSpriteRenderer(sprite, from.set(-5, -2, 0), to.set(-5, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(-4, -3, 0), to.set(-4, -1, 0)); + addSpriteRenderer(sprite, from.set(-3, -2, 0)); + addGroupSpriteRenderer(sprite, from.set(-2, -3, 0), to.set(-2, -2, 0)); + addGroupSpriteRenderer(sprite, from.set(-1, -2, 0), to.set(-1, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(5, -2, 0), to.set(5, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(4, -3, 0), to.set(4, -1, 0)); + addSpriteRenderer(sprite, from.set(3, -2, 0)); + addGroupSpriteRenderer(sprite, from.set(2, -3, 0), to.set(2, -2, 0)); + addGroupSpriteRenderer(sprite, from.set(1, -2, 0), to.set(1, 5, 0)); + + // Draw the ground. + sprite = atlas.getSprite("terrains-0"); + addGroupSpriteRenderer(sprite, from.set(0, -5, 0), to.set(0, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(-1, -3, 0), to.set(1, -3, 0)); + + // Draw the magma. + sprite = atlas.getSprite("terrains-45"); + addGroupSpriteRenderer(sprite, from.set(-5, -5, 0), to.set(-1, -4, 0)); + addGroupSpriteRenderer(sprite, from.set(-4, -3, 0), to.set(-4, -3, 0)); + addSpriteRenderer(sprite, from.set(-5, -3, 0)); + addSpriteRenderer(sprite, from.set(-3, -3, 0)); + addGroupSpriteRenderer(sprite, from.set(1, -5, 0), to.set(5, -4, 0)); + addGroupSpriteRenderer(sprite, from.set(4, -3, 0), to.set(4, -3, 0)); + addSpriteRenderer(sprite, from.set(5, -3, 0)); + addSpriteRenderer(sprite, from.set(3, -3, 0)); + + // Draw the river. + sprite = atlas.getSprite("terrains-46"); + addGroupSpriteRenderer(sprite, from.set(-4, 0, 0), to.set(-2, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(-3, -1, 0), to.set(-2, -1, 0)); + addGroupSpriteRenderer(sprite, from.set(2, 0, 0), to.set(4, 5, 0)); + addGroupSpriteRenderer(sprite, from.set(2, -1, 0), to.set(3, -1, 0)); + + // Draw the npcs. + addSpriteRenderer(atlas.getSprite("npcs-0"), from.set(0, -4, 1)); + addSpriteRenderer(atlas.getSprite("npcs-7"), from.set(-1, -3, 1)); + }); + + /** + * Draw a set of items. + * @param spriteName - The name of the sprite resource used for drawing + * @param from - Starting point of drawing + * @param to - End point of drawing + */ + function addGroupSpriteRenderer( + sprite: Sprite, + from: Vector3, + to: Vector3 + ): void { + const { x: fromX, y: fromY } = from; + const { x: toX, y: toY } = to; + for (let i = fromX, n = toX; i <= n; i++) { + for (let j = fromY, m = toY; j <= m; j++) { + addSpriteRenderer(sprite, from.set(i, j, 0)); + } + } + } + + /** + * Draw an item. + * @param spriteName - The name of the sprite resource used for drawing + * @param position - Position of drawing + */ + function addSpriteRenderer(sprite: Sprite, position: Vector3): void { + const spriteEntity = rootEntity.createChild(); + spriteEntity.transform.position = position; + spriteEntity.transform.scale.set(100 / 32, 100 / 32, 100 / 32); + spriteEntity.addComponent(SpriteRenderer).sprite = sprite; + } + + engine.run(); +}); diff --git a/examples/sprite-color.ts b/examples/sprite-color.ts new file mode 100644 index 000000000..613cbdc2b --- /dev/null +++ b/examples/sprite-color.ts @@ -0,0 +1,62 @@ +/** + * @title Sprite Color + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*7alGRZp4EusAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Color, + Entity, + Sprite, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*KjnzTpE8LdAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create origin sprite entity. + const spriteEntity = new Entity(engine, "spriteColor"); + const spriteRenderer = spriteEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = new Sprite(engine, texture); + const color = new Color(); + // Display normal + addColorEntity(spriteEntity, -20, color.set(1, 1, 1, 1)); + // Display red + addColorEntity(spriteEntity.clone(), -10, color.set(1, 0, 0, 1)); + // Display green + addColorEntity(spriteEntity.clone(), 0, color.set(0, 1, 0, 1)); + // Display blue + addColorEntity(spriteEntity.clone(), 10, color.set(0, 0, 1, 1)); + // Display alpha + addColorEntity(spriteEntity.clone(), 20, color.set(1, 1, 1, 0.2)); + }); + + engine.run(); + + function addColorEntity(entity: Entity, posX: number, color: Color): void { + rootEntity.addChild(entity); + entity.transform.setPosition(posX, 0, 0); + entity.getComponent(SpriteRenderer).color = color; + } +}); diff --git a/examples/sprite-drawMode.ts b/examples/sprite-drawMode.ts new file mode 100644 index 000000000..3555f3d71 --- /dev/null +++ b/examples/sprite-drawMode.ts @@ -0,0 +1,316 @@ +/** + * @title Sprite Draw Mode + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*KllLQLmE3kAAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + Color, + Entity, + Logger, + MathUtil, + MeshRenderer, + MeshTopology, + ModelMesh, + Script, + Sprite, + SpriteDrawMode, + SpriteRenderer, + SpriteTileMode, + SubMesh, + Texture2D, + UnlitMaterial, + Vector4, + WebGLEngine, +} from "@galacean/engine"; + +Logger.enable(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + // Create engine object. + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 5); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + camera.orthographicSize = 5; + + let spriteSlice: Sprite; + let spriteTile: Sprite; + engine.resourceManager + // @ts-ignore + .load([ + { + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*0vm_SJVssKAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + url: "https://mdn.alipayobjects.com/huamei_jvf0dp/afts/img/A*5whjSZVzm_kAAAAAAAAAAAAADleLAQ/original", + type: AssetType.Texture2D, + }, + ]) + .then((textureArr) => { + spriteSlice = new Sprite(engine, textureArr[0]); + spriteTile = new Sprite(engine, textureArr[1]); + // Create origin sprite entity. + const spriteEntity = rootEntity.createChild("spriteSlice"); + const spriteRenderer = spriteEntity.addComponent(SpriteRenderer); + const sprite = (spriteRenderer.sprite = spriteSlice); + spriteRenderer.drawMode = SpriteDrawMode.Sliced; + sprite.border = new Vector4(); + addDataGUI(spriteEntity); + }); + + engine.run(); + + /** + * Auxiliary display. + */ + class TriangleScript extends Script { + private triangleEntity: Entity; + private modelMesh: ModelMesh; + private targetSpriteRenderer: SpriteRenderer; + + onAwake(): void { + const { engine, entity } = this; + const triangleEntity = (this.triangleEntity = entity.createChild("tag")); + const meshRenderer = triangleEntity.addComponent(MeshRenderer); + meshRenderer.mesh = this.modelMesh = new ModelMesh(engine, "tag"); + // @ts-ignore + this.modelMesh._primitive.enableVAO = false; + const material = new UnlitMaterial(engine); + material.baseColor = new Color(1, 0, 0, 1); + meshRenderer.setMaterial(material); + this.targetSpriteRenderer = entity.getComponent(SpriteRenderer); + } + + setShow(value: boolean) { + this.enabled = this.triangleEntity.isActive = value; + } + + onUpdate(): void { + const { modelMesh, targetSpriteRenderer } = this; + const { positions, vertexCount, triangles } = + // @ts-ignore + targetSpriteRenderer._verticesData; + if (vertexCount > 0) { + const trianglesCount = + targetSpriteRenderer.drawMode === SpriteDrawMode.Sliced + ? (vertexCount * 27) / 4 + : vertexCount * 3; + const myTriangles = + modelMesh.getIndices() || new Uint16Array(Math.floor(4096 * 3)); + const myPositions = modelMesh.getPositions() || []; + for (let i = 0; i < vertexCount; i++) { + if (myPositions[i]) { + myPositions[i].copyFrom(positions[i]); + } else { + myPositions[i] = positions[i].clone(); + } + } + for (let i = 0, l = trianglesCount / 6; i < l; i++) { + myTriangles[6 * i] = triangles[i * 3]; + myTriangles[6 * i + 1] = triangles[i * 3 + 1]; + myTriangles[6 * i + 2] = triangles[i * 3 + 1]; + myTriangles[6 * i + 3] = triangles[i * 3 + 2]; + myTriangles[6 * i + 4] = triangles[i * 3 + 2]; + myTriangles[6 * i + 5] = triangles[i * 3]; + } + modelMesh.setPositions(myPositions); + modelMesh.setIndices(myTriangles); + const subMesh = modelMesh.subMesh; + if (subMesh) { + subMesh.count = trianglesCount; + } else { + modelMesh.addSubMesh( + new SubMesh(0, trianglesCount, MeshTopology.Lines) + ); + } + modelMesh.uploadData(false); + } + } + } + + /** + * Add data GUI. + */ + function addDataGUI(entity: Entity) { + const spriteRenderer = entity.getComponent(SpriteRenderer); + const triangleScript = entity.addComponent(TriangleScript); + triangleScript.setShow(false); + const sprite = spriteRenderer.sprite; + const border = sprite.border; + const gui = new dat.GUI(); + const defaultWidth = spriteRenderer.width; + const defaultHeight = spriteRenderer.height; + const guiData = { + drawMode: "Slice", + left: 0, + bottom: 0, + right: 0, + top: 0, + showTriangle: false, + width: defaultWidth, + height: defaultHeight, + tiledMode: "Continuous", + threshold: 0.5, + reset: () => { + spriteRenderer.width = guiData.width = defaultWidth; + spriteRenderer.height = guiData.height = defaultHeight; + spriteRenderer.tileMode = SpriteTileMode.Continuous; + spriteRenderer.tiledAdaptiveThreshold = 0.5; + sprite.border = border.set(0, 0, 0, 0); + guiData.tiledMode = "Continuous"; + guiData.threshold = 0.5; + guiData.left = guiData.bottom = guiData.right = guiData.top = 0; + guiData.showTriangle = false; + if (spriteRenderer.drawMode === SpriteDrawMode.Tiled) { + show(tileModeGui); + hide(tiledAdaptiveThresholdGui); + } else { + hide(tileModeGui); + hide(tiledAdaptiveThresholdGui); + } + }, + }; + + function hide(gui) { + gui.__li.style.display = "none"; + } + function show(gui) { + gui.__li.style.display = "block"; + } + + const rendererFolder = gui.addFolder("SpriteRenderer"); + rendererFolder.open(); + rendererFolder + .add(guiData, "drawMode", ["Simple", "Slice", "Tiled"]) + .onChange((value: string) => { + switch (value) { + case "Simple": + spriteRenderer.drawMode = SpriteDrawMode.Simple; + hide(tileModeGui); + hide(tiledAdaptiveThresholdGui); + spriteRenderer.sprite = spriteSlice; + break; + case "Slice": + spriteRenderer.drawMode = SpriteDrawMode.Sliced; + hide(tileModeGui); + hide(tiledAdaptiveThresholdGui); + spriteRenderer.sprite = spriteSlice; + break; + case "Tiled": + spriteRenderer.drawMode = SpriteDrawMode.Tiled; + spriteRenderer.sprite = spriteTile; + show(tileModeGui); + if (guiData.tiledMode === "Adaptive") { + show(tiledAdaptiveThresholdGui); + } else { + hide(tiledAdaptiveThresholdGui); + } + break; + default: + break; + } + }) + .listen(); + + const tileModeGui = rendererFolder + .add(guiData, "tiledMode", ["Adaptive", "Continuous"]) + .onChange((value: string) => { + switch (value) { + case "Adaptive": + spriteRenderer.tileMode = SpriteTileMode.Adaptive; + show(tiledAdaptiveThresholdGui); + break; + case "Continuous": + spriteRenderer.tileMode = SpriteTileMode.Continuous; + hide(tiledAdaptiveThresholdGui); + break; + default: + break; + } + }) + .listen(); + + const tiledAdaptiveThresholdGui = rendererFolder + .add(guiData, "threshold", 0.0, 1.0, 0.01) + .onChange((value: number) => { + spriteRenderer.tiledAdaptiveThreshold = value; + }); + + rendererFolder + .add( + guiData, + "width", + defaultWidth / 5, + defaultWidth * 20, + defaultWidth / 10 + ) + .onChange((value: number) => { + spriteRenderer.width = value; + }) + .listen(); + rendererFolder + .add( + guiData, + "height", + defaultHeight / 5, + defaultHeight * 20, + defaultHeight / 10 + ) + .onChange((value: number) => { + spriteRenderer.height = value; + }) + .listen(); + + const spriteFolder = gui.addFolder("Sprite Border"); + spriteFolder.open(); + spriteFolder + .add(guiData, "left", 0.0, 1.0, 0.01) + .onChange((value: number) => { + guiData.left = border.x = MathUtil.clamp(value, 0, 1 - border.z); + sprite.border = border; + }) + .listen(); + spriteFolder + .add(guiData, "bottom", 0.0, 1.0, 0.01) + .onChange((value: number) => { + guiData.bottom = border.y = MathUtil.clamp(value, 0, 1 - border.w); + sprite.border = border; + }) + .listen(); + spriteFolder + .add(guiData, "right", 0.0, 1.0, 0.01) + .onChange((value: number) => { + guiData.right = border.z = MathUtil.clamp(value, 0, 1 - border.x); + sprite.border = border; + }) + .listen(); + spriteFolder + .add(guiData, "top", 0.0, 1.0, 0.01) + .onChange((value: number) => { + guiData.top = border.w = MathUtil.clamp(value, 0, 1 - border.y); + sprite.border = border; + }) + .listen(); + gui + .add(guiData, "showTriangle") + .onChange((value: boolean) => { + triangleScript.setShow(value); + }) + .listen(); + + gui.add(guiData, "reset"); + hide(tileModeGui); + hide(tiledAdaptiveThresholdGui); + return guiData; + } +}); diff --git a/examples/sprite-flip.ts b/examples/sprite-flip.ts new file mode 100644 index 000000000..3eb44030c --- /dev/null +++ b/examples/sprite-flip.ts @@ -0,0 +1,68 @@ +/** + * @title Sprite Flip + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*rJy-SY0iivEAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Entity, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*KjnzTpE8LdAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create origin sprite entity. + const spriteEntity = new Entity(engine, "spriteFlip"); + const spriteRenderer = spriteEntity.addComponent(SpriteRenderer); + spriteRenderer.sprite = new Sprite(engine, texture); + + // Display normal. + addFlipEntity(spriteEntity, -15, false, false); + // Display flip x. + addFlipEntity(spriteEntity.clone(), -5, true, false); + // Display flip y. + addFlipEntity(spriteEntity.clone(), 5, false, true); + // Display flip x and y. + addFlipEntity(spriteEntity.clone(), 15, true, true); + }); + + engine.run(); + + /** + * Add flip entity. + */ + function addFlipEntity( + entity: Entity, + posX: number, + flipX: boolean, + flipY: boolean + ): void { + rootEntity.addChild(entity); + entity.transform.setPosition(posX, 0, 0); + const flipRenderer = entity.getComponent(SpriteRenderer); + flipRenderer.flipX = flipX; + flipRenderer.flipY = flipY; + } +}); diff --git a/examples/sprite-mask.ts b/examples/sprite-mask.ts new file mode 100644 index 000000000..bca7246d6 --- /dev/null +++ b/examples/sprite-mask.ts @@ -0,0 +1,160 @@ +/** + * @title Sprite Mask + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*OLkqSKvnD9kAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Entity, + Script, + Sprite, + SpriteMask, + SpriteMaskInteraction, + SpriteMaskLayer, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create sprite and mask + engine.resourceManager + .load([ + { + // Sprite texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*rgNGR4Vb7lQAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + // Mask texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*qyhFT5Un5AgAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + // Mask texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*pgrpQIneqSUAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + ]) + .then((textures: Texture2D[]) => { + const pos = new Vector3(); + const scale = new Vector3(); + + // Create sprites. + const sprite = new Sprite(engine, textures[0]); + const maskSprite0 = new Sprite(engine, textures[1]); + const maksSprite1 = new Sprite(engine, textures[2]); + + // Show inside mask. + pos.set(-5, 0, 0); + scale.set(2, 2, 2); + addSpriteRenderer( + pos, + scale, + sprite, + SpriteMaskInteraction.VisibleInsideMask, + SpriteMaskLayer.Layer0 + ); + addMask(pos, maskSprite0, SpriteMaskLayer.Layer0, ScaleScript); + + // Show outside mask. + pos.set(5, 0, 0); + scale.set(2, 2, 2); + addSpriteRenderer( + pos, + scale, + sprite, + SpriteMaskInteraction.VisibleOutsideMask, + SpriteMaskLayer.Layer1 + ); + addMask(pos, maksSprite1, SpriteMaskLayer.Layer1, RotationScript); + }); + + engine.run(); + + /** + * Add sprite renderer and set mask interaction and layer. + */ + function addSpriteRenderer( + pos: Vector3, + scale: Vector3, + sprite: Sprite, + maskInteraction: SpriteMaskInteraction, + maskLayer: number + ): void { + const entity = rootEntity.createChild("Sprite"); + const renderer = entity.addComponent(SpriteRenderer); + const { transform } = entity; + + transform.position = pos; + transform.scale = scale; + renderer.sprite = sprite; + renderer.maskInteraction = maskInteraction; + renderer.maskLayer = maskLayer; + } + + /** + * Add sprite mask and set influence layers, include mask animation script. + */ + function addMask( + pos: Vector3, + sprite: Sprite, + influenceLayers: number, + scriptType: new (entity: Entity) => T + ): void { + const entity = rootEntity.createChild("Mask"); + const mask = entity.addComponent(SpriteMask); + + entity.addComponent(scriptType); + entity.transform.position = pos; + mask.sprite = sprite; + mask.influenceLayers = influenceLayers; + } + + class ScaleScript extends Script { + private _scaleSpeed: number = 0.01; + + /** + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + const { transform } = this.entity; + let curScale = transform.scale.x; + + if (curScale <= 0 || curScale >= 2) { + this._scaleSpeed *= -1; + } + + curScale += this._scaleSpeed; + transform.setScale(curScale, curScale, curScale); + } + } + + class RotationScript extends Script { + private _rotationSpeed: number = -0.5; + + /** + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + this.entity.transform.rotate(0, 0, this._rotationSpeed); + } + } +}); diff --git a/examples/sprite-material-blur.ts b/examples/sprite-material-blur.ts new file mode 100644 index 000000000..30624ad34 --- /dev/null +++ b/examples/sprite-material-blur.ts @@ -0,0 +1,166 @@ +/** + * @title Sprite Material Blur + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*mcarQpZIA3QAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + BlendFactor, + BlendOperation, + Camera, + CullMode, + Entity, + Material, + RenderQueueType, + Shader, + Sprite, + SpriteRenderer, + Texture2D, + TextureWrapMode, + Vector2, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 20); + cameraEntity.addComponent(Camera).isOrthographic = true; + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*L2GNRLWn9EAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create origin sprite entity. + const texSize = new Vector2(texture.width, texture.height); + const spriteEntity = rootEntity.createChild("spriteBlur"); + + spriteEntity.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + texture + ); + // The blur algorithm will sample the edges of the texture. + // Set the clamp warp mode to avoid mis-sampling caused by repeate warp mode. + texture.wrapModeU = texture.wrapModeV = TextureWrapMode.Clamp; + + // Display normal + addCustomMaterialSpriteEntity(spriteEntity, -7.5, texSize, 0.0); + // Display low blur + addCustomMaterialSpriteEntity(spriteEntity.clone(), -2.5, texSize, 1.0); + // Display moderate blur + addCustomMaterialSpriteEntity(spriteEntity.clone(), 2.5, texSize, 2.0); + // Display highly blur + addCustomMaterialSpriteEntity(spriteEntity.clone(), 7.5, texSize, 3.0); + }); + + engine.run(); + + function addCustomMaterialSpriteEntity( + entity: Entity, + posX: number, + texSize: Vector2, + blurSize: number + ): void { + rootEntity.addChild(entity); + entity.transform.setPosition(posX, 0, 0); + // Create material + const material = new Material(engine, Shader.find("SpriteBlur")); + entity.getComponent(SpriteRenderer).setMaterial(material); + // Init state + const target = material.renderState.blendState.targetBlendState; + target.enabled = true; + target.sourceColorBlendFactor = BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.sourceAlphaBlendFactor = BlendFactor.One; + target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.colorBlendOperation = target.alphaBlendOperation = + BlendOperation.Add; + material.renderState.depthState.writeEnabled = false; + material.renderState.renderQueueType = RenderQueueType.Transparent; + material.renderState.rasterState.cullMode = CullMode.Off; + // Set uniform + material.shaderData.setVector2("u_texSize", texSize); + material.shaderData.setFloat("u_blurSize", blurSize); + } + + // Custom shader + const spriteVertShader = ` + precision highp float; + + uniform mat4 camera_VPMat; + + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + attribute vec4 COLOR_0; + + varying vec4 v_color; + varying vec2 v_uv; + + void main() + { + gl_Position = camera_VPMat * vec4(POSITION, 1.0); + v_color = COLOR_0; + v_uv = TEXCOORD_0; + } +`; + + const spriteFragmentShader = ` + precision mediump float; + precision mediump int; + + uniform sampler2D renderer_SpriteTexture; + uniform float u_blurSize; + uniform vec2 u_texSize; + + varying vec2 v_uv; + varying vec4 v_color; + + float normpdf(float x, float sigma) { + return 0.39894 * exp(-0.5 * x * x / (sigma * sigma)) / sigma; + } + + void main() { + vec4 color = texture2D(renderer_SpriteTexture, v_uv); + const int mSize = 11; + const int kSize = (mSize - 1) / 2; + float kernel[mSize]; + vec3 final_colour = vec3(0.0); + + // create the 1-D kernel + float sigma = 7.0; + float Z = 0.0; + for (int i = 0; i <= kSize; ++i) { + kernel[kSize+i] = kernel[kSize - i] = normpdf(float(i), sigma); + } + + // get the normalization factor (as the gaussian has been clamped) + for (int i = 0; i < mSize; ++i) { + Z += kernel[i]; + } + + // read out the texels + float offsetX = u_blurSize / u_texSize.x; + float offsetY = u_blurSize / u_texSize.y; + vec2 uv; + for (int i = -kSize; i <= kSize; ++i) { + for (int j = -kSize; j <= kSize; ++j) { + uv = v_uv.xy + vec2(float(i) * offsetX, float(j) * offsetY); + final_colour += kernel[kSize + j] * kernel[kSize + i] * texture2D(renderer_SpriteTexture, uv).rgb; + } + } + + gl_FragColor = vec4(final_colour / (Z * Z), color.a) * v_color; + } +`; + + Shader.create("SpriteBlur", spriteVertShader, spriteFragmentShader); +}); diff --git a/examples/sprite-material-dissolve.ts b/examples/sprite-material-dissolve.ts new file mode 100644 index 000000000..3b74d52b8 --- /dev/null +++ b/examples/sprite-material-dissolve.ts @@ -0,0 +1,220 @@ +/** + * @title Sprite Material Dissolve + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*9EfxR5IhShwAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + BlendFactor, + Camera, + CullMode, + Engine, + Material, + RenderQueueType, + Script, + Shader, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +main(); + +async function main() { + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create root entity + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 12); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load([ + { + // Sprite texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*L2GNRLWn9EAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + // Noise texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*j2xJQL0e6J4AAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + { + // Ramp texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ygj3S7sm4hQAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + ]) + .then((textures: Texture2D[]) => { + // Create origin sprite entity. + const spriteEntity = rootEntity.createChild("DissolveSprite"); + const material = addCustomMaterial(engine, textures[1], textures[2]); + const renderer = spriteEntity.addComponent(SpriteRenderer); + renderer.sprite = new Sprite(engine, textures[0]); + renderer.setMaterial(material); + + // Add dissolve animate script. + const script = spriteEntity.addComponent(AnimateScript); + // Add custom material. + script.material = material; + // Add Data UI. + script.guiData = addDataGUI(script.material, script); + }); + + engine.run(); +} + +function addCustomMaterial( + engine: Engine, + noiseTexture: Texture2D, + rampTexture: Texture2D +): Material { + const material = new Material(engine, Shader.find("SpriteDissolve")); + + // Init state. + const renderState = material.renderState; + const target = renderState.blendState.targetBlendState; + target.enabled = true; + target.sourceColorBlendFactor = BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.sourceAlphaBlendFactor = BlendFactor.One; + target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + renderState.depthState.writeEnabled = false; + renderState.rasterState.cullMode = CullMode.Off; + material.renderState.renderQueueType = RenderQueueType.Transparent; + + // Set material shader data. + const { shaderData } = material; + shaderData.setFloat("u_threshold", 0.0); + shaderData.setFloat("u_edgeLength", 0.1); + shaderData.setTexture("u_rampTexture", rampTexture); + shaderData.setTexture("u_noiseTexture", noiseTexture); + + return material; +} + +/** + * Add data GUI. + */ +function addDataGUI(material: Material, animationScript: AnimateScript) { + const { shaderData } = material; + const gui = new dat.GUI(); + const guiData = { + threshold: 0.0, + edgeLength: 0.1, + reset: () => { + guiData.threshold = 0.0; + guiData.edgeLength = 0.1; + shaderData.setFloat("u_threshold", 0.0); + shaderData.setFloat("u_edgeLength", 0.1); + }, + pause: function () { + animationScript.enabled = false; + }, + resume: function () { + animationScript.enabled = true; + }, + }; + + gui + .add(guiData, "threshold", 0.0, 1.0, 0.01) + .onChange((value: number) => { + shaderData.setFloat("u_threshold", value); + }) + .listen(); + gui + .add(guiData, "edgeLength", 0.0, 0.2, 0.001) + .onChange((value: number) => { + shaderData.setFloat("u_edgeLength", value); + }) + .listen(); + gui.add(guiData, "reset").name("重置"); + gui.add(guiData, "pause").name("暂停动画"); + gui.add(guiData, "resume").name("继续动画"); + + return guiData; +} + +class AnimateScript extends Script { + guiData: any; + material: Material; + + /** + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + const { guiData } = this; + const threshold = (guiData.threshold + deltaTime * 0.3) % 1.0; + + // Update gui data. + guiData.threshold = threshold; + // Update material data. + this.material.shaderData.setFloat("u_threshold", threshold); + } +} + +// Custom shader +const spriteVertShader = ` + precision highp float; + + uniform mat4 camera_VPMat; + + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + attribute vec4 COLOR_0; + + varying vec4 v_color; + varying vec2 v_uv; + + void main() + { + gl_Position = camera_VPMat * vec4(POSITION, 1.0); + v_color = COLOR_0; + v_uv = TEXCOORD_0; + } +`; + +const spriteFragmentShader = ` + precision mediump float; + precision mediump int; + + uniform sampler2D renderer_SpriteTexture; + uniform sampler2D u_noiseTexture; + uniform sampler2D u_rampTexture; + uniform float u_threshold; + uniform float u_edgeLength; + + varying vec2 v_uv; + varying vec4 v_color; + + vec4 lerp(vec4 a, vec4 b, float w) { + return a + w * (b - a); + } + + void main() { + float r = texture2D(u_noiseTexture, v_uv).r; + float diff = r - u_threshold; + if (diff <= 0.0) { + discard; + } + + float degree = clamp(0.0, 1.0, diff / u_edgeLength); + vec4 edgeColor = texture2D(u_rampTexture, vec2(degree, degree)); + vec4 color = texture2D(renderer_SpriteTexture, v_uv); + vec4 finalColor = lerp(edgeColor, color, degree); + gl_FragColor = vec4(finalColor.rgb, color.a) * v_color; + } +`; + +Shader.create("SpriteDissolve", spriteVertShader, spriteFragmentShader); diff --git a/examples/sprite-material-glitch-rgbSplit.ts b/examples/sprite-material-glitch-rgbSplit.ts new file mode 100644 index 000000000..83df66307 --- /dev/null +++ b/examples/sprite-material-glitch-rgbSplit.ts @@ -0,0 +1,149 @@ +/** + * @title Sprite Material Glitch + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*cfmPS5POgLgAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + BlendFactor, + Camera, + CullMode, + Engine, + Material, + RenderQueueType, + Shader, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +main(); + +async function main() { + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 12); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + // Sprite texture + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*5wypQ5JyDLkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture: Texture2D) => { + // Create origin sprite entity. + const spriteEntity = rootEntity.createChild("GlitchSprite"); + const material = addCustomMaterial(engine); + const renderer = spriteEntity.addComponent(SpriteRenderer); + renderer.sprite = new Sprite(engine, texture); + renderer.setMaterial(material); + + // Add Data UI. + addDataGUI(material); + }); + + engine.run(); +} + +function addCustomMaterial(engine: Engine): Material { + const material = new Material(engine, Shader.find("SpriteGlitchRGBSplit")); + + // Init state. + const renderState = material.renderState; + const target = renderState.blendState.targetBlendState; + target.enabled = true; + target.sourceColorBlendFactor = BlendFactor.SourceAlpha; + target.destinationColorBlendFactor = BlendFactor.OneMinusSourceAlpha; + target.sourceAlphaBlendFactor = BlendFactor.One; + target.destinationAlphaBlendFactor = BlendFactor.OneMinusSourceAlpha; + renderState.depthState.writeEnabled = false; + renderState.rasterState.cullMode = CullMode.Off; + renderState.renderQueueType = RenderQueueType.Transparent; + + // Set material shader data. + const { shaderData } = material; + shaderData.setFloat("u_indensity", 0.5); + + return material; +} + +/** + * Add data GUI. + */ +function addDataGUI(material: Material) { + const { shaderData } = material; + const gui = new dat.GUI(); + const guiData = { + indensity: 0.5, + reset: () => { + guiData.indensity = 0.5; + shaderData.setFloat("u_indensity", 0.5); + }, + }; + + gui + .add(guiData, "indensity", 0.0, 1.0, 0.01) + .onChange((value: number) => { + shaderData.setFloat("u_indensity", value); + }) + .listen(); + + gui.add(guiData, "reset").name("重置"); + return guiData; +} + +// Custom shader +const spriteVertShader = ` + uniform mat4 camera_VPMat; + + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + attribute vec4 COLOR_0; + + varying vec4 v_color; + varying vec2 v_uv; + + void main() + { + gl_Position = camera_VPMat * vec4(POSITION, 1.0); + v_color = COLOR_0; + v_uv = TEXCOORD_0; + } +`; + +const spriteFragmentShader = ` + uniform sampler2D renderer_SpriteTexture; + uniform vec4 scene_ElapsedTime; + uniform float u_indensity; + + varying vec2 v_uv; + varying vec4 v_color; + + float randomNoise(float time) { + return fract(sin(dot(vec2(time, 2), vec2(12.9898, 78.233)))); + } + + void main() { + float splitAmount = u_indensity * randomNoise(scene_ElapsedTime.x * 100.0); + + vec4 normalColor = texture2D(renderer_SpriteTexture, v_uv); + float r = texture2D(renderer_SpriteTexture, vec2(v_uv.x + splitAmount, v_uv.y)).r; + float b = texture2D(renderer_SpriteTexture, vec2(v_uv.x - splitAmount, v_uv.y)).b; + gl_FragColor = vec4(r, normalColor.g, b, normalColor.a) * v_color; + } +`; + +Shader.create("SpriteGlitchRGBSplit", spriteVertShader, spriteFragmentShader); diff --git a/examples/sprite-pivot.ts b/examples/sprite-pivot.ts new file mode 100644 index 000000000..97b2fe4f1 --- /dev/null +++ b/examples/sprite-pivot.ts @@ -0,0 +1,115 @@ +/** + * @title Sprite Pivot + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*0irsTpRlOLAAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + Entity, + Script, + Sprite, + SpriteRenderer, + Texture2D, + Vector2, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*d3N9RYpcKncAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create origin sprite entity. + const spriteEntity = new Entity(engine, "spritePivot"); + const { transform } = spriteEntity; + transform.setScale(3, 3, 3); + transform.setPosition(0, 5, 0); + spriteEntity.addComponent(SpriteRenderer); + + // Display normal. + addPivotEntity(spriteEntity, texture, 8); + // Display pivot entity + const pivotEntity = spriteEntity.clone(); + pivotEntity.addComponent(RotateScript); + addPivotEntity(pivotEntity, texture, -8); + addDataGUI(pivotEntity); + }); + + engine.run(); + + /** + * Add flip entity. + */ + function addPivotEntity( + entity: Entity, + texture: Texture2D, + posY: number + ): void { + rootEntity.addChild(entity); + entity.transform.setPosition(0, posY, 0); + entity.getComponent(SpriteRenderer).sprite = new Sprite( + entity.engine, + texture + ); + } + + /** + * Add data GUI. + */ + function addDataGUI(entity: Entity) { + const sprite = entity.getComponent(SpriteRenderer).sprite; + const pivot = new Vector2(0.5, 0.5); + const gui = new dat.GUI(); + const guiData = { + pivotX: 0.5, + pivotY: 0.5, + reset: () => { + guiData.pivotX = 0.5; + guiData.pivotY = 0.5; + pivot.set(0.5, 0.5); + sprite.pivot = pivot; + }, + }; + + gui + .add(guiData, "pivotX", 0.0, 1.0, 0.01) + .onChange((value: number) => { + pivot.x = value; + sprite.pivot = pivot; + }) + .listen(); + gui + .add(guiData, "pivotY", 0.0, 1.0, 0.01) + .onChange((value: number) => { + pivot.y = value; + sprite.pivot = pivot; + }) + .listen(); + gui.add(guiData, "reset").name("重置"); + + return guiData; + } + + class RotateScript extends Script { + onUpdate(dt: number) { + this.entity.transform.rotate(0, 0, 1); + } + } +}); diff --git a/examples/sprite-region.ts b/examples/sprite-region.ts new file mode 100644 index 000000000..aedcc75be --- /dev/null +++ b/examples/sprite-region.ts @@ -0,0 +1,79 @@ +/** + * @title Sprite Region + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*E45XT5aZhW0AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Entity, + Rect, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*d3N9RYpcKncAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + // Create origin sprite entity. + const spriteEntity = new Entity(engine, "spriteRegion"); + spriteEntity.transform.setScale(3, 3, 3); + spriteEntity.addComponent(SpriteRenderer); + + const rect = new Rect(); + // Display normal. + rect.set(0, 0, 1, 1); + addRegionEntity(spriteEntity, texture, 0, 5, rect); + // Display top left half. + rect.set(0, 0, 0.5, 0.5); + addRegionEntity(spriteEntity.clone(), texture, -7.5, -5, rect); + // Display top right half. + rect.set(0.5, 0, 1, 0.5); + addRegionEntity(spriteEntity.clone(), texture, -2.5, -5, rect); + // Display bottom left half. + rect.set(0, 0.5, 0.5, 0.5); + addRegionEntity(spriteEntity.clone(), texture, 2.5, -5, rect); + // Display bottom right half. + rect.set(0.5, 0.5, 1, 1); + addRegionEntity(spriteEntity.clone(), texture, 7.5, -5, rect); + }); + + engine.run(); + + /** + * Add flip entity. + */ + function addRegionEntity( + entity: Entity, + texture: Texture2D, + posX: number, + posY: number, + region: Rect + ): void { + rootEntity.addChild(entity); + entity.transform.setPosition(posX, posY, 0); + const regionRenderer = entity.getComponent(SpriteRenderer); + const sprite = new Sprite(entity.engine, texture); + sprite.region = region; + regionRenderer.sprite = sprite; + } +}); diff --git a/examples/sprite-renderer.ts b/examples/sprite-renderer.ts new file mode 100644 index 000000000..8562feb6a --- /dev/null +++ b/examples/sprite-renderer.ts @@ -0,0 +1,99 @@ +/** + * @title Sprite Renderer + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*SvZDRpwVX-IAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Script, + Sprite, + SpriteRenderer, + Texture2D, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 50); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create sprite renderer + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*ApFPTZSqcMkAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + for (let i = 0; i < 10; ++i) { + setTimeout(() => { + const spriteEntity = rootEntity.createChild(`sprite_${i}`); + spriteEntity.transform.position = new Vector3(0, 0, 0); + const spriteRenderer = spriteEntity.addComponent(SpriteRenderer); + const sprite = new Sprite(engine, texture); + spriteRenderer.sprite = sprite; + // spriteRenderer.flipX = true; + // spriteRenderer.flipY = true; + const rect = spriteRenderer.sprite.region; + const scaleX = 100.0 / rect.width; + const scaleY = 100.0 / rect.height; + spriteEntity.transform.setScale(scaleX, scaleY, 1); + spriteEntity.addComponent(SpriteController); + }, 2000 * i); + } + }); + + engine.run(); + + // Script for sprite + class SpriteController extends Script { + static _curRotation: number = 0; + + private _radius: number = 1.5; + private _curRadian: number; + private _scale: number; + private _scaleFlag: boolean; + + onAwake() { + this._curRadian = 0; + this._radius = 15; + this._scale = 0.5; + this._scaleFlag = true; + } + + onUpdate() { + // Update position. + this._curRadian += 0.005; + const { _radius, _curRadian, entity } = this; + const { transform } = entity; + const posX = Math.cos(_curRadian) * _radius; + const posY = Math.sin(_curRadian) * _radius; + transform.setPosition(posX, posY, 0); + + // Update scale. + this._scale += this._scaleFlag ? 0.005 : -0.005; + const { _scale } = this; + transform.setScale(_scale, _scale, _scale); + if (this._scale >= 0.6) { + this._scaleFlag = false; + } else if (this._scale <= 0.4) { + this._scaleFlag = true; + } + + // Update rotation. + SpriteController._curRotation += 0.05; + const { _curRotation } = SpriteController; + transform.setRotation(0, 0, _curRotation); + } + } +}); diff --git a/examples/sprite-sheetAnimation.ts b/examples/sprite-sheetAnimation.ts new file mode 100644 index 000000000..c05fb15de --- /dev/null +++ b/examples/sprite-sheetAnimation.ts @@ -0,0 +1,150 @@ +/** + * @title Sprite SheetAnimation + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*mLQbQZ_umdQAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + AssetType, + Camera, + Script, + Sprite, + SpriteRenderer, + Texture2D, + Vector2, + WebGLEngine, + Transform, +} from "@galacean/engine"; +import * as TWEEN from "@tweenjs/tween.js"; + +main(); + +async function main(): Promise { + // Create engine object. + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create rootEntity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 15); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Load texture and create sprite sheet animation. + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*9nsHSpx28rAAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + const spriteEntity = rootEntity.createChild("Sprite"); + spriteEntity.addComponent(SpriteRenderer).sprite = new Sprite( + engine, + texture, + null, + null, + null + ); + spriteEntity.addComponent(FrameSpriteScript); + }); + + // Run engine. + engine.run(); +} + +/** + * Script for sprite sheet animation. + */ +class FrameSpriteScript extends Script { + /** Offsets of sprite sheet animation. */ + private _regions: Vector2[]; + /** Reciprocal Of SliceWidth. */ + private _reciprocalSliceWidth: number; + /** Reciprocal Of SliceHeight. */ + private _reciprocalSliceHeight: number; + /** Total frames. */ + private _totalFrames: number; + /** Frame interval time, the unit of time is ms. */ + private _frameInterval: number = 0.15; + + private _sprite: Sprite; + private _curFrameIndex: number; + private _cumulativeTime: number = 0; + private _birdTransform: Transform; + + onAwake(): void { + // Sprite sheet animation pictures have 3 rows and 1 columns, if you modify the picture, please modify this. + const row = 3; + const col = 1; + const reciprocalSliceWidth = 1 / row; + const reciprocalSliceHeight = 1 / col; + const regions = new Array(); + for (let i = 0; i < col; i++) { + const y = i * reciprocalSliceHeight; + for (let j = 0; j < row; j++) { + regions.push(new Vector2(j * reciprocalSliceWidth, y)); + } + } + + const { entity } = this; + this._sprite = entity.getComponent(SpriteRenderer).sprite; + this._regions = regions; + this._reciprocalSliceWidth = reciprocalSliceWidth; + this._reciprocalSliceHeight = reciprocalSliceHeight; + this._totalFrames = row * col; + this._setFrameIndex(0); + + this._birdTransform = entity.transform; + new TWEEN.Tween(this) + .to({ birdPosY: 0.4 }, 380) + .repeat(Infinity) + .yoyo(true) + .easing(TWEEN.Easing.Sinusoidal.InOut) + .start(); + } + + onUpdate(deltaTime: number): void { + // Update TWEEN + TWEEN.update(); + + const frameInterval = this._frameInterval; + this._cumulativeTime += deltaTime; + if (this._cumulativeTime >= frameInterval) { + // Need update frameIndex. + const addFrameCount = Math.floor(this._cumulativeTime / frameInterval); + this._cumulativeTime -= addFrameCount * frameInterval; + this._setFrameIndex( + (this._curFrameIndex + addFrameCount) % this._totalFrames + ); + } + } + + private _setFrameIndex(frameIndex: number): void { + if (this._curFrameIndex !== frameIndex) { + this._curFrameIndex = frameIndex; + const frameInfo = this._regions[frameIndex]; + const region = this._sprite.region; + region.set( + frameInfo.x, + frameInfo.y, + this._reciprocalSliceWidth, + this._reciprocalSliceHeight + ); + this._sprite.region = region; + } + } + + set birdPosY(val) { + const transform = this._birdTransform; + const position = transform.position; + position.y = val; + transform.position = position; + } + + get birdPosY() { + return this._birdTransform.position.y; + } +} diff --git a/examples/sprite-size.ts b/examples/sprite-size.ts new file mode 100644 index 000000000..8fcfddad6 --- /dev/null +++ b/examples/sprite-size.ts @@ -0,0 +1,77 @@ +/** + * @title Sprite Size + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*peiFQ6T-wN8AAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + Entity, + Sprite, + SpriteRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine object. +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + camera.isOrthographic = true; + camera.orthographicSize = 5; + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*d3N9RYpcKncAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + const entity = rootEntity.createChild("sprite"); + entity.addComponent(SpriteRenderer).sprite = new Sprite(engine, texture); + addDataGUI(entity); + }); + engine.run(); + + /** + * Add data GUI. + */ + function addDataGUI(entity: Entity) { + const spriteRenderer = entity.getComponent(SpriteRenderer); + const sprite = spriteRenderer.sprite; + const gui = new dat.GUI(); + const defaultWidth = sprite.width; + const defaultHeight = sprite.height; + const guiData = { + width: defaultWidth, + height: defaultHeight, + reset: () => { + spriteRenderer.width = guiData.width = defaultWidth; + spriteRenderer.height = guiData.height = defaultHeight; + }, + }; + + gui + .add(guiData, "width", 0, defaultWidth * 5, defaultWidth / 10) + .onChange((value: number) => { + spriteRenderer.width = value; + }) + .listen(); + gui + .add(guiData, "height", 0, defaultHeight * 5, defaultHeight / 10) + .onChange((value: number) => { + spriteRenderer.height = value; + }) + .listen(); + gui.add(guiData, "reset").name("重置"); + + return guiData; + } +}); diff --git a/examples/text-barrage.ts b/examples/text-barrage.ts new file mode 100644 index 000000000..7d48887b1 --- /dev/null +++ b/examples/text-barrage.ts @@ -0,0 +1,129 @@ +/** + * @title Text Barrage + * @category Benchmark + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*z0fYTriO8-YAAAAAAAAAAAAADiR2AQ/original + */ +import { + Camera, + Color, + Font, + Script, + TextHorizontalAlignment, + TextRenderer, + WebGLEngine, +} from "@galacean/engine"; +import { Stats } from "@galacean/engine-toolkit-stats"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.run(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + const camera = cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(0, 0, 10); + camera.isOrthographic = true; + cameraEntity.addComponent(Stats); + + // Create text barrage + const textCount = 50; + for (let i = 0; i < textCount; ++i) { + const textEntity = rootEntity.createChild(); + + // Init text renderer + const textRenderer = textEntity.addComponent(TextRenderer); + textRenderer.font = Font.createFromOS(engine, "Arial"); + textRenderer.fontSize = 36; + textRenderer.priority = i; + textRenderer.horizontalAlignment = TextHorizontalAlignment.Right; + + // Init and reset text barrage animation + const barrage = textEntity.addComponent(TextBarrageAnimation); + barrage.camera = camera; + barrage.priorityOffset = textCount; + barrage.play(); + } +}); + +class TextBarrageAnimation extends Script { + // prettier-ignore + static words = [ "GALACEAN", "galacean", "HELLO", "hello", "WORLD", "world", "TEXT", "text", "PEACE", "peace", "LOVE", "love", "abcdefg", "hijklmn", "opqrst", "uvwxyz", "ABCDEFG", "HIJKLMN", "OPQRST", "UVWXYZ", "~!@#$", "%^&*", "()_+" ]; + static colors = [ + new Color(1, 1, 1, 1), + new Color(1, 0, 0, 1), + new Color(0, 1, 0.89, 1), + ]; + + public camera: Camera; + public priorityOffset: number = 0; + + private _speed: number = 0; + private _range: number = 0; + private _isPlaying: boolean = false; + private _textRenderer: TextRenderer; + + play() { + this._isPlaying = true; + } + + onStart(): void { + this._textRenderer = this.entity.getComponent(TextRenderer); + this._range = -this.camera.orthographicSize * this.camera.aspectRatio; + this._reset(true); + } + + onUpdate(dt: number): void { + if (this._isPlaying) { + const { position } = this.entity.transform; + position.x += this._speed * dt; + if (position.x < this._range) { + this._reset(false); + } + } + } + + private _reset(isFirst: boolean) { + const textRenderer = this._textRenderer; + const { words, colors } = TextBarrageAnimation; + + // Reset priority for renderer + textRenderer.priority += this.priorityOffset; + + // Reset the text to render + const wordLastIndex = words.length - 1; + textRenderer.text = `${words[getRandomNum(0, wordLastIndex)]} ${ + words[getRandomNum(0, wordLastIndex)] + } ${getRandomNum(0, 99)}`; + + // Reset color + textRenderer.color = colors[getRandomNum(0, colors.length - 1)]; + + // Reset position + const { position } = this.entity.transform; + const { orthographicSize } = this.camera; + if (isFirst) { + const halfOrthoWidth = orthographicSize * this.camera.aspectRatio; + position.x = getRandomNum(-halfOrthoWidth, halfOrthoWidth); + } else { + const { bounds } = textRenderer; + position.x = + orthographicSize * this.camera.aspectRatio + + bounds.max.x - + bounds.min.x; + } + position.y = getRandomNum(-orthographicSize, orthographicSize); + + // Reset speed + this._speed = getRandomNum(-500, -200) * 0.01; + } +} + +function getRandomNum(min: number, max: number): number { + const range = max - min; + const rand = Math.random(); + return min + Math.round(rand * range); +} diff --git a/examples/text-ktv-subtitle.ts b/examples/text-ktv-subtitle.ts new file mode 100644 index 000000000..fdf0bbd53 --- /dev/null +++ b/examples/text-ktv-subtitle.ts @@ -0,0 +1,202 @@ +/** + * @title Text KTV Subtitle + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*pmZWSbuMNo8AAAAAAAAAAAAADiR2AQ/original + */ + +import { + AssetType, + BackgroundMode, + BaseMaterial, + Camera, + Color, + Entity, + Font, + Material, + RenderFace, + Script, + Shader, + TextRenderer, + Texture2D, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine object +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + engine.run(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + + async function init() { + // Set background + const bgTex = await engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/zos/OasisHub/440000983/3784/vcg_VCG211258128318_RF.jpg?x-oss-process=image/format,webp", + type: AssetType.Texture2D, + }); + const background = engine.sceneManager.activeScene.background; + background.mode = BackgroundMode.Texture; + background.texture = bgTex; + + // Create texts + const text1Entity = createText("听我说 谢谢你", -2, -2); + const text2Entity = createText("因为有你 温暖了四季", 0, -3); + + // Add KTV subtitle material and animate script + const animateScript1 = addCustomMaterialAndAnimateScript(text1Entity, 5000); + const animateScript2 = addCustomMaterialAndAnimateScript(text2Entity, 5000); + + // Play animation loop + while (true) { + await animateScript1.play(); + animateScript1.reset(); + await animateScript2.play(); + animateScript2.reset(); + } + } + + function createText(text: string, posX: number, posY: number): Entity { + // Create text entity + const textEntity = rootEntity.createChild("text"); + rootEntity.addChild(textEntity); + // Add text renderer for text entity + const renderer = textEntity.addComponent(TextRenderer); + // Set font size + renderer.fontSize = 48; + // Set font with font family + renderer.font = Font.createFromOS(textEntity.engine, "Arial"); + // Set text to display + renderer.text = text; + // Set position + textEntity.transform.position.set(posX, posY, 0); + return textEntity; + } + + function addCustomMaterialAndAnimateScript( + entity: Entity, + time: number + ): AnimateScript { + // Create material + const material = new BaseMaterial(engine, Shader.find("TextKTVSubtitle")); + entity.getComponent(TextRenderer).setMaterial(material); + // Init state + material.isTransparent = true; + material.renderFace = RenderFace.Double; + // Set uniform + material.shaderData.setFloat("u_percent", 0); + material.shaderData.setColor("u_subtitleColor", new Color(0, 1, 0.89, 1)); + + // Add AnimateScript + const script = entity.addComponent(AnimateScript); + script.material = material; + script.totalTime = time; + return script; + } + + // Custom shader + const vertShader = ` + precision highp float; + + uniform mat4 camera_VPMat; + uniform float u_startX; + uniform float u_endX; + + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + attribute vec4 COLOR_0; + + varying vec4 v_color; + varying vec2 v_uv; + varying float v_startX; + varying float v_width; + varying float v_posX; + + void main() + { + gl_Position = camera_VPMat * vec4(POSITION, 1.0); + v_uv = TEXCOORD_0; + v_color = COLOR_0; + v_startX = u_startX; + v_width = u_endX - u_startX; + v_posX = POSITION.x; + } +`; + + const fragmentShader = ` + precision mediump float; + precision mediump int; + + uniform sampler2D renderer_SpriteTexture; + uniform float u_percent; + uniform vec4 u_subtitleColor; + + varying vec2 v_uv; + varying vec4 v_color; + varying float v_startX; + varying float v_width; + varying float v_posX; + + void main() { + vec4 baseColor = texture2D(renderer_SpriteTexture, v_uv); + float percent = (v_posX - v_startX) / v_width; + if (percent <= u_percent) { + gl_FragColor = baseColor * u_subtitleColor; + } else { + gl_FragColor = baseColor * v_color; + } + } +`; + + Shader.create("TextKTVSubtitle", vertShader, fragmentShader); + + class AnimateScript extends Script { + material: Material; + totalTime: number = 0; + + private _curTime: number = 0; + private _isPlaying: boolean = false; + private _cb: Function = null; + + onUpdate(dt: number) { + if (this._isPlaying) { + this._curTime += dt * 1000; + const { _curTime: curTime, totalTime } = this; + const { shaderData } = this.material; + const bounds = this.entity.getComponent(TextRenderer).bounds; + shaderData.setFloat("u_startX", bounds.min.x); + shaderData.setFloat("u_endX", bounds.max.x); + shaderData.setFloat("u_percent", curTime / totalTime); + if (curTime >= totalTime) { + this._isPlaying = false; + this._cb && this._cb(); + } + } + } + + play() { + const { material } = this; + if (!material) { + return; + } + + this._isPlaying = true; + this._curTime = 0; + return new Promise((resolve) => { + this._cb = resolve; + }); + } + + reset() { + this._isPlaying = false; + this.material.shaderData.setFloat("u_percent", 0); + } + } + + init(); +}); diff --git a/examples/text-renderer-font.ts b/examples/text-renderer-font.ts new file mode 100644 index 000000000..5d6fd18bd --- /dev/null +++ b/examples/text-renderer-font.ts @@ -0,0 +1,68 @@ +/** + * @title Text Renderer Font + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*7xc-QKMlMwkAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Camera, + Color, + Font, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // The position of text + const pos = new Vector3(); + // Create text with cursive font family + pos.set(0, 0.75, 0); + createText("Galacean system font: Hello World"); + // Create text with font size 36 + pos.set(0, 0.25, 0); + createText( + "Galacean custom font: Hello World", + "https://lg-2fw0hhsc-1256786476.cos.ap-shanghai.myqcloud.com/Avelia.otf" + ); + + engine.run(); + + /** + * Create text to display by params. + * @param text - The text to render + * @param fontUrl - The url of font, if not, use system font + */ + async function createText(text: string, fontUrl: string = ""): Promise { + // Create text entity + const entity = rootEntity.createChild("text"); + entity.transform.position = pos; + // Add text renderer for text entity + const renderer = entity.addComponent(TextRenderer); + // Set text color + renderer.color = new Color(1, 0, 0, 1); + // Set text to render + renderer.text = text; + // Set font + if (fontUrl) { + renderer.font = await engine.resourceManager.load({ url: fontUrl }); + } else { + renderer.font = Font.createFromOS(engine, "Arial"); + } + // Set font size + renderer.fontSize = 30; + } +}); diff --git a/examples/text-renderer.ts b/examples/text-renderer.ts new file mode 100644 index 000000000..645b5dcb4 --- /dev/null +++ b/examples/text-renderer.ts @@ -0,0 +1,96 @@ +/** + * @title Text Renderer + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*g-oMQas6VsMAAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Camera, + Color, + Font, + FontStyle, + TextRenderer, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // The text to display + const text = "Galacean 文字系统来啦~"; + // The position of text + const pos = new Vector3(); + // The color of text + const color = new Color(); + + // Create text with default params + pos.set(0, 1.25, 0); + color.set(1, 1, 1, 1); + createText(); + // Create text with cursive font family + pos.set(0, 0.75, 0); + color.set(1, 1, 1, 1); + createText("cursive"); + // Create text with font size 36 + pos.set(0, 0.25, 0); + color.set(1, 0.5, 0.5, 1); + createText("Arial", 36); + // Create text with bold + pos.set(0, -0.25, 0); + color.set(1, 1, 1, 1); + createText("Arial", 26, true); + // Create text with italic + pos.set(0, -0.75, 0); + color.set(1, 1, 1, 1); + createText("Arial", 26, false, true); + // Create text with bold and italic + pos.set(0, -1.25, 0); + color.set(1, 1, 1, 1); + createText("Arial", 26, true, true); + + engine.run(); + + /** + * Create text to display by params. + * @param fontFamily - The font family + * @param fontSize - The size of font + * @param bold - The text whether bold + * @param italic - The text whether italic + */ + function createText( + fontFamily: string = "Arial", + fontSize: number = 26, + bold: boolean = false, + italic: boolean = false + ): void { + // Create text entity + const entity = rootEntity.createChild("text"); + entity.transform.position = pos; + // Add text renderer for text entity + const renderer = entity.addComponent(TextRenderer); + // Set text color + renderer.color = color; + // Set text to render + renderer.text = text; + // Set font with font family + renderer.font = Font.createFromOS(entity.engine, fontFamily); + // Set font size + renderer.fontSize = fontSize; + // Set font whether bold + bold && (renderer.fontStyle |= FontStyle.Bold); + // Set font whether italic + italic && (renderer.fontStyle |= FontStyle.Italic); + } +}); diff --git a/examples/text-wrap-alignment.ts b/examples/text-wrap-alignment.ts new file mode 100644 index 000000000..db6c7e4c5 --- /dev/null +++ b/examples/text-wrap-alignment.ts @@ -0,0 +1,92 @@ +/** + * @title Text Wrap And Alignment + * @category 2D + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*E7dVT6Lfmx8AAAAAAAAAAAAADiR2AQ/original + */ + +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Camera, + Entity, + TextHorizontalAlignment, + TextRenderer, + WebGLEngine, +} from "@galacean/engine"; + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("camera_entity"); + cameraEntity.transform.setPosition(0, 0, 10); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // The text to display + const text = "文字折行对齐示例,根据设置的宽自动换行"; + const textEntity = rootEntity.createChild("text"); + textEntity.addComponent(TextRenderer); + + // Text display no wrap, center align + setTextRenderer( + textEntity, + 2, + `${text} (不换行,居中对齐)`, + 2, + false, + TextHorizontalAlignment.Center + ); + // Text display with wrap, center align + setTextRenderer( + textEntity.clone(), + 1, + `${text}\n(换行,居中对齐)`, + 2, + true, + TextHorizontalAlignment.Center + ); + // Text display with wrap, left align + setTextRenderer( + textEntity.clone(), + 0, + `${text}\n(换行,左对齐)`, + 2, + true, + TextHorizontalAlignment.Left + ); + // Text display with wrap, right align + setTextRenderer( + textEntity.clone(), + -1, + `${text}\n(换行,右对齐)`, + 2, + true, + TextHorizontalAlignment.Right + ); + + engine.run(); + + function setTextRenderer( + entity: Entity, + posY: number, + text: string, + width: number, + wrap: boolean, + hAlign: TextHorizontalAlignment + ): void { + rootEntity.addChild(entity); + entity.transform.position.y = posY; + // Get the text renderer + const renderer = entity.getComponent(TextRenderer); + renderer.text = text; + renderer.width = width; + // Set whether wrap + renderer.enableWrapping = wrap; + // Set horizontal alignment + renderer.horizontalAlignment = hAlign; + } +}); diff --git a/examples/texture-aniso.ts b/examples/texture-aniso.ts new file mode 100644 index 000000000..653df21f3 --- /dev/null +++ b/examples/texture-aniso.ts @@ -0,0 +1,59 @@ +/** + * @title Anisotropic + * @category Texture + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*O8m2Ta79iXYAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + MeshRenderer, + PrimitiveMesh, + RenderFace, + Texture2D, + UnlitMaterial, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 1); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create Plane + const mesh = PrimitiveMesh.createPlane(engine, 2, 2); + const material = new UnlitMaterial(engine); + material.renderFace = RenderFace.Double; + material.tilingOffset.set(30, 30, 0, 0); + const planeEntity = rootEntity.createChild("ground"); + planeEntity.transform.setRotation(5, 0, 0); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeRenderer.mesh = mesh; + planeRenderer.setMaterial(material); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_CtuR7LW4C0AAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + material.baseTexture = texture; + addGUI(texture); + engine.run(); + }); + + function addGUI(texture: Texture2D) { + const maxAnisoLevel = engine._hardwareRenderer.capability.maxAnisoLevel; + gui.add(texture, "anisoLevel", 1, maxAnisoLevel, 1); + } +}); diff --git a/examples/texture-mipmap.ts b/examples/texture-mipmap.ts new file mode 100644 index 000000000..9981d4ce3 --- /dev/null +++ b/examples/texture-mipmap.ts @@ -0,0 +1,76 @@ +/** + * @title Mipmap + * @category Texture + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*feqhQrfN1XIAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + Camera, + MeshRenderer, + PrimitiveMesh, + RenderFace, + Texture2D, + UnlitMaterial, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine object +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 1); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create Plane + const mesh = PrimitiveMesh.createPlane(engine, 2, 2); + const material = new UnlitMaterial(engine); + material.renderFace = RenderFace.Double; + material.tilingOffset.set(30, 30, 0, 0); + const planeEntity = rootEntity.createChild("ground"); + planeEntity.transform.setRotation(5, 0, 0); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeRenderer.mesh = mesh; + planeRenderer.setMaterial(material); + + const img = new Image(); + img.crossOrigin = "anonymous"; + img.src = + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*_CtuR7LW4C0AAAAAAAAAAAAAARQnAQ"; + img.onload = () => { + const { width, height } = img; + const texture = new Texture2D(engine, width, height); + texture.setImageSource(img); + texture.generateMipmaps(); + + const textureNoMipmap = new Texture2D( + engine, + width, + height, + undefined, + false + ); + textureNoMipmap.setImageSource(img); + + material.baseTexture = texture; + addGUI(texture, textureNoMipmap); + engine.run(); + }; + + function addGUI(texture: Texture2D, textureNoMipmap: Texture2D) { + gui.add({ mipmap: true }, "mipmap").onChange((v) => { + if (v) { + material.baseTexture = texture; + } else { + material.baseTexture = textureNoMipmap; + } + }); + } +}); diff --git a/examples/tiling-offset.ts b/examples/tiling-offset.ts new file mode 100644 index 000000000..aedf8d6c1 --- /dev/null +++ b/examples/tiling-offset.ts @@ -0,0 +1,143 @@ +/** + * @title Tiling Offset + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*M8yOQJfewiQAAAAAAAAAAAAADiR2AQ/original + */ +import { + AssetType, + Camera, + MeshRenderer, + PrimitiveMesh, + RenderFace, + Script, + Texture2D, + UnlitMaterial, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; + +main(); + +async function main() { + // Create engine object + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Load texture + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*Umw_RJGiZLYAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D + }) + .then((texture) => { + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 20); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create plane + const entity = rootEntity.createChild(); + entity.transform.setRotation(90, 0, 0); + const renderer = entity.addComponent(MeshRenderer); + const mesh = PrimitiveMesh.createPlane(engine, 10, 10); + const material = new UnlitMaterial(engine); + + texture.anisoLevel = 16; + material.renderFace = RenderFace.Double; + material.baseTexture = texture; + + renderer.mesh = mesh; + renderer.setMaterial(material); + + // Add animation script + const animationScript = rootEntity.addComponent(AnimateScript); + + // Add data GUI + const guiData = addDataGUI(material, animationScript); + animationScript.guiData = guiData; + animationScript.material = material; + + // Run engine + engine.run(); + }); +} + +/** + * Add data GUI. + */ +function addDataGUI(material: UnlitMaterial, animationScript: AnimateScript): any { + const gui = new dat.GUI(); + const guiData = { + tilingX: 1, + tilingY: 1, + offsetX: 0, + offsetY: 0, + reset: function () { + guiData.tilingX = 1; + guiData.tilingY = 1; + guiData.offsetX = 0; + guiData.offsetY = 0; + material.tilingOffset.set(1, 1, 0, 0); + }, + pause: function () { + animationScript.enabled = false; + }, + resume: function () { + animationScript.enabled = true; + } + }; + + gui + .add(guiData, "tilingX", 0, 10) + .onChange((value: number) => { + material.tilingOffset.x = value; + }) + .listen(); + gui + .add(guiData, "tilingY", 0, 10) + .onChange((value: number) => { + material.tilingOffset.y = value; + }) + .listen(); + gui + .add(guiData, "offsetX", 0, 1) + .onChange((value: number) => { + material.tilingOffset.z = value; + }) + .listen(); + gui + .add(guiData, "offsetY", 0, 1) + .onChange((value: number) => { + material.tilingOffset.w = value; + }) + .listen(); + gui.add(guiData, "reset").name("重置"); + gui.add(guiData, "pause").name("暂停动画"); + gui.add(guiData, "resume").name("继续动画"); + + return guiData; +} + +/** + * Animation script. + */ +class AnimateScript extends Script { + guiData: any; + material: UnlitMaterial; + + /** + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + const { material, guiData } = this; + material.tilingOffset.x = guiData.tilingX = ((guiData.tilingX - 1 + deltaTime) % 9) + 1; + material.tilingOffset.y = guiData.tilingY = ((guiData.tilingY - 1 + deltaTime) % 9) + 1; + } +} diff --git a/examples/transform-basic.ts b/examples/transform-basic.ts new file mode 100644 index 000000000..59984b680 --- /dev/null +++ b/examples/transform-basic.ts @@ -0,0 +1,104 @@ +/** + * @title Transform Basic + * @category Basic + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*b__aRb7Zv4UAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + Camera, + Color, + DirectLight, + Entity, + GLTFResource, + Script, + WebGLEngine, +} from "@galacean/engine"; + +main(); + +/** + * Init demo. + */ +async function main() { + // Create engine + const engine = await WebGLEngine.create({ canvas: "canvas" }); + engine.canvas.resizeByClientSize(); + + // Create yellow duck + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/OasisHub/267000040/9994/%25E5%25BD%2592%25E6%25A1%25A3.gltf" + ) + .then((gltf) => { + // Create root entity. + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + // Create camera. + const cameraEntity = rootEntity.createChild("CameraEntity"); + cameraEntity.transform.setPosition(0, 3, 9); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create light. + const lightEntity = rootEntity.createChild("LightEntity"); + const directLight = lightEntity.addComponent(DirectLight); + directLight.color = new Color(0.8, 0.8, 0.8); + + // Create three duck modles, set rotation and position. + const duck0 = gltf.defaultSceneRoot; + duck0.transform.rotate(0, -45, 0); + + const duck1 = duck0.clone(); + const duck2 = duck0.clone(); + duck1.transform.setPosition(-3, 0, 0); + duck2.transform.setPosition(3, 0, 0); + + // Create root entity and add transform script. + const script = rootEntity.addComponent(TransformScript); + script.duck0 = duck0; + script.duck1 = duck1; + script.duck2 = duck2; + + // Add ducks to scene. + rootEntity.addChild(duck0); + rootEntity.addChild(duck1); + rootEntity.addChild(duck2); + + //Run engine. + engine.run(); + }); +} + +/** + * Script for updating ducks position, rotation, and scale. + */ +class TransformScript extends Script { + /** Duck0. */ + duck0: Entity; + /** Duck1. */ + duck1: Entity; + /** Duck2. */ + duck2: Entity; + + /** + * @override + * The main loop, called frame by frame. + * @param deltaTime - The deltaTime when the script update. + */ + onUpdate(deltaTime: number): void { + const nowTime = this.engine.time.actualElapsedTime; + const sinFactor = Math.sin(2 * nowTime); + + // Update duck0's position. + const positionFactor = Math.max(sinFactor, 0); + this.duck0.transform.setPosition(0, positionFactor, 0); + + // Update duck1's roatation. + const rotateFactor = nowTime * 100; + this.duck1.transform.setRotation(0, rotateFactor, 0); + + // Update duck2's scale. + const scaleFactor = (sinFactor + 1.0) * 0.01; + this.duck2.transform.setScale(scaleFactor, scaleFactor, scaleFactor); + } +} diff --git a/examples/transparent-shadow.ts b/examples/transparent-shadow.ts new file mode 100644 index 000000000..0d0e28833 --- /dev/null +++ b/examples/transparent-shadow.ts @@ -0,0 +1,199 @@ +/** + * @title Transparent Shadow + * @category Light + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*B1m6SKbaYEsAAAAAAAAAAAAADiR2AQ/original + */ +import * as dat from "dat.gui"; +import { + AssetType, + BackgroundMode, + BaseMaterial, + Camera, + Color, + DirectLight, + Engine, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + Script, + Shader, + ShaderPass, + ShadowResolution, + ShadowType, + Vector3, + WebGLEngine, +} from "@galacean/engine"; + +const customForwardPass = new ShaderPass( + ` +#include +#include +#include +#include + +#include + +void main() { + + #include + #include + #include + #include + #include + #include + + #include +} +`, + ` +#include +#include +#include +#include +#include + +uniform vec4 material_BaseColor; +uniform float material_AlphaCutoff; + +void main() { + float shadowAttenuation = 1.0; + #ifdef SCENE_IS_CALCULATE_SHADOWS + shadowAttenuation *= sampleShadowMap(); + #endif + + gl_FragColor = vec4(material_BaseColor.rgb, saturate(1.0 - shadowAttenuation) * material_BaseColor.a); + + #ifndef ENGINE_IS_COLORSPACE_GAMMA + gl_FragColor = linearToGamma(gl_FragColor); + #endif +} +`, + { pipelineStage: "Forward" } +); + +Shader.create("transparent-shadow", [ + customForwardPass, + Shader.find("pbr").subShaders[0].passes[1], // PBR shader builtin shadow caster pass +]); + +class TransparentShadow extends BaseMaterial { + /** + * Base color. + */ + get baseColor(): Color { + return this.shaderData.getColor(TransparentShadow._baseColorProp); + } + + set baseColor(value: Color) { + const baseColor = this.shaderData.getColor( + TransparentShadow._baseColorProp + ); + if (value !== baseColor) { + baseColor.copyFrom(value); + } + } + + constructor(engine: Engine) { + super(engine, Shader.find("transparent-shadow")); + this.isTransparent = true; + this.shaderData.setColor( + TransparentShadow._baseColorProp, + new Color(0, 0, 0, 1) + ); + this.shaderData.enableMacro("MATERIAL_NEED_WORLD_POS"); + } +} + +class Rotation extends Script { + pause = false; + private _time = 0; + + onUpdate(deltaTime: number) { + if (!this.pause) { + this._time += deltaTime; + this.entity.transform.setRotation(0, this._time * 50, 0); + } + } +} + +const gui = new dat.GUI(); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + scene.shadowResolution = ShadowResolution.High; + scene.shadowDistance = 800; + + const rootEntity = engine.sceneManager.activeScene.createRootEntity(); + + const cameraEntity = rootEntity.createChild("camera"); + cameraEntity.transform.setPosition(-140, 210, 1020); + cameraEntity.transform.setRotation(0, -16, 0); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 800; + + const transparentShadowMtl = new TransparentShadow(engine); + transparentShadowMtl.baseColor.set(9 / 255, 8 / 255, 9 / 255, 1); + const debugMtl = new PBRMaterial(engine); + debugMtl.baseColor.set(1, 0, 0, 0.5); + debugMtl.isTransparent = true; + + const planeEntity = rootEntity.createChild(); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeRenderer.mesh = PrimitiveMesh.createPlane(engine, 300, 2000); + planeRenderer.setMaterial(transparentShadowMtl); + + // init direct light + const light = rootEntity.createChild("light"); + light.transform.setPosition(-140, 1000, -1020); + light.transform.lookAt(new Vector3(30, 0, 300)); + const directLight = light.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + directLight.shadowStrength = 0.75; + + engine.resourceManager + .load([ + { + url: "https://gw.alipayobjects.com/os/bmw-prod/93196534-bab3-4559-ae9f-bcb3e36a6419.glb", + type: AssetType.GLTF, + }, + { + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + type: AssetType.Env, + }, + { + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*X0IjQ5E1OUEAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }, + ]) + .then(([gltf, ambientLight, background]) => { + gltf.defaultSceneRoot.addComponent(Rotation); + const character = rootEntity.createChild("gltf"); + character.transform.setScale(20, 20, 20); + character.transform.setPosition(100, 0, 300); + character.addChild(gltf.defaultSceneRoot); + + scene.background.mode = BackgroundMode.Texture; + scene.background.texture = background; + + scene.ambientLight = ambientLight; + scene.ambientLight.specularIntensity = 0.1; + openDebug(); + engine.run(); + }); + + function openDebug() { + const info = { + debug: false, + }; + + gui.add(info, "debug").onChange((v) => { + if (v) { + planeRenderer.setMaterial(debugMtl); + } else { + planeRenderer.setMaterial(transparentShadowMtl); + } + }); + } +}); diff --git a/examples/unlit-material.ts b/examples/unlit-material.ts new file mode 100644 index 000000000..1fd6fb7fb --- /dev/null +++ b/examples/unlit-material.ts @@ -0,0 +1,57 @@ +/** + * @title Unlit Material + * @category Material + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*nWlHSZjfBc0AAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + Animator, + Camera, + GLTFResource, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.position = new Vector3(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + engine.run(); + + engine.resourceManager + .load( + "https://gw.alipayobjects.com/os/bmw-prod/8d36415b-5905-461f-9336-68a23d41518e.gltf" + ) + .then((gltf) => { + const { materials, animations, defaultSceneRoot } = gltf; + rootEntity.addChild(defaultSceneRoot); + + const animator = defaultSceneRoot.getComponent(Animator); + animator.play(animations[0].name); + addGUI(materials as UnlitMaterial[]); + }); + + function addGUI(materials: UnlitMaterial[]) { + const state = { + baseColor: [255, 255, 255], + }; + + gui.addColor(state, "baseColor").onChange((v) => { + materials.forEach((material) => { + material.baseColor.set(v[0] / 255, v[1] / 255, v[2] / 255, 1); + }); + }); + } +}); diff --git a/examples/video-background.ts b/examples/video-background.ts new file mode 100644 index 000000000..4003a6f06 --- /dev/null +++ b/examples/video-background.ts @@ -0,0 +1,124 @@ +/** + * @title Video Background + * @category Video + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*8t0SSqZFaXcAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import { + BackgroundMode, + Camera, + CompareFunction, + CullMode, + Engine, + Material, + PrimitiveMesh, + Script, + Shader, + Texture2D, + TextureFormat, + TextureUsage, + WebGLEngine, +} from "@galacean/engine"; + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild(); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 80; + const control = cameraEntity.addComponent(OrbitControl); + control.autoRotate = true; + control.autoRotateSpeed = 0.1; + engine.run(); + + // video skybox + Shader.create( + "video", + ` +attribute vec3 POSITION; +attribute vec2 TEXCOORD_0; +varying vec2 v_uv; + +uniform mat4 camera_VPMat; + +void main() { + v_uv = TEXCOORD_0; + gl_Position = camera_VPMat * vec4( POSITION, 1.0 ); +} + `, + ` + uniform sampler2D u_texture; + varying vec2 v_uv; + + void main(){ + gl_FragColor = texture2D(u_texture, vec2(-v_uv.x,v_uv.y)); + + } + ` + ); + class VideoMaterial extends Material { + constructor(engine: Engine) { + super(engine, Shader.find("video")); + + this.renderState.rasterState.cullMode = CullMode.Off; + this.renderState.depthState.compareFunction = CompareFunction.LessEqual; + } + + get texture(): Texture2D { + return this.shaderData.getTexture("u_texture") as Texture2D; + } + + set texture(v: Texture2D) { + this.shaderData.setTexture("u_texture", v); + } + } + + class UpdateVideoScript extends Script { + video: HTMLVideoElement; + texture: Texture2D; + + onUpdate() { + this.texture.setImageSource(this.video); + } + } + + const dom: HTMLVideoElement = document.createElement("video"); + const width = 3840; + const height = 1920; + dom.src = + "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/file/A*p_f5QYjE_2kAAAAAAAAAAAAAARQnAQ"; + dom.crossOrigin = "anonymous"; + dom.loop = true; + dom.muted = true; + dom.play(); + + // create video background + const texture = new Texture2D( + engine, + width, + height, + TextureFormat.R8G8B8, + false, + TextureUsage.Dynamic + ); + const { background } = scene; + background.mode = BackgroundMode.Sky; + const skyMaterial = (background.sky.material = new VideoMaterial(engine)); + background.sky.mesh = PrimitiveMesh.createSphere(engine, 2); + skyMaterial.texture = texture; + + function updateVideo() { + texture.setImageSource(dom); + (dom as any).requestVideoFrameCallback(updateVideo); + } + + if ("requestVideoFrameCallback" in dom) { + (dom as any).requestVideoFrameCallback(updateVideo); + } else { + const script = rootEntity.addComponent(UpdateVideoScript); + script.video = dom; + script.texture = texture; + } +}); diff --git a/examples/video-transparent.ts b/examples/video-transparent.ts new file mode 100644 index 000000000..2b5f44f03 --- /dev/null +++ b/examples/video-transparent.ts @@ -0,0 +1,150 @@ +/** + * @title Video Transparent + * @category Video + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*2CRnR4sZL0kAAAAAAAAAAAAADiR2AQ/original + */ + +import { + BaseMaterial, + BlendMode, + Camera, + Engine, + Script, + Shader, + Sprite, + SpriteRenderer, + Texture2D, + TextureFormat, + TextureUsage, + WebGLEngine, +} from "@galacean/engine"; + +// 透明度信息所在方向 +function getFragmentByDirection(direction) { + const rgb = `vec2(v_uv.x * 0.5000 + 0.5000, v_uv.y)`; + const alpha = `vec2(v_uv.x * 0.5000, v_uv.y)`; + const _rgb = `vec2(v_uv.x, v_uv.y * 0.5000 + 0.5000)`; + const _alpha = `vec2(v_uv.x, v_uv.y * 0.5000)`; + switch (direction) { + case "left": + return `vec2 uv_rgb = ${rgb};vec2 uv_alpha = ${alpha};`; + case "right": + return `vec2 uv_rgb = ${alpha};vec2 uv_alpha = ${rgb};`; + case "top": + return `vec2 uv_rgb = ${_rgb};vec2 uv_alpha = ${_alpha};`; + case "down": + return `vec2 uv_rgb = ${_alpha};vec2 uv_alpha = ${_rgb};`; + default: + return `vec2 uv_rgb = ${_alpha};vec2 uv_alpha = ${_rgb};`; + } +} + +class TransparentVideoMaterial extends BaseMaterial { + static direction: "left" | "right" | "up" | "down" = "right"; + + constructor(engine: Engine) { + const name = "TransparentVideo" + TransparentVideoMaterial.direction; + const shader = + Shader.find(name) || + Shader.create( + name, + ` + attribute vec3 POSITION; + attribute vec2 TEXCOORD_0; + + uniform mat4 renderer_MVPMat; + varying vec2 v_uv; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_uv = TEXCOORD_0; + } + `, + + ` + precision highp float; + uniform sampler2D renderer_SpriteTexture; + varying vec2 v_uv; + void main() { + ${getFragmentByDirection(TransparentVideoMaterial.direction)} + vec3 rgb = texture2D(renderer_SpriteTexture, uv_rgb).rgb; + float alpha = texture2D(renderer_SpriteTexture, uv_alpha).r; + gl_FragColor = vec4(rgb / alpha, alpha); + } + ` + ); + super(engine, shader); + this.setState(); + } + + setState() { + this.isTransparent = true; + this.blendMode = BlendMode.Normal; + } +} + +class UpdateVideoScript extends Script { + video: HTMLVideoElement; + texture: Texture2D; + + onUpdate() { + this.texture.setImageSource(this.video); + } +} + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + const cameraEntity = rootEntity.createChild(); + cameraEntity.transform.setPosition(0, 0, 10); + const camera = cameraEntity.addComponent(Camera); + camera.fieldOfView = 80; + + engine.run(); + + const dom: HTMLVideoElement = document.createElement("video"); + // 视频分辨率 + const width = 1500; + const height = 1624; + dom.src = + "https://gw.alipayobjects.com/v/wufu_ainianhua/afts/video/zKhSTJqO8dUAAAAAAAAAAAAALW6BAQBr"; + dom.crossOrigin = "anonymous"; + dom.loop = true; + dom.muted = true; + dom.play(); + + // create video texture + const texture = new Texture2D( + engine, + width, + height, + TextureFormat.R8G8B8, + false, + TextureUsage.Dynamic + ); + + // 创建精灵用于渲染视频 + const entity = rootEntity.createChild("video-transparent"); + const sr = entity.addComponent(SpriteRenderer); + sr.sprite = new Sprite(engine, texture); + // 初始化的时候调用一次即可 + sr.width *= 0.5; + // 视频左半边存储透明度 + TransparentVideoMaterial.direction = "left"; + const material = new TransparentVideoMaterial(engine); + sr.setMaterial(material); + + function updateVideo() { + texture.setImageSource(dom); + (dom as any).requestVideoFrameCallback(updateVideo); + } + + if ("requestVideoFrameCallback" in dom) { + (dom as any).requestVideoFrameCallback(updateVideo); + } else { + const script = rootEntity.addComponent(UpdateVideoScript); + script.video = dom; + script.texture = texture; + } +}); diff --git a/examples/wrap-mode.ts b/examples/wrap-mode.ts new file mode 100644 index 000000000..56c3387a2 --- /dev/null +++ b/examples/wrap-mode.ts @@ -0,0 +1,78 @@ +/** + * @title Wrap Mode + * @category Texture + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*8YM_SKVG3HsAAAAAAAAAAAAADiR2AQ/original + */ +import { OrbitControl } from "@galacean/engine-toolkit-controls"; +import * as dat from "dat.gui"; +import { + AssetType, + Camera, + CullMode, + MeshRenderer, + PrimitiveMesh, + Texture2D, + TextureFilterMode, + TextureWrapMode, + UnlitMaterial, + WebGLEngine, +} from "@galacean/engine"; +const gui = new dat.GUI(); + +// Create engine +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity(); + + // Create camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 0, 5); + cameraEntity.addComponent(Camera); + cameraEntity.addComponent(OrbitControl); + + // Create Plane + const mesh = PrimitiveMesh.createPlane(engine, 2, 2); + const material = new UnlitMaterial(engine); + material.tilingOffset.x = 2; + material.tilingOffset.y = 2; + material.isTransparent = true; + material.renderState.rasterState.cullMode = CullMode.Off; + const planeEntity = rootEntity.createChild("plane"); + const planeRenderer = planeEntity.addComponent(MeshRenderer); + planeEntity.transform.setRotation(90, 0, 0); + planeRenderer.mesh = mesh; + planeRenderer.setMaterial(material); + + engine.resourceManager + .load({ + url: "https://gw.alipayobjects.com/mdn/rms_7c464e/afts/img/A*rgNGR4Vb7lQAAAAAAAAAAAAAARQnAQ", + type: AssetType.Texture2D, + }) + .then((texture) => { + material.baseTexture = texture; + addGUI(texture); + engine.run(); + }); + + function addGUI(texture: Texture2D) { + const wrapModeMap: Record = { + [TextureWrapMode.Clamp]: "Clamp", + [TextureWrapMode.Repeat]: "Repeat", + [TextureWrapMode.Mirror]: "Mirror", + }; + const state = { + wrapMode: wrapModeMap[texture.wrapModeU], + }; + gui.add(state, "wrapMode", Object.values(wrapModeMap)).onChange((v) => { + for (let key in wrapModeMap) { + const value = wrapModeMap[key]; + if (v === value) { + texture.wrapModeU = Number(key); + texture.wrapModeV = Number(key); + } + } + }); + } +}); diff --git a/examples/xr-ar-imageTracking.ts b/examples/xr-ar-imageTracking.ts new file mode 100644 index 000000000..afb6ade2b --- /dev/null +++ b/examples/xr-ar-imageTracking.ts @@ -0,0 +1,272 @@ +/** + * @title AR image tracking + * @category XR + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*-LBVTK0rt7kAAAAAAAAAAAAADiR2AQ/original + */ + +import { + Camera, + Color, + Entity, + MeshRenderer, + PrimitiveMesh, + Script, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { + XRImageTracking, + XRReferenceImage, + XRSessionMode, + XRTrackedImage, + XRTrackedInputDevice, +} from "@galacean/engine-xr"; +import { WebXRDevice } from "@galacean/engine-xr-webxr"; + +WebGLEngine.create({ + canvas: "canvas", + xrDevice: new WebXRDevice(), +}).then((engine) => { + // 设置屏幕分辨率 + engine.canvas.resizeByClientSize(1); + const { sceneManager, xrManager } = engine; + const scene = sceneManager.scenes[0]; + const origin = (xrManager.origin = scene.createRootEntity("origin")); + const camera = origin.createChild("Camera").addComponent(Camera); + xrManager.cameraManager.attachCamera(XRTrackedInputDevice.Camera, camera); + const image = new Image(); + image.onload = () => { + const refImage = new XRReferenceImage("test", image, 0.08); + xrManager.addFeature(XRImageTracking, [refImage]); + const prefab = new Entity(engine); + prefab.addComponent(Axis); + origin.addComponent(XRTrackedImageManager).prefab = prefab; + xrManager.sessionManager.isSupportedMode(XRSessionMode.AR).then( + () => { + const content = xrManager.isSupportedFeature(XRImageTracking) + ? "Enter AR" + : "Not Support Image Tracking"; + addXRButton(content).onclick = () => { + xrManager.enterXR(XRSessionMode.AR); + }; + }, + (error) => { + addXRButton("Not Support"); + console.error(error); + } + ); + }; + + image.src = + "https://mdn.alipayobjects.com/huamei_jvf0dp/afts/img/A*br03RK1-XTMAAAAAAAAAAAAADleLAQ/original"; + engine.run(); +}); + +class XRTrackedImageManager extends Script { + private _prefab: Entity; + private _trackIdToIndex: number[] = []; + private _trackedComponents: Array = []; + + get prefab(): Entity { + return this._prefab; + } + + set prefab(value: Entity) { + this._prefab = value; + } + + getTrackedComponentByTrackId(trackId: number): TrackedComponent | null { + const index = this._trackIdToIndex[trackId]; + return index !== undefined ? this._trackedComponents[index] : null; + } + + override onAwake(): void { + const imageTracking = this._engine.xrManager.getFeature(XRImageTracking); + this._onChanged = this._onChanged.bind(this); + imageTracking?.addChangedListener(this._onChanged); + } + + private _onChanged( + added: readonly XRTrackedImage[], + updated: readonly XRTrackedImage[], + removed: readonly XRTrackedImage[] + ) { + if (added.length > 0) { + for (let i = 0, n = added.length; i < n; i++) { + this._createOrUpdateTrackedComponents(added[i]); + } + } + if (updated.length > 0) { + for (let i = 0, n = updated.length; i < n; i++) { + this._createOrUpdateTrackedComponents(updated[i]); + } + } + if (removed.length > 0) { + const { + _trackIdToIndex: trackIdToIndex, + _trackedComponents: trackedComponents, + } = this; + for (let i = 0, n = removed.length; i < n; i++) { + const { id } = removed[i]; + const index = trackIdToIndex[id]; + if (index !== undefined) { + const trackedComponent = trackedComponents[index]; + trackedComponents.splice(index, 1); + delete trackIdToIndex[id]; + if (trackedComponent.destroyedOnRemoval) { + trackedComponent.entity.destroy(); + } else { + trackedComponent.entity.parent = null; + } + } + } + } + } + + private _createOrUpdateTrackedComponents( + sessionRelativeData: XRTrackedImage + ): TrackedComponent { + let trackedComponent = this.getTrackedComponentByTrackId( + sessionRelativeData.id + ); + if (!trackedComponent) { + const { + _trackIdToIndex: trackIdToIndex, + _trackedComponents: trackedComponents, + } = this; + trackedComponent = this._createTrackedComponents(sessionRelativeData); + trackIdToIndex[sessionRelativeData.id] = trackedComponents.length; + trackedComponents.push(trackedComponent); + } + trackedComponent.data = sessionRelativeData; + const { transform } = trackedComponent.entity; + const { pose } = sessionRelativeData; + transform.position = pose.position; + transform.rotationQuaternion = pose.rotation; + return trackedComponent; + } + + private _createTrackedComponents( + sessionRelativeData: XRTrackedImage + ): TrackedComponent { + const { origin } = this._engine.xrManager; + const { _prefab: prefab } = this; + let entity: Entity; + if (prefab) { + entity = prefab.clone(); + entity.name = `TrackedImage${sessionRelativeData.id}`; + origin.addChild(entity); + } else { + entity = origin.createChild(`TrackedImage${sessionRelativeData.id}`); + } + const trackedComponent = entity.addComponent(TrackedComponent); + return trackedComponent; + } +} + +export class TrackedComponent extends Script { + private _data: XRTrackedImage; + private _destroyedOnRemoval = true; + + get destroyedOnRemoval(): boolean { + return this._destroyedOnRemoval; + } + + set destroyedOnRemoval(value: boolean) { + this._destroyedOnRemoval = value; + } + + get data(): XRTrackedImage { + return this._data; + } + + set data(value: XRTrackedImage) { + this._data = value; + } +} + +class Axis extends Script { + private _length = 0.1; + private _arrows: Record = {}; + private _sides: Record = {}; + + get length(): number { + return this._length; + } + + set length(value: number) { + if (this._length !== value) { + this._length = value; + this._reset(value); + } + } + + onStart(): void { + this._initSide("x", new Vector3(0, 0, -90), new Color(1, 0, 0, 1)); + this._initSide("y", new Vector3(0, 0, 0), new Color(0, 1, 0, 1)); + this._initSide("z", new Vector3(90, 0, 0), new Color(0, 0, 1, 1)); + this._initArrow("x", new Vector3(0, 0, -90), new Color(1, 0, 0, 1)); + this._initArrow("y", new Vector3(0, 0, 0), new Color(0, 1, 0, 1)); + this._initArrow("z", new Vector3(90, 0, 0), new Color(0, 0, 1, 1)); + this._reset(this._length); + } + + private _reset(length: number): void { + const { _arrows: arrows, _sides: sides } = this; + arrows.x.transform.setPosition(length, 0, 0); + arrows.y.transform.setPosition(0, length, 0); + arrows.z.transform.setPosition(0, 0, length); + + sides.x.transform.setScale(1, length, 1); + sides.y.transform.setScale(1, length, 1); + sides.z.transform.setScale(1, length, 1); + } + + private _initArrow(type: string, rot: Vector3, col: Color): void { + const { engine, entity } = this; + const arrow = (this._arrows[type] = entity.createChild("arrow" + type)); + const arrowRenderer = arrow.addComponent(MeshRenderer); + arrowRenderer.mesh = PrimitiveMesh.createCone(engine, 0.004, 0.012); + const material = new UnlitMaterial(engine); + material.baseColor = col; + arrowRenderer.setMaterial(material); + arrow.transform.rotation = rot; + } + + private _initSide(type: string, rot: Vector3, col: Color): void { + const { engine, entity } = this; + const side = (this._sides[type] = entity.createChild("side" + type)); + const rendererEntity = side.createChild("rendererEntity"); + rendererEntity.transform.position.set(0, 0.5, 0); + const renderer = rendererEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCylinder(engine, 0.002, 0.002, 1); + const material = new UnlitMaterial(engine); + material.baseColor = col; + renderer.setMaterial(material); + side.transform.rotation = rot; + } +} + +function addXRButton(content: string): HTMLButtonElement { + const button = document.createElement("button"); + button.textContent = content; + const { style } = button; + style.position = "absolute"; + style.bottom = "20px"; + style.padding = "12px 6px"; + style.border = "1px solid rgb(255, 255, 255)"; + style.borderRadius = "4px"; + style.background = "rgba(0, 0, 0, 0.1)"; + style.color = "rgb(255, 255, 255)"; + style.font = "13px sans-serif"; + style.textAlign = "center"; + style.opacity = "0.5"; + style.outline = "none"; + style.zIndex = "999"; + style.cursor = "pointer"; + style.left = "calc(50% - 50px)"; + style.width = "100px"; + document.body.appendChild(button); + return button; +} diff --git a/examples/xr-ar-planeTracking.ts b/examples/xr-ar-planeTracking.ts new file mode 100644 index 000000000..4174bb37e --- /dev/null +++ b/examples/xr-ar-planeTracking.ts @@ -0,0 +1,297 @@ +/** + * @title AR plane tracking + * @category XR + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*MngORYg29VUAAAAAAAAAAAAADiR2AQ/original + */ + +import { + Camera, + Color, + Entity, + MeshRenderer, + MeshTopology, + ModelMesh, + PrimitiveMesh, + Script, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { + XRPlaneMode, + XRPlaneTracking, + XRSessionMode, + XRTrackedInputDevice, + XRTrackedPlane, +} from "@galacean/engine-xr"; +import { WebXRDevice } from "@galacean/engine-xr-webxr"; + +WebGLEngine.create({ + canvas: "canvas", + xrDevice: new WebXRDevice(), +}).then((engine) => { + // 设置屏幕分辨率 + engine.canvas.resizeByClientSize(1); + const { sceneManager, xrManager } = engine; + const scene = sceneManager.scenes[0]; + const origin = (xrManager.origin = scene.createRootEntity("origin")); + const camera = origin.createChild("Camera").addComponent(Camera); + xrManager.cameraManager.attachCamera(XRTrackedInputDevice.Camera, camera); + + const entity = new Entity(engine); + entity.addComponent(Axis); + entity.addComponent(XRPlane); + origin.addComponent(XRTrackedPlaneManager).prefab = entity; + + try { + xrManager.addFeature(XRPlaneTracking, XRPlaneMode.EveryThing); + xrManager.sessionManager.isSupportedMode(XRSessionMode.AR).then( + () => { + const content = xrManager.isSupportedFeature(XRPlaneTracking) + ? "Enter AR" + : "Not Support Plane Tracking"; + addXRButton(content).onclick = () => { + xrManager.enterXR(XRSessionMode.AR); + }; + }, + (error) => { + addXRButton("Not Support"); + console.error(error); + } + ); + } catch (error) { + addXRButton("Not Support"); + } + + engine.run(); +}); + +class XRPlane extends Script { + onStart(): void { + const trackedComponent = this.entity.getComponent(TrackedComponent); + const trackedPlane = trackedComponent?.data; + if (!trackedPlane) return; + const { entity, engine } = this; + const renderer = entity.addComponent(MeshRenderer); + const mesh = new ModelMesh(engine); + mesh.setPositions(trackedPlane.polygon); + mesh.addSubMesh(0, trackedPlane.polygon.length, MeshTopology.TriangleStrip); + renderer.mesh = mesh; + const material = new UnlitMaterial(engine); + material.baseColor = new Color( + Math.random(), + Math.random(), + Math.random(), + 0.5 + ); + renderer.setMaterial(material); + } +} + +class XRTrackedPlaneManager extends Script { + private _prefab: Entity; + private _trackIdToIndex: number[] = []; + private _trackedComponents: Array = []; + + get prefab(): Entity { + return this._prefab; + } + + set prefab(value: Entity) { + this._prefab = value; + } + + getTrackedComponentByTrackId(trackId: number): TrackedComponent | null { + const index = this._trackIdToIndex[trackId]; + return index !== undefined ? this._trackedComponents[index] : null; + } + + override onAwake(): void { + const planeTracking = this._engine.xrManager.getFeature(XRPlaneTracking); + this._onChanged = this._onChanged.bind(this); + planeTracking?.addChangedListener(this._onChanged); + } + + private _onChanged( + added: readonly XRTrackedPlane[], + updated: readonly XRTrackedPlane[], + removed: readonly XRTrackedPlane[] + ) { + if (added.length > 0) { + for (let i = 0, n = added.length; i < n; i++) { + this._createOrUpdateTrackedComponents(added[i]); + } + } + if (updated.length > 0) { + for (let i = 0, n = updated.length; i < n; i++) { + this._createOrUpdateTrackedComponents(updated[i]); + } + } + if (removed.length > 0) { + const { + _trackIdToIndex: trackIdToIndex, + _trackedComponents: trackedComponents, + } = this; + for (let i = 0, n = removed.length; i < n; i++) { + const { id } = removed[i]; + const index = trackIdToIndex[id]; + if (index !== undefined) { + const trackedComponent = trackedComponents[index]; + trackedComponents.splice(index, 1); + delete trackIdToIndex[id]; + if (trackedComponent.destroyedOnRemoval) { + trackedComponent.entity.destroy(); + } else { + trackedComponent.entity.parent = null; + } + } + } + } + } + + private _createOrUpdateTrackedComponents( + sessionRelativeData: XRTrackedPlane + ): TrackedComponent { + let trackedComponent = this.getTrackedComponentByTrackId( + sessionRelativeData.id + ); + if (!trackedComponent) { + const { + _trackIdToIndex: trackIdToIndex, + _trackedComponents: trackedComponents, + } = this; + trackedComponent = this._createTrackedComponents(sessionRelativeData); + trackIdToIndex[sessionRelativeData.id] = trackedComponents.length; + trackedComponents.push(trackedComponent); + } + trackedComponent.data = sessionRelativeData; + const { transform } = trackedComponent.entity; + const { pose } = sessionRelativeData; + transform.position = pose.position; + transform.rotationQuaternion = pose.rotation; + return trackedComponent; + } + + private _createTrackedComponents( + sessionRelativeData: XRTrackedPlane + ): TrackedComponent { + const { origin } = this._engine.xrManager; + const { _prefab: prefab } = this; + let entity: Entity; + if (prefab) { + entity = prefab.clone(); + entity.name = `TrackedPlane${sessionRelativeData.id}`; + origin.addChild(entity); + } else { + entity = origin.createChild(`TrackedPlane${sessionRelativeData.id}`); + } + const trackedComponent = entity.addComponent(TrackedComponent); + return trackedComponent; + } +} + +export class TrackedComponent extends Script { + private _data: XRTrackedPlane; + private _destroyedOnRemoval = true; + + get destroyedOnRemoval(): boolean { + return this._destroyedOnRemoval; + } + + set destroyedOnRemoval(value: boolean) { + this._destroyedOnRemoval = value; + } + + get data(): XRTrackedPlane { + return this._data; + } + + set data(value: XRTrackedPlane) { + this._data = value; + } +} + +class Axis extends Script { + private _length = 0.1; + private _arrows: Record = {}; + private _sides: Record = {}; + + get length(): number { + return this._length; + } + + set length(value: number) { + if (this._length !== value) { + this._length = value; + this._reset(value); + } + } + + onStart(): void { + this._initSide("x", new Vector3(0, 0, -90), new Color(1, 0, 0, 1)); + this._initSide("y", new Vector3(0, 0, 0), new Color(0, 1, 0, 1)); + this._initSide("z", new Vector3(90, 0, 0), new Color(0, 0, 1, 1)); + this._initArrow("x", new Vector3(0, 0, -90), new Color(1, 0, 0, 1)); + this._initArrow("y", new Vector3(0, 0, 0), new Color(0, 1, 0, 1)); + this._initArrow("z", new Vector3(90, 0, 0), new Color(0, 0, 1, 1)); + this._reset(this._length); + } + + private _reset(length: number): void { + const { _arrows: arrows, _sides: sides } = this; + arrows.x.transform.setPosition(length, 0, 0); + arrows.y.transform.setPosition(0, length, 0); + arrows.z.transform.setPosition(0, 0, length); + + sides.x.transform.setScale(1, length, 1); + sides.y.transform.setScale(1, length, 1); + sides.z.transform.setScale(1, length, 1); + } + + private _initArrow(type: string, rot: Vector3, col: Color): void { + const { engine, entity } = this; + const arrow = (this._arrows[type] = entity.createChild("arrow" + type)); + const arrowRenderer = arrow.addComponent(MeshRenderer); + arrowRenderer.mesh = PrimitiveMesh.createCone(engine, 0.004, 0.012); + const material = new UnlitMaterial(engine); + material.baseColor = col; + arrowRenderer.setMaterial(material); + arrow.transform.rotation = rot; + } + + private _initSide(type: string, rot: Vector3, col: Color): void { + const { engine, entity } = this; + const side = (this._sides[type] = entity.createChild("side" + type)); + const rendererEntity = side.createChild("rendererEntity"); + rendererEntity.transform.position.set(0, 0.5, 0); + const renderer = rendererEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCylinder(engine, 0.002, 0.002, 1); + const material = new UnlitMaterial(engine); + material.baseColor = col; + renderer.setMaterial(material); + side.transform.rotation = rot; + } +} + +function addXRButton(content: string): HTMLButtonElement { + const button = document.createElement("button"); + button.textContent = content; + const { style } = button; + style.position = "absolute"; + style.bottom = "20px"; + style.padding = "12px 6px"; + style.border = "1px solid rgb(255, 255, 255)"; + style.borderRadius = "4px"; + style.background = "rgba(0, 0, 0, 0.1)"; + style.color = "rgb(255, 255, 255)"; + style.font = "13px sans-serif"; + style.textAlign = "center"; + style.opacity = "0.5"; + style.outline = "none"; + style.zIndex = "999"; + style.cursor = "pointer"; + style.left = "calc(50% - 50px)"; + style.width = "100px"; + document.body.appendChild(button); + return button; +} diff --git a/examples/xr-ar-simple.ts b/examples/xr-ar-simple.ts new file mode 100644 index 000000000..d53a1103b --- /dev/null +++ b/examples/xr-ar-simple.ts @@ -0,0 +1,89 @@ +/** + * @title AR simple + * @category XR + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*t4cXTbFa6kkAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + Camera, + DirectLight, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { XRSessionMode, XRTrackedInputDevice } from "@galacean/engine-xr"; +import { WebXRDevice } from "@galacean/engine-xr-webxr"; + +// Create engine +WebGLEngine.create({ canvas: "canvas", xrDevice: new WebXRDevice() }).then( + (engine) => { + engine.canvas.resizeByClientSize(1); + const { sceneManager, xrManager } = engine; + const scene = sceneManager.scenes[0]; + const origin = (xrManager.origin = scene.createRootEntity("origin")); + // init direct light + const light = origin.createChild("light"); + light.transform.setPosition(-10, 10, 10); + light.transform.lookAt(new Vector3()); + light.addComponent(DirectLight); + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + }); + + const ball = origin.createChild("ball"); + const renderer = ball.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(engine, 0.5, 24); + const material = new PBRMaterial(engine); + material.metallic = 0.5; + material.roughness = 0.5; + renderer.setMaterial(new PBRMaterial(engine)); + ball.transform.translate(0, 0, -3); + const camera = origin.createChild("camera").addComponent(Camera); + xrManager.cameraManager.attachCamera(XRTrackedInputDevice.Camera, camera); + xrManager.sessionManager.isSupportedMode(XRSessionMode.AR).then( + () => { + addXRButton("Enter AR").onclick = () => { + xrManager.enterXR(XRSessionMode.AR); + }; + }, + (error) => { + addXRButton("Not Support"); + console.error(error); + } + ); + engine.run(); + } +); + +function addXRButton(content: string): HTMLButtonElement { + const button = document.createElement("button"); + button.textContent = content; + const { style } = button; + style.position = "absolute"; + style.bottom = "20px"; + style.padding = "12px 6px"; + style.border = "1px solid rgb(255, 255, 255)"; + style.borderRadius = "4px"; + style.background = "rgba(0, 0, 0, 0.1)"; + style.color = "rgb(255, 255, 255)"; + style.font = "13px sans-serif"; + style.textAlign = "center"; + style.opacity = "0.5"; + style.outline = "none"; + style.zIndex = "999"; + style.cursor = "pointer"; + style.left = "calc(50% - 50px)"; + style.width = "100px"; + document.body.appendChild(button); + return button; +} diff --git a/examples/xr-vr-shotball.ts b/examples/xr-vr-shotball.ts new file mode 100644 index 000000000..fd6eeb895 --- /dev/null +++ b/examples/xr-vr-shotball.ts @@ -0,0 +1,368 @@ +/** + * @title VR shot ball + * @category XR + * @thumbnail https://mdn.alipayobjects.com/merchant_appfe/afts/img/A*PwDEQK58LPwAAAAAAAAAAAAADiR2AQ/original + */ + +import { + AmbientLight, + AssetType, + BoxColliderShape, + Camera, + Collider, + Color, + DirectLight, + DynamicCollider, + Entity, + FixedJoint, + GLTFResource, + MeshRenderer, + PBRMaterial, + PrimitiveMesh, + Quaternion, + Script, + ShadowType, + SphereColliderShape, + SpringJoint, + UnlitMaterial, + Vector3, + WebGLEngine, +} from "@galacean/engine"; +import { PhysXPhysics } from "@galacean/engine-physics-physx"; +import { + XRController, + XRInputButton, + XRInputManager, + XRSessionMode, + XRTrackedInputDevice, + XRTrackingState, +} from "@galacean/engine-xr"; +import { WebXRDevice } from "@galacean/engine-xr-webxr"; +import { XRInput } from "@galacean/engine-xr/types/input/XRInput"; +// Create engine +WebGLEngine.create({ + canvas: "canvas", + xrDevice: new WebXRDevice(), + physics: new PhysXPhysics(), +}).then((engine) => { + const { sceneManager, xrManager } = engine; + const scene = sceneManager.scenes[0]; + const origin = (xrManager.origin = scene.createRootEntity("origin")); + engine.canvas.resizeByClientSize(1); + + createChain(origin, new Vector3(5.0, 10.0, -22.0), new Quaternion(), 10, 2.0); + createSpring(origin, new Vector3(-5.0, 5.0, -21.0), new Quaternion()); + + // init direct light + const light = origin.createChild("light"); + light.transform.setPosition(-10, 10, 10); + light.transform.lookAt(new Vector3()); + const directLight = light.addComponent(DirectLight); + directLight.shadowType = ShadowType.SoftLow; + + engine.resourceManager + .load({ + type: AssetType.Env, + url: "https://gw.alipayobjects.com/os/bmw-prod/89c54544-1184-45a1-b0f5-c0b17e5c3e68.bin", + }) + .then((ambientLight) => { + scene.ambientLight = ambientLight; + }); + + const leftCamera = origin.createChild("leftCamera").addComponent(Camera); + xrManager.cameraManager.attachCamera( + XRTrackedInputDevice.LeftCamera, + leftCamera + ); + const rightCamera = origin.createChild("rightCamera").addComponent(Camera); + xrManager.cameraManager.attachCamera( + XRTrackedInputDevice.RightCamera, + rightCamera + ); + + origin.addComponent(ControllerManager); + + xrManager.sessionManager.isSupportedMode(XRSessionMode.VR).then( + () => { + addXRButton("Enter VR").onclick = () => { + xrManager.enterXR(XRSessionMode.VR); + }; + }, + (error) => { + addXRButton("Not Support"); + console.error(error); + } + ); + engine.run(); +}); + +function addBox( + rootEntity: Entity, + size: Vector3, + position: Vector3, + rotation: Quaternion +): Entity { + const mtl = new PBRMaterial(rootEntity.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.roughness = 0.5; + mtl.metallic = 0.0; + const boxEntity = rootEntity.createChild(); + const renderer = boxEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createCuboid( + rootEntity.engine, + size.x, + size.y, + size.z + ); + renderer.setMaterial(mtl); + boxEntity.transform.position = position; + boxEntity.transform.rotationQuaternion = rotation; + + const physicsBox = new BoxColliderShape(); + physicsBox.size = size; + const boxCollider = boxEntity.addComponent(DynamicCollider); + boxCollider.addShape(physicsBox); + + return boxEntity; +} + +function transform( + position: Vector3, + rotation: Quaternion, + outPosition: Vector3, + outRotation: Quaternion +) { + Quaternion.multiply(rotation, outRotation, outRotation); + Vector3.transformByQuat(outPosition, rotation, outPosition); + outPosition.add(position); +} + +function createChain( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion, + length: number, + separation: number +) { + const offset = new Vector3(); + let prevCollider: Collider | null = null; + for (let i = 0; i < length; i++) { + const localPosition = new Vector3(0, (-separation / 2) * (2 * i + 1), 0); + const localQuaternion = new Quaternion(); + transform(position, rotation, localPosition, localQuaternion); + const currentEntity = addBox( + rootEntity, + new Vector3(2.0, 2.0, 0.5), + localPosition, + localQuaternion + ); + + const currentCollider = currentEntity.getComponent(DynamicCollider); + const fixedJoint = currentEntity.addComponent(FixedJoint); + if (prevCollider !== null) { + Vector3.subtract( + currentEntity.transform.worldPosition, + prevCollider.entity.transform.worldPosition, + offset + ); + fixedJoint.connectedAnchor = offset; + fixedJoint.connectedCollider = prevCollider; + } else { + fixedJoint.connectedAnchor = position; + } + prevCollider = currentCollider; + } +} + +function createSpring( + rootEntity: Entity, + position: Vector3, + rotation: Quaternion +) { + const currentEntity = addBox( + rootEntity, + new Vector3(2, 2, 1), + position, + rotation + ); + const springJoint = currentEntity.addComponent(SpringJoint); + springJoint.connectedAnchor = position; + springJoint.swingOffset = new Vector3(0, 1, 0); + springJoint.maxDistance = 2; + springJoint.stiffness = 10; + springJoint.damping = 1; +} + +function addSphere( + origin: Entity, + position: Vector3, + velocity: Vector3 +): Entity { + const mtl = new PBRMaterial(origin.engine); + mtl.baseColor.set(Math.random(), Math.random(), Math.random(), 1.0); + mtl.roughness = 0.5; + mtl.metallic = 0.0; + const radius = 0.35; + const sphereEntity = origin.createChild(); + const renderer = sphereEntity.addComponent(MeshRenderer); + renderer.mesh = PrimitiveMesh.createSphere(origin.engine, radius); + renderer.setMaterial(mtl); + Vector3.add(position, velocity, sphereEntity.transform.position); + + const physicsSphere = new SphereColliderShape(); + physicsSphere.radius = radius; + const sphereCollider = sphereEntity.addComponent(DynamicCollider); + sphereCollider.addShape(physicsSphere); + sphereCollider.linearVelocity = velocity.scale(50); + sphereCollider.angularDamping = 0.5; + + sphereEntity.addComponent( + class extends Script { + onUpdate(deltaTime: number): void { + if (sphereEntity.transform.worldPosition.y < -20) { + this.entity.destroy(); + } + } + } + ); + + return sphereEntity; +} + +class ControllerManager extends Script { + private _controllers: Entity[] = []; + onStart(): void { + const inputManager = this.engine.xrManager.inputManager; + inputManager.addTrackedDeviceChangedListener( + (added: readonly XRInput[], removed: readonly XRInput[]) => { + for (let i = 0, n = added.length; i < n; i++) { + const { type } = added[i]; + switch (type) { + case XRTrackedInputDevice.LeftController: + case XRTrackedInputDevice.RightController: + this._createOrAddController(type); + break; + default: + break; + } + } + for (let i = 0, n = removed.length; i < n; i++) { + const { type } = removed[i]; + switch (type) { + case XRTrackedInputDevice.LeftController: + case XRTrackedInputDevice.RightController: + this._removeController(type); + break; + default: + break; + } + } + } + ); + } + + private _createOrAddController(type: XRTrackedInputDevice): Entity { + const { _controllers: controllers, engine } = this; + let controller = controllers[type]; + if (!controller) { + controller = controllers[type] = engine.xrManager.origin.createChild( + "controller" + type + ); + controller.addComponent(Controller).type = type; + } else { + controller.isActive = true; + } + return controller; + } + + private _removeController(type: XRTrackedInputDevice): void { + const controller = this._controllers[type]; + controller && (controller.isActive = false); + } +} + +class Controller extends Script { + private _controllerURL: string[] = [ + "", + "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/meta-quest-touch-plus/left.glb", + "https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/meta-quest-touch-plus/right.glb", + ]; + private _inputManager: XRInputManager; + private _type: XRTrackedInputDevice; + private _grip: Entity; + private _ray: Entity; + private _dir: Vector3 = new Vector3(); + + get type(): XRTrackedInputDevice { + return this._type; + } + + set type(val: XRTrackedInputDevice) { + this._type = val; + } + + onStart(): void { + const { engine, entity } = this; + this._inputManager = engine.xrManager.inputManager; + + const grip = (this._grip = entity.createChild(`grip`)); + engine.resourceManager + .load(this._controllerURL[this._type]) + .then((resource) => { + grip.addChild(resource.instantiateSceneRoot()); + }); + + const ray = (this._ray = entity.createChild(`ray`)); + const sub = ray.createChild(); + const rayRenderer = sub.addComponent(MeshRenderer); + rayRenderer.mesh = PrimitiveMesh.createCylinder(engine, 0.001, 0.001, 40); + sub.transform.setRotation(90, 0, 0); + sub.transform.setPosition(0, 0, -20); + const material = new UnlitMaterial(engine); + material.baseColor = new Color(0, 1, 0); + rayRenderer.setMaterial(material); + } + + onUpdate(): void { + const { _inputManager: inputManager, _grip: grip, _ray: ray } = this; + const input = inputManager.getTrackedDevice(this._type) as XRController; + if (input.trackingState === XRTrackingState.Tracking) { + grip.transform.localMatrix = input.gripPose.matrix; + ray.transform.localMatrix = input.targetRayPose.matrix; + if (input.isButtonDown(XRInputButton.Select)) { + const forward = this._dir.copyFrom(ray.transform.worldForward); + addSphere( + this.engine.xrManager.origin, + ray.transform.position, + forward + ); + } + grip.isActive = ray.isActive = true; + } else { + grip.isActive = ray.isActive = false; + } + } +} + +function addXRButton(content: string): HTMLButtonElement { + const button = document.createElement("button"); + button.textContent = content; + const { style } = button; + style.position = "absolute"; + style.bottom = "20px"; + style.padding = "12px 6px"; + style.border = "1px solid rgb(255, 255, 255)"; + style.borderRadius = "4px"; + style.background = "rgba(0, 0, 0, 0.1)"; + style.color = "rgb(255, 255, 255)"; + style.font = "13px sans-serif"; + style.textAlign = "center"; + style.opacity = "0.5"; + style.outline = "none"; + style.zIndex = "999"; + style.cursor = "pointer"; + style.left = "calc(50% - 50px)"; + style.width = "100px"; + document.body.appendChild(button); + return button; +}