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)
Changes
@@@ -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 anyterrain (base tiling half its extent), the mask read its clamped edge, so one layer covered the whole terrain. Themask 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@@@ -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 = lightDirthis.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) * dimthis.sun.diffuse.set(1 - night * 0.35,0.96 - dusk * 0.22 - night * 0.3,@@@ -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 = armfist.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 = armthumb.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 = armwrist.position.y = 0.015const forearm = createTaperedBox(@@@ -137,12 +137,28 @@ export class Viewmodel {},this.scene,)-- forearm.material = materialFor(this.scene, sleeve)++ forearm.material = this.armMaterial(sleeve)forearm.parent = armforearm.position.y = -0.04return 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 = nullprivate 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) returnif (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 = nullthis.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) returnthis.currentItem = itemDefthis.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.rigconst 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@@@ -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;
@@ -15,6 +15,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -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 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
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.
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
### Added
@@ -220,13 +220,16 @@ export class Environment {
@@ -220,13 +220,16 @@ export class Environment {
const azimuth = Math.cos(phase)
const azimuth = Math.cos(phase)
const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)
const dir = new Vector3(azimuth * 0.62, -Math.max(elevation, -0.6), 0.45)
dir.normalize()
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(
this.shadowCenter.set(
Math.round(cameraPos.x / 4) * 4,
Math.round(cameraPos.x / 4) * 4,
Math.round(cameraPos.y / 4) * 4,
Math.round(cameraPos.y / 4) * 4,
Math.round(cameraPos.z / 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.
// Sky dome follows the sun; turbidity rises toward dusk for warm haze.
const sunPos = dir.scale(-800)
const sunPos = dir.scale(-800)
@@ -246,7 +249,7 @@ export class Environment {
@@ -246,7 +249,7 @@ export class Environment {
const day = Math.max(0, elevation)
const day = Math.max(0, elevation)
const night = Math.max(0, -elevation)
const night = Math.max(0, -elevation)
const dim = 1 - this.weatherDim * 0.65
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(
this.sun.diffuse.set(
1 - night * 0.35,
1 - night * 0.35,
0.96 - dusk * 0.22 - night * 0.3,
0.96 - dusk * 0.22 - night * 0.3,
@@ -3,7 +3,7 @@ import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
@@ -3,7 +3,7 @@ import type { Mesh } from '@babylonjs/core/Meshes/mesh.js'
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial.js'
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js'
import { Texture } from '@babylonjs/core/Materials/Textures/texture.js'
import { Color3 } from '@babylonjs/core/Maths/math.color.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 type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh.js'
import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
import { TransformNode } from '@babylonjs/core/Meshes/transformNode.js'
import type { Scene } from '@babylonjs/core/scene.js'
import type { Scene } from '@babylonjs/core/scene.js'
@@ -89,7 +89,7 @@ export class Viewmodel {
@@ -89,7 +89,7 @@ export class Viewmodel {
},
},
this.scene,
this.scene,
)
)
- fist.material = materialFor(this.scene, skin)
+ fist.material = this.armMaterial(skin)
fist.parent = arm
fist.parent = arm
fist.position.y = 0.1
fist.position.y = 0.1
// Thumb: small block sunk INTO the mitt's inner side (reads attached).
// Thumb: small block sunk INTO the mitt's inner side (reads attached).
@@ -105,7 +105,7 @@ export class Viewmodel {
@@ -105,7 +105,7 @@ export class Viewmodel {
},
},
this.scene,
this.scene,
)
)
- thumb.material = materialFor(this.scene, skin)
+ thumb.material = this.armMaterial(skin)
thumb.parent = arm
thumb.parent = arm
thumb.position.set(-0.05 * m, 0.075, -0.01)
thumb.position.set(-0.05 * m, 0.075, -0.01)
@@ -122,7 +122,7 @@ export class Viewmodel {
@@ -122,7 +122,7 @@ export class Viewmodel {
},
},
this.scene,
this.scene,
)
)
- wrist.material = materialFor(this.scene, skin)
+ wrist.material = this.armMaterial(skin)
wrist.parent = arm
wrist.parent = arm
wrist.position.y = 0.015
wrist.position.y = 0.015
const forearm = createTaperedBox(
const forearm = createTaperedBox(
@@ -137,12 +137,28 @@ export class Viewmodel {
@@ -137,12 +137,28 @@ export class Viewmodel {
},
},
this.scene,
this.scene,
)
)
- forearm.material = materialFor(this.scene, sleeve)
+ forearm.material = this.armMaterial(sleeve)
forearm.parent = arm
forearm.parent = arm
forearm.position.y = -0.04
forearm.position.y = -0.04
return arm
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 leftFist: TransformNode | null = null
private rightArm: TransformNode | null = null
private rightArm: TransformNode | null = null
/** Alternating punch: which fist jabs next, and the live thrust node. */
/** Alternating punch: which fist jabs next, and the live thrust node. */
@@ -163,26 +179,48 @@ export class Viewmodel {
@@ -163,26 +179,48 @@ export class Viewmodel {
this.layoutHands(null)
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 {
private layoutHands(itemDef: string | null): void {
if (!this.hands || !this.rightArm || !this.leftFist) return
if (!this.hands || !this.rightArm || !this.leftFist) return
if (itemDef === null) {
if (itemDef === null) {
// Center the guard on screen (cancel the rig's +0.26 offset; local x
// 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).
// is mirrored, so PLUS moves left... i.e. toward screen center).
this.hands.position.set(0.26, 0.05, 0.08)
this.hands.position.set(0.26, 0.05, 0.08)
+ this.rightArm.rotationQuaternion = null
this.rightArm.position.set(-0.24, 0, 0)
this.rightArm.position.set(-0.24, 0, 0)
this.rightArm.rotation.set(-1.05, -0.28, 0)
this.rightArm.rotation.set(-1.05, -0.28, 0)
this.leftFist.position.set(0.24, 0, 0)
this.leftFist.position.set(0.24, 0, 0)
this.leftFist.rotation.set(-1.05, 0.28, 0)
this.leftFist.rotation.set(-1.05, 0.28, 0)
this.leftFist.setEnabled(true)
this.leftFist.setEnabled(true)
} else {
} 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)
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> {
private async loadPhysgun(): Promise<void> {
try {
try {
const result = await SceneLoader.ImportMeshAsync('', '/assets/', 'physgun.obj', this.scene)
const result = await SceneLoader.ImportMeshAsync('', '/assets/', 'physgun.obj', this.scene)
@@ -301,6 +339,11 @@ export class Viewmodel {
@@ -301,6 +339,11 @@ export class Viewmodel {
if (itemDef === this.currentItem) return
if (itemDef === this.currentItem) return
this.currentItem = itemDef
this.currentItem = itemDef
this.equipT = 0
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-
// Hands always show — empty hands are FISTS (you can punch, Minecraft-
// style). The left guard fist appears only when unarmed.
// style). The left guard fist appears only when unarmed.
if (this.hands) this.hands.setEnabled(true)
if (this.hands) this.hands.setEnabled(true)
@@ -319,12 +362,13 @@ export class Viewmodel {
@@ -319,12 +362,13 @@ export class Viewmodel {
if (this.toolProp) {
if (this.toolProp) {
this.toolProp.root.parent = this.rig
this.toolProp.root.parent = this.rig
const isTool = def?.tool !== undefined
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
// 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
// +z (muzzle) points AWAY from the camera. Without this the muzzle
// sat BETWEEN gun and camera and the beam projected wildly off-tip.
// 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.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
// Viewmodels sit in the scene's shadow side; self-illuminate them a
// touch so their shapes read instead of silhouetting to black.
// touch so their shapes read instead of silhouetting to black.
for (const child of this.toolProp.root.getChildMeshes()) {
for (const child of this.toolProp.root.getChildMeshes()) {
@@ -366,3 +410,32 @@ export class Viewmodel {
@@ -366,3 +410,32 @@ export class Viewmodel {
}
}
const VIEWMODEL_RENDER_GROUP = 1
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
@@ -307,6 +307,10 @@ body.ovg-frame-hidden #ov-footer {
@@ -307,6 +307,10 @@ body.ovg-frame-hidden #ov-footer {
font-size: 12px;
font-size: 12px;
color: #8fa0b0;
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 {
.overlay-center {
position: absolute;
position: absolute;