openvibers/openvibe.games Commits

Scraplandia in first person and at night: the right fist closes on each tool's grip with the forearm out of the lower-right corner (it stood upright in front of the tool, a flat block in the corner), the moon lights the night from above (the light pointed up out of the ground), and the status line moves under the OpenVibe button (#112)

OP OpenVibers committed 25 days ago · 07143114b9 · 4 files, +103 −14

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 8cb897e..74ed835 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -15,6 +15,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- **Terrain paint never showed.** The layered surface sampled the paint mask at the base texture's tiled UV. On any
terrain (base tiling half its extent), the mask read its clamped edge, so one layer covered the whole terrain. The
mask now spans the surface once, and each layer tiles by its own count.
++- **Nights lit from under the ground.** After sunset the sun's light kept the set sun's direction, pointing up out of
++ the ground, so the ground got only sky light and nights were brown-black. The light is now the moon's, shining down
++ from the moon, a little stronger than the old night light.
++- **The first-person hand.** The right fist stood upright in front of a held tool, a flat skin-coloured block in the
++ corner of the screen, and the gun's grip sat below the screen's edge. Now the fist closes on each tool's grip, the
++ gun's grip or low on an axe or pickaxe handle (the head leaning away), and the forearm runs out of the lower-right
++ corner. The arm is lit a little from within, like the tool, so it reads at night.
++- **The status line under the OpenVibe button.** In the world, the tick, fps and entity line sat under the folded
++ Frame's button in the same corner. It now sits below it.
### Added
ddiff --git a/apps/client/src/render/environment.ts b/apps/client/src/render/environment.ts
iindex 14ee9fd..dd404df 100644
---- a/apps/client/src/render/environment.ts
++++ b/apps/client/src/render/environment.ts
@@@ -220,13 +220,16 @@ export class Environment {
const azimuth = Math.cos(phase)
const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)
dir.normalize()
-- this.sun.direction = dir
++ // After sunset the same light is the moon's, shining down from the moon (opposite the sun, above the horizon).
++ // It used to keep the set sun's direction, pointing up out of the ground, so the ground got only sky light.
++ const lightDir = elevation >= 0 ? dir : dir.negate()
++ this.sun.direction = lightDir
this.shadowCenter.set(
Math.round(cameraPos.x / 4) * 4,
Math.round(cameraPos.y / 4) * 4,
Math.round(cameraPos.z / 4) * 4,
)
-- this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(dir.scale(200))
++ this.sun.position.copyFrom(this.shadowCenter).subtractInPlace(lightDir.scale(200))
// Sky dome follows the sun; turbidity rises toward dusk for warm haze.
const sunPos = dir.scale(-800)
@@@ -246,7 +249,7 @@ export class Environment {
const day = Math.max(0, elevation)
const night = Math.max(0, -elevation)
const dim = 1 - this.weatherDim * 0.65
-- this.sun.intensity = (day * 1.25 + night * 0.12 + 0.03) * dim
++ this.sun.intensity = (day * 1.25 + night * 0.35 + 0.03) * dim
this.sun.diffuse.set(
1 - night * 0.35,
0.96 - dusk * 0.22 - night * 0.3,
ddiff --git a/apps/client/src/render/viewmodel.ts b/apps/client/src/render/viewmodel.ts
iindex aa362e7..f61524a 100644
---- a/apps/client/src/render/viewmodel.ts
++++ b/apps/client/src/render/viewmodel.ts
@@@ -3,7 +3,7 @@ import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js'
import { Color3 } from '@babylonjs/core/Maths/math.color.js'
--import { Vector3 } from '@babylonjs/core/Maths/math.vector.js'
++import { Matrix, Quaternion, Vector3 } from '@babylonjs/core/Maths/math.vector.js'
import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'
import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
import type { Scene } from '@babylonjs/core/scene.js'
@@@ -89,7 +89,7 @@ export class Viewmodel {
},
this.scene,
)
-- fist.material = materialFor(this.scene, skin)
++ fist.material = this.armMaterial(skin)
fist.parent = arm
fist.position.y = 0.1
// Thumb: small block sunk INTO the mitt's inner side (reads attached).
@@@ -105,7 +105,7 @@ export class Viewmodel {
},
this.scene,
)
-- thumb.material = materialFor(this.scene, skin)
++ thumb.material = this.armMaterial(skin)
thumb.parent = arm
thumb.position.set(-0.05 * m, 0.075, -0.01)
@@@ -122,7 +122,7 @@ export class Viewmodel {
},
this.scene,
)
-- wrist.material = materialFor(this.scene, skin)
++ wrist.material = this.armMaterial(skin)
wrist.parent = arm
wrist.position.y = 0.015
const forearm = createTaperedBox(
@@@ -137,12 +137,28 @@ export class Viewmodel {
},
this.scene,
)
-- forearm.material = materialFor(this.scene, sleeve)
++ forearm.material = this.armMaterial(sleeve)
forearm.parent = arm
forearm.position.y = -0.04
return arm
}
++ private readonly armMaterials = new Map<string, StandardMaterial>()
++ /**
++ * The arm's copy of a world colour, lit a little from within like the tool
++ * props, so it reads in shade and at night instead of going brown-black.
++ */
++ private armMaterial(hex: string): StandardMaterial {
++ let mat = this.armMaterials.get(hex)
++ if (!mat) {
++ const base = materialFor(this.scene, hex)
++ mat = base.clone(`${base.name}:vm`)
++ mat.emissiveColor = base.diffuseColor.scale(0.3)
++ this.armMaterials.set(hex, mat)
++ }
++ return mat
++ }
++
private leftFist: TransformNode | null = null
private rightArm: TransformNode | null = null
/** Alternating punch: which fist jabs next, and the live thrust node. */
@@@ -163,26 +179,48 @@ export class Viewmodel {
this.layoutHands(null)
}
-- /** Armed: one support hand under the tool. Unarmed: boxer guard. */
++ /** Armed: the right hand grips the tool (gripTool). Unarmed: boxer guard. */
private layoutHands(itemDef: string | null): void {
if (!this.hands || !this.rightArm || !this.leftFist) return
if (itemDef === null) {
// Center the guard on screen (cancel the rig's +0.26 offset; local x
// is mirrored, so PLUS moves left... i.e. toward screen center).
this.hands.position.set(0.26, 0.05, 0.08)
++ this.rightArm.rotationQuaternion = null
this.rightArm.position.set(-0.24, 0, 0)
this.rightArm.rotation.set(-1.05, -0.28, 0)
this.leftFist.position.set(0.24, 0, 0)
this.leftFist.rotation.set(-1.05, 0.28, 0)
this.leftFist.setEnabled(true)
} else {
-- this.hands.position.set(0.02, -0.06, -0.04)
-- this.rightArm.position.set(0, 0, 0)
-- this.rightArm.rotation.set(0, 0, 0)
this.leftFist.setEnabled(false)
}
}
++ /**
++ * Holds the tool by its grip at HAND_SPOT, the right fist closed around it
++ * and the forearm running back out of the lower-right corner of the screen.
++ * The fist used to stand upright in front of the tool, a flat skin-coloured
++ * block covering the corner, and a gun's grip sat below the screen's edge.
++ */
++ private gripTool(root: TransformNode, grip: Grip): void {
++ if (!this.hands || !this.rightArm) return
++ const turn = Matrix.Compose(
++ root.scaling,
++ Quaternion.FromEulerVector(root.rotation),
++ Vector3.Zero(),
++ )
++ root.position
++ .copyFrom(HAND_SPOT)
++ .subtractInPlace(Vector3.TransformCoordinates(new Vector3(...grip.at), turn))
++ const q = Quaternion.FromUnitVectorsToRef(ARM_DOWN, FOREARM, new Quaternion())
++ this.hands.position.setAll(0)
++ this.rightArm.rotationQuaternion = q
++ // The fist's centre (arm-local y = FIST_CENTRE) sits on the grip.
++ const centre = new Vector3(0, FIST_CENTRE, 0).rotateByQuaternionToRef(q, new Vector3())
++ this.rightArm.position.copyFrom(HAND_SPOT).subtractInPlace(centre)
++ }
++
private async loadPhysgun(): Promise<void> {
try {
const result = await SceneLoader.ImportMeshAsync('', '/assets/', 'physgun.obj', this.scene)
@@@ -301,6 +339,11 @@ export class Viewmodel {
if (itemDef === this.currentItem) return
this.currentItem = itemDef
this.equipT = 0
++ // A jab still in flight would reset the arm's pose on top of the grip.
++ if (this.punchingFist) {
++ this.punchingFist.position.set(this.punchingFist === this.rightArm ? -0.24 : 0.24, 0, 0)
++ this.punchingFist = null
++ }
// Hands always show — empty hands are FISTS (you can punch, Minecraft-
// style). The left guard fist appears only when unarmed.
if (this.hands) this.hands.setEnabled(true)
@@@ -319,12 +362,13 @@ export class Viewmodel {
if (this.toolProp) {
this.toolProp.root.parent = this.rig
const isTool = def?.tool !== undefined
-- this.toolProp.root.position.set(0, isTool ? -0.06 : 0.01, isTool ? -0.08 : -0.11)
++ const grip = GRIPS[isTool ? toolGripKind(def?.tool?.kind) : 'item']
// The rig is yawed PI (see update()); cancel it on the prop so its
// +z (muzzle) points AWAY from the camera. Without this the muzzle
// sat BETWEEN gun and camera and the beam projected wildly off-tip.
-- this.toolProp.root.rotation.y = Math.PI
++ this.toolProp.root.rotation.set(grip.tilt, Math.PI, 0)
this.toolProp.root.scaling.scaleInPlace(isTool ? 1.05 : 1)
++ this.gripTool(this.toolProp.root, grip)
// Viewmodels sit in the scene's shadow side; self-illuminate them a
// touch so their shapes read instead of silhouetting to black.
for (const child of this.toolProp.root.getChildMeshes()) {
@@@ -366,3 +410,32 @@ export class Viewmodel {
}
const VIEWMODEL_RENDER_GROUP = 1
++
++/** Where the right fist closes on a held thing, in its own frame (+z muzzle or head side, +y up), and how far it tips. */
++interface Grip {
++ at: [number, number, number]
++ /** Rotation about the tool's x axis, radians: an axe leans its head away from the camera. */
++ tilt: number
++}
++type GripKind = 'physgun' | 'axe' | 'pickaxe' | 'generic' | 'item'
++const GRIPS: Record<GripKind, Grip> = {
++ physgun: { at: [0, -0.075, -0.012], tilt: 0 },
++ axe: { at: [0, -0.15, 0], tilt: 0.6 },
++ pickaxe: { at: [0, -0.15, 0], tilt: 0.6 },
++ generic: { at: [0, -0.07, 0.04], tilt: 0 },
++ item: { at: [0, -0.07, 0], tilt: 0 },
++}
++function toolGripKind(kind: string | undefined): GripKind {
++ return kind === 'physgun' || kind === 'axe' || kind === 'pickaxe' ? kind : 'generic'
++}
++/** Where the right fist holds things, in the rig's frame: low in the lower-right of the view, where it shows. */
++const HAND_SPOT = new Vector3(0, -0.05, -0.1)
++/**
++ * The forearm runs from the fist back toward the camera, down and out to the right, whatever the tool's tilt. Rig
++ * frame: +z is toward the camera and +x is screen-left.
++ */
++const FOREARM = new Vector3(-0.32, -0.5, 0.8).normalize()
++/** An arm is built fist up, the sleeve hanging below it. */
++const ARM_DOWN = new Vector3(0, -1, 0)
++/** Arm-local height of the fist's centre (buildArm: the fist spans y 0 to 0.1). */
++const FIST_CENTRE = 0.05
ddiff --git a/apps/client/src/ui/styles.css b/apps/client/src/ui/styles.css
iindex ff39a4b..2735377 100644
---- a/apps/client/src/ui/styles.css
++++ b/apps/client/src/ui/styles.css
@@@ -307,6 +307,10 @@ body.ovg-frame-hidden #ov-footer {
font-size: 12px;
color: #8fa0b0;
}
++/* In the world the folded Frame's OpenVibe pill sits in that corner: the status line goes under it. */
++body:has(#ovg-frame-toggle:not([hidden])) .status {
++ top: 42px;
++}
.overlay-center {
position: absolute;