|
| 1 | +import { |
| 2 | + ChangeDetectionStrategy, |
| 3 | + Component, |
| 4 | + computed, |
| 5 | + CUSTOM_ELEMENTS_SCHEMA, |
| 6 | + effect, |
| 7 | + ElementRef, |
| 8 | + signal, |
| 9 | + viewChild, |
| 10 | +} from '@angular/core'; |
| 11 | +import { extend, injectBeforeRender, injectStore } from 'angular-three'; |
| 12 | +import { NgtsPerspectiveCamera } from 'angular-three-soba/cameras'; |
| 13 | +import { NgtsOrbitControls } from 'angular-three-soba/controls'; |
| 14 | +import { injectGLTF } from 'angular-three-soba/loaders'; |
| 15 | +import * as THREE from 'three/webgpu'; |
| 16 | + |
| 17 | +import { NgTemplateOutlet } from '@angular/common'; |
| 18 | +import { NgtsEnvironment } from 'angular-three-soba/staging'; |
| 19 | +import { NgtTweakCheckbox, NgtTweakColor, NgtTweakNumber, NgtTweakPane } from 'angular-three-tweakpane'; |
| 20 | +import { GLTF } from 'three-stdlib'; |
| 21 | +import gearsGLB from './gears.glb' with { loader: 'file' }; |
| 22 | +import { SliceMaterial } from './slice-material'; |
| 23 | + |
| 24 | +injectGLTF.preload(() => gearsGLB); |
| 25 | + |
| 26 | +interface GearsGLB extends GLTF { |
| 27 | + nodes: { |
| 28 | + axle: THREE.Mesh; |
| 29 | + gears: THREE.Mesh; |
| 30 | + outerHull: THREE.Mesh; |
| 31 | + }; |
| 32 | +} |
| 33 | + |
| 34 | +@Component({ |
| 35 | + selector: 'app-scene-graph', |
| 36 | + template: ` |
| 37 | + <ngts-perspective-camera [options]="{ makeDefault: true, position: [-5, 5, 12] }" /> |
| 38 | +
|
| 39 | + <ngt-directional-light |
| 40 | + castShadow |
| 41 | + [intensity]="4" |
| 42 | + [position]="[6.25, 3, 4]" |
| 43 | + [shadow.camera.near]="0.1" |
| 44 | + [shadow.camera.far]="30" |
| 45 | + [shadow.camera.left]="-8" |
| 46 | + [shadow.camera.right]="8" |
| 47 | + [shadow.camera.top]="8" |
| 48 | + [shadow.camera.bottom]="-8" |
| 49 | + [shadow.camera.normalBias]="0.05" |
| 50 | + [shadow.mapSize.x]="2048" |
| 51 | + [shadow.mapSize.y]="2048" |
| 52 | + /> |
| 53 | +
|
| 54 | + <ng-template #mesh let-mesh> |
| 55 | + <ngt-mesh |
| 56 | + [geometry]="mesh.geometry" |
| 57 | + [scale]="mesh.scale" |
| 58 | + [position]="mesh.position" |
| 59 | + [rotation]="mesh.rotation" |
| 60 | + castShadow |
| 61 | + receiveShadow |
| 62 | + > |
| 63 | + <ngt-mesh-physical-material [parameters]="{ metalness, roughness, envMapIntensity, color }" /> |
| 64 | + </ngt-mesh> |
| 65 | + </ng-template> |
| 66 | +
|
| 67 | + <ngt-group #gears [position]="gearsPosition"> |
| 68 | + @if (gltf(); as gltf) { |
| 69 | + @let gears = gltf.nodes.gears; |
| 70 | + @let axle = gltf.nodes.axle; |
| 71 | + @let outerHull = gltf.nodes.outerHull; |
| 72 | +
|
| 73 | + <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ $implicit: gears }" /> |
| 74 | + <ng-container [ngTemplateOutlet]="mesh" [ngTemplateOutletContext]="{ $implicit: axle }" /> |
| 75 | +
|
| 76 | + <ngt-mesh |
| 77 | + [geometry]="outerHull.geometry" |
| 78 | + [scale]="outerHull.scale" |
| 79 | + [position]="outerHull.position" |
| 80 | + [rotation]="outerHull.rotation" |
| 81 | + castShadow |
| 82 | + receiveShadow |
| 83 | + > |
| 84 | + <ngt-mesh-physical-node-material |
| 85 | + slice |
| 86 | + [arcAngle]="arcAngle()" |
| 87 | + [startAngle]="startAngle()" |
| 88 | + [sliceColor]="sliceColor()" |
| 89 | + [parameters]="{ metalness, roughness, envMapIntensity, color, side: DoubleSide }" |
| 90 | + /> |
| 91 | + </ngt-mesh> |
| 92 | + } |
| 93 | + </ngt-group> |
| 94 | +
|
| 95 | + <ngt-mesh #plane [position]="[-4, -3, -4]" [scale]="10" receiveShadow> |
| 96 | + <ngt-plane-geometry /> |
| 97 | + <ngt-mesh-standard-material color="#aaaaaa" /> |
| 98 | + </ngt-mesh> |
| 99 | +
|
| 100 | + <ngts-environment [options]="{ preset: 'warehouse', background: true, blur: 0.5 }" /> |
| 101 | + <ngts-orbit-controls /> |
| 102 | +
|
| 103 | + <ngt-tweak-pane title="Slice Material" left="8px"> |
| 104 | + <ngt-tweak-checkbox [(value)]="rotate" label="rotate" /> |
| 105 | + <ngt-tweak-color [(value)]="sliceColor" label="slice color" /> |
| 106 | + <ngt-tweak-number |
| 107 | + [(value)]="startAngleDegrees" |
| 108 | + label="start angle degrees" |
| 109 | + debounce="0" |
| 110 | + [params]="{ min: 0, max: 360, step: 1 }" |
| 111 | + /> |
| 112 | + <ngt-tweak-number |
| 113 | + [(value)]="arcAngleDegrees" |
| 114 | + label="arc angle degrees" |
| 115 | + debounce="0" |
| 116 | + [params]="{ min: 0, max: 360, step: 1 }" |
| 117 | + /> |
| 118 | + </ngt-tweak-pane> |
| 119 | + `, |
| 120 | + imports: [ |
| 121 | + NgtsPerspectiveCamera, |
| 122 | + NgtsOrbitControls, |
| 123 | + NgtsEnvironment, |
| 124 | + NgTemplateOutlet, |
| 125 | + SliceMaterial, |
| 126 | + NgtTweakPane, |
| 127 | + NgtTweakColor, |
| 128 | + NgtTweakCheckbox, |
| 129 | + NgtTweakNumber, |
| 130 | + ], |
| 131 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 132 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 133 | +}) |
| 134 | +export class SceneGraph { |
| 135 | + protected readonly DoubleSide = THREE.DoubleSide; |
| 136 | + |
| 137 | + private gearsRef = viewChild.required<ElementRef<THREE.Group>>('gears'); |
| 138 | + private planeRef = viewChild.required<ElementRef<THREE.Mesh>>('plane'); |
| 139 | + |
| 140 | + protected gltf = injectGLTF<GearsGLB>(() => gearsGLB); |
| 141 | + private store = injectStore(); |
| 142 | + |
| 143 | + protected metalness = 0.5; |
| 144 | + protected roughness = 0.25; |
| 145 | + protected envMapIntensity = 0.5; |
| 146 | + protected color = '#858080'; |
| 147 | + |
| 148 | + protected gearsPosition = new THREE.Vector3(); |
| 149 | + |
| 150 | + protected rotate = signal(true); |
| 151 | + protected sliceColor = signal('#9370DB'); |
| 152 | + protected startAngleDegrees = signal(60); |
| 153 | + protected arcAngleDegrees = signal(90); |
| 154 | + |
| 155 | + protected arcAngle = computed(() => THREE.MathUtils.DEG2RAD * this.arcAngleDegrees()); |
| 156 | + protected startAngle = computed(() => THREE.MathUtils.DEG2RAD * this.startAngleDegrees()); |
| 157 | + |
| 158 | + constructor() { |
| 159 | + extend(THREE); |
| 160 | + |
| 161 | + injectBeforeRender(({ delta }) => { |
| 162 | + const [gears, plane] = [this.gearsRef().nativeElement, this.planeRef().nativeElement]; |
| 163 | + plane.lookAt(gears.position); |
| 164 | + |
| 165 | + if (!this.rotate()) return; |
| 166 | + |
| 167 | + gears.rotation.y += 0.1 * delta; |
| 168 | + }); |
| 169 | + |
| 170 | + effect((onCleanup) => { |
| 171 | + const [scene, gl] = [this.store.scene(), this.store.gl()]; |
| 172 | + |
| 173 | + const blurriness = scene.backgroundBlurriness; |
| 174 | + const lastToneMapping = gl.toneMapping; |
| 175 | + |
| 176 | + scene.backgroundBlurriness = 0.5; |
| 177 | + gl.toneMapping = THREE.ACESFilmicToneMapping; |
| 178 | + |
| 179 | + onCleanup(() => { |
| 180 | + scene.backgroundBlurriness = blurriness; |
| 181 | + gl.toneMapping = lastToneMapping; |
| 182 | + }); |
| 183 | + }); |
| 184 | + } |
| 185 | +} |
0 commit comments