Materializr
오픈소스 파라메트릭 3D CAD로 제약 스케치·솔리드 모델·나사·SVG·텍스트 조각·STL/STEP 내보내기를 지원합니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
An interactive 3D web experience built with Three.js. A Harley-Davidson motorcycle model is displayed in a bright daytime scene. Pressing RIDE starts the animation and an orbiting camera that follows the bike as it rides its full curved route, down a two-lane street built to match its exact trajectory, with curbs and a grass shoulder on each side.
• Vite — dev server and bundler
• Three.js — 3D rendering, scene graph, animation mixer
• GLTFLoader — loads the .glb model
• OrbitControls — mouse camera interaction
Open http://localhost:5173 (or the port Vite picks if that is in use).README 전문 보기 ↗An interactive 3D web experience built with Three.js. A Harley-Davidson motorcycle model is displayed in a bright daytime scene. Pressing RIDE starts the animation and an orbiting camera that follows the bike as it rides its full curved route, down a two-lane street built to match its exact trajectory, with curbs and a grass shoulder on each side.
| Feature | Detail |
|---|---|
| 3D model | GLB format, loaded via GLTFLoader |
| Animations | All embedded clips play simultaneously and loop forever |
| Infinite forward movement | The bike never reaches the end of the road |
| Showcase orbit | Slow 360° camera rotation while the bike is stationary |
| Cinematic intro | Zoom-in → hold → zoom-out sequence when RIDE is pressed |
| Orbiting camera | Smooth 360° orbit with gentle height breathing during the ride |
| Street | A curved two-lane road (via a Catmull-Rom spline) built to match the bike's actual sampled trajectory, with curbs, a dashed centre line, edge lines, and grass shoulders |
| Part fading | Model parts far from the camera fade out to reduce clutter |
| Day atmosphere | Sky gradient, mountains, floating dust, fog |
| Action | How |
|---|---|
| RIDE | Click the amber button — starts animation and intro sequence |
| STOP | Click the amber button — freezes the bike |
| Rotate camera | Click and drag (OrbitControls active during showcase) |
| Zoom | Scroll wheel |
.glb modelnpm install
npm run dev
Open http://localhost:5173 (or the port Vite picks if that is in use).
npm run build # production bundle → dist/
src/
├── main.js Entry point — wires all modules, owns the render loop
├── state.js All shared constants and mutable runtime state
├── scene.js Renderer, camera, OrbitControls, lighting
├── environment.js Day sky: mountains, dust (stars/moon/city-glow kept but hidden)
├── ground.js Curved road/curbs/terrain (built from the bike's path) and shadow catcher
├── bike.js GLB loader, animation mixer, infinite forward movement
├── camera.js Intro sequence logic + continuous orbit update
└── ui.js RIDE / STOP buttons
public/
└── models/
└── Bobber harley.glb Harley-Davidson 3D model
src/state.jsSingle source of truth for the entire app. Contains:
ORBIT_SPEED, ORBIT_DISTANCE, ORBIT_HEIGHT, ORBIT_FOV), intro sequence timings (INTRO_ZOOM_IN_DUR, INTRO_HOLD_DUR, INTRO_ZOOM_OUT_DUR), fade distances, animation start time, model path.state object — mutable runtime flags: rideStarted, displayAngle, introPhase, introTimer, liveAngle/Distance/Height/Fov, references to the Three.js mixer, model, followTarget, activeActions, animationClips, fadedMeshes.THREE.Vector3 instances — followPosition, prevFollowPosition, smoothedOrbitCenter, smoothedLookTarget, desiredLook, meshWP, lastFadePos, loopDisplace, loopStartPos. Pre-allocated at startup so the render loop never allocates on the heap.smoothstep(t) — easing function used by the intro sequence.src/scene.jscreateScene() returns { scene, camera, renderer, controls, clock }.
WebGLRenderer targeting the #scene canvas with anti-aliasing, SRGB colour space, PCF soft shadows.PerspectiveCamera (34° FOV, near 0.05, far 90).OrbitControls with damping, pan disabled, distance clamped 1.8–16.HemisphereLight (white sky / dark-teal ground, intensity 2.1)DirectionalLight key light (white, 3.2 intensity, casts shadows with 1024² shadow map)DirectionalLight rim light (cool blue, 1.5 intensity, from upper-left-back)DirectionalLight back fill (warm amber, 1.0 intensity, from behind)src/environment.jscreateEnvironment(scene) returns { bgGroup, dust }.
Points in a random ring 3–23 units out, slowly rotating in the render loop (dust.rotation.y += delta * 0.025).ConeGeometry meshes with additive blending; hidden (visible = false) since additive blending doesn't read well against a bright day sky.bgGroup — a THREE.Group that follows the bike's XZ position every frame so the background never runs out. Contains:
CylinderGeometry, BackSide material, render order −3)visible = false); toggle them back on for a night looksrc/ground.jscreateGround(scene) returns { shadowCatcher }. The road, curbs, and terrain aren't built here — they depend on the bike's actual animated trajectory, which isn't known until bike.js has loaded the model and sampled its clips (see sampleBikePath, below).
makeAsphaltTexture(512) — procedurally generates one road tile (512×512 canvas): dark grey base fill, pixel-level noise, 420 random ellipses for aggregate stones, 7 random crack-line strokes, a dashed centre-line segment (one dash per tile, so it repeats seamlessly), and two solid white edge lines inset from where the curbs begin. Returns a THREE.CanvasTexture with RepeatWrapping.sampleSmoothPath(pathPoints, divisions) — fits an open THREE.CatmullRomCurve3 through the raw sampled points and calls curve.getSpacedPoints() for evenly arc-length-spaced points plus curve.getTangentAt() for the exact analytical tangent at each one. Open, not closed: the raw samples run from the animation's start to a point ~90 units short of a full lap, then the clip hard-teleports back to its start rather than smoothly curving back (see loopDisplace, below) — fitting a closed spline here would fabricate a fake road segment bridging that teleport gap. This is what makes the road a genuinely smooth spline through the curve rather than a faceted polyline between the raw, unevenly-time-spaced samples.buildRibbon(points, tangents, width, material, y) — the shared ribbon-builder: offsets each point left/right by width / 2 along its tangent's perpendicular to build one open-ended BufferGeometry (positions, UVs, flat-up normals, indices) in a single draw call. UVs use u = 0/1 across the width (so markings always land in the same place relative to the edges, even through curves) and v = cumulativeDistanceAlongPath / TILE_SIZE along the length (so dash/texture spacing stays consistent regardless of how sharply the path bends). Reused for the road surface and both curbs so all three bend identically.buildRoadAndTerrain(parent, pathPoints) — called once after bike.js samples the path, with bikeContainer passed as parent (not the top-level scene — see below): builds the asphalt ribbon (ROAD_WIDTH = 10), a light-grey curb ribbon on each side (raised CURB_HEIGHT above the road), and a plain green terrain plane sized and centred on the path's bounding box (plus a padding margin) so the grass shoulder comfortably covers the arc. All of this is static, built once.
bikeContainer and not scene: the sampled points are local coordinates within bikeContainer, captured before it has accumulated any drift. bikeContainer.position gets shifted by every time the animation loops (a large shift here — see below), and parenting the road there means it inherits that same shift for free, automatically staying under the bike on every repetition of the arc. Adding it to directly was an earlier bug: it only ever covered the very first lap's stretch of world space, so the bike would drive clean off the end of the road on lap two.src/bike.jsOwns the model and animation system. Exports: bikeContainer, loadBike(), playAllAnimations(), playAnimation(), fadeBackgroundParts(), frameCameraOnBike(), resumeBikeCameraFollow().
bikeContainer (THREE.Group)The model is placed inside this group rather than directly in the scene. Every time the animation loops, the container's position is shifted forward by loopDisplace — the net world displacement of one full animation cycle — making the bike appear to travel forward indefinitely without any position reset visible in the animation.
sampleBikePath() — path samplingOn load, a temporary AnimationMixer (sampleMixer) plays the longest clip and samples followTarget.getWorldPosition() — the exact same object frameCameraOnBike() and the live camera-follow system already track — at 200 evenly-spaced times across its duration. This records the bike's real local XZ trajectory, curves included, and is the single source of truth handed to buildRoadAndTerrain() (ground.js) to build the road. loopDisplace is the first and last of these same 200 points, subtracted (previously a separate two-sample measurement; now free as a byproduct of the fuller sampling) — for the current clip this comes out to roughly 90 units, since the sampled path is an open arc, not a loop that closes on itself (see "How the Road Works"). Runs synchronously before the real mixer starts.
mixer.addEventListener('loop', ...)Fires every time the animation mixer completes one cycle. Shifts bikeContainer.position by loopDisplace, then snaps smoothedOrbitCenter and smoothedLookTarget to the new position so the camera shows zero visible jump. Also resets prevFollowPosition so the camera's speed calculation doesn't spike on the jump frame.
fadeBackgroundParts()Throttled to fire only when the bike moves more than 2.5 units from _lastFadePos. Iterates fadedMeshes (all mesh nodes collected during prepareModel). Uses setMaterialOpacity() which has an early-out guard (abs(current - target) < 0.005) to avoid triggering Three.js render-list re-sorts — this was the fix for the periodic "ping" stutter.
frameCameraOnBike()Hard-snaps camera, controls target, bgGroup, dust, and shadowCatcher to the bike's current world position. Called once right after load, before the path has been sampled and the road built.
src/camera.jsupdateCamera(delta, { camera, controls }) — called every frame during the ride.
| Phase | Duration | What happens |
|---|---|---|
| 0 | 2.8 s | Smooth zoom-in from 12 units to 3.5 units using smoothstep easing |
| 1 | 0.5 s (or until motion detected) | Hold at close distance, waiting for bike to start moving |
| 2 | 2.2 s | Zoom back out to orbit distance (7 units), transitioning FOV 28°→42° |
| 3 | ∞ | Continuous orbit |
state.liveAngle += delta * ORBIT_SPEED — accumulates at 0.19 rad/s, completing one full 360° in ~33 secondsstate.liveHeight = ORBIT_HEIGHT + sin(elapsedTime * 0.11) * 0.45 — gentle height breathing at ~0.017 Hz, so the camera slowly rises and dips every ~60 secondssmoothedOrbitCenter.lerp(followPosition, 1 - exp(-delta / 0.12)) so the camera center follows the bike with a 120 ms time constantcontrols.update() orderingcontrols.update() is called before camera.position.set(...). If it were called after, the damping would overwrite the cinematic position each frame.
src/ui.jscreateUI({ onRide, onStop }) creates two amber-styled fixed buttons.
onRide() and swaps to STOP.onStop() and swaps back to RIDE.rgba(255,170,51,0.18) on hover).opacity + pointerEvents with a 0.55 s transition.src/main.jsEntry point. Responsibilities:
create* factory functions and passes their outputs to the modules that need them.onRide / onStop callbacks to createUI, which snap camera targets, reset intro state, and toggle state.rideStarted.animate() render loop, which:
mixer.update(delta) only when rideStarted is true.displayAngle orbit at 0.38 rad/s.updateCamera(delta, ...), moves bgGroup and shadowCatcher to track the bike XZ, calls fadeBackgroundParts() when the bike moves more than 2.5 units. The road, curbs, and terrain are static meshes built once at load time (see "How the Road Works" below) — they cover the bike's whole path already, so they don't need to be moved, scrolled, or regenerated each frame.window.resize.Update MODEL_PATH in src/state.js to point at any GLB file (URL or local path) that has embedded animations. The loader picks the longest animation clip, samples its loop displacement, and everything else adapts automatically.
All camera constants live in src/state.js:
ORBIT_SPEED = 0.19 // rad/s — lower = slower orbit
ORBIT_DISTANCE = 7.0 // units — distance from bike
ORBIT_HEIGHT = 1.4 // units — base camera height
ORBIT_FOV = 42 // degrees
The bike's animation isn't a straight line — sampling its follow target across the clip shows it holds straight for ~6.6s, then sweeps through a curve that swings its heading by over 150° before straightening out on a new diagonal, ending about 90 units from where it started. At that point the clip hard-teleports back to its start (confirmed by sampling exactly at the clip's duration) rather than smoothly curving back — it's an open arc, not a loop that closes on itself. This is exactly what the pre-existing loopDisplace/bikeContainer mechanism is for: on every 'loop' event, bikeContainer.position is shifted by that ~90-unit gap so the bike looks like it keeps driving forward seamlessly, even though the local animation just teleported.
A road that re-centers a straight strip under the bike's XZ position looks fine on the straight section but makes the bike look like it's drifting once its heading diverges from the strip's fixed orientation. A first attempt at fixing that built a curved mesh from the sampled path but added it directly to the scene, treating the path as a closed loop — both wrong: the fabricated "closing" segment cut straight across the 90-unit teleport gap, and because the road never moved, the bike would sail clean off the end of it on lap two (once bikeContainer had shifted by that same 90 units). The working version:
sampleBikePath() (bike.js) scrubs a temporary mixer across the whole clip (200 samples) and records the bike's real local XZ position at each one — this is the same followTarget the live camera-follow system already tracks, not a separate/new path. Local, because bikeContainer.position is (0,0,0) at sample time.buildRoadAndTerrain() (ground.js) fits those samples to an open THREE.CatmullRomCurve3 (no fake closing segment), re-samples it into evenly arc-length-spaced points with exact analytical tangents, and builds a ribbon BufferGeometry that offsets left/right along those tangents — a real curved mesh hugging the path, not a re-oriented straight primitive. Curbs are the same ribbon shape, offset further out and raised slightly.bikeContainer, not the scene. Since the sampled points are already local to bikeContainer, and bikeContainer.position accumulates loopDisplace on every loop, parenting the road there means it inherits that exact same shift for free — it stays under the bike on every repetition of the arc, lap after lap, with no extra code needed in the loop handler.loopDisplacesceneCircleGeometry with ShadowMaterial (opacity 0.35), built in createGround and added to the scene directly (unlike the road/terrain) since it needs to keep tracking the bike's live position every frame to catch its shadow correctly.