Add WebXR rendering mode toggle with 4 modes

Implemented a single button in the toolbox that cycles through four rendering modes:
1. Lightmap + Lighting - diffuseColor + lightmapTexture with lighting enabled
2. Emissive Texture - emissiveColor + emissiveTexture with lighting disabled (default)
3. Flat Color - emissiveColor only with lighting disabled
4. Diffuse + Lights - diffuseColor with two dynamic scene lights enabled

Features:
- Single clickable button displays current mode and cycles to next on click
- Automatically manages two scene lights (HemisphericLight + PointLight) for Diffuse + Lights mode
- UI materials (buttons, handles, labels) are excluded from mode changes to remain readable
- Button positioned below color grid with user-adjusted scaling
- Added comprehensive naming conventions documentation
- Updated inspector hotkey to Ctrl+Shift+I

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Michael Mainguy 2025-11-13 10:36:03 -06:00
parent c7887d7d8f
commit bda0735c7f
10 changed files with 694 additions and 15 deletions

View File

@ -108,3 +108,4 @@ Databases can be optionally encrypted. The `Encryption` class handles AES encryp
- `VITE_SYNCDB_ENDPOINT`: Remote database sync endpoint - `VITE_SYNCDB_ENDPOINT`: Remote database sync endpoint
Check `.env.local` for local configuration. Check `.env.local` for local configuration.
- document the toolId and material naming conventions.

138
docs/NAMING_CONVENTIONS.md Normal file
View File

@ -0,0 +1,138 @@
# Naming Conventions
## Tool and Material Naming
This document describes the naming conventions used for tools, materials, and related entities in the immersive WebXR application.
## Material Naming
Materials follow a consistent naming pattern based on their color:
**Format:** `material-{color}`
**Where:**
- `{color}` is the hex string representation of the material's color (e.g., `#ff0000` for red)
**Examples:**
- `material-#ff0000` - Red material
- `material-#00ff00` - Green material
- `material-#222222` - Dark gray material
**Implementation:**
```typescript
const material = new StandardMaterial("material-" + color.toHexString(), scene);
```
**Location:** Materials are created in:
- `src/toolbox/functions/buildColor.ts` - For toolbox color swatches
- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Fallback material creation via `buildMissingMaterial()`
## Tool Mesh Naming
Tool meshes use a compound naming pattern that includes both the tool type and color:
**Format:** `tool-{toolId}`
**Where:**
- `{toolId}` = `{toolType}-{color}`
- `{toolType}` is a value from the `ToolType` enum (e.g., `BOX`, `SPHERE`, `CYLINDER`, `CONE`, `PLANE`, `PERSON`)
- `{color}` is the hex string representation of the tool's color
**Examples:**
- `tool-BOX-#ff0000` - Red box tool
- `tool-SPHERE-#00ff00` - Green sphere tool
- `tool-CYLINDER-#0000ff` - Blue cylinder tool
- `tool-PLANE-#ffff00` - Yellow plane tool
**Implementation:**
```typescript
function toolId(tool: ToolType, color: Color3) {
return tool + "-" + color.toHexString();
}
const newItem = await buildMesh(tool, `tool-${id}`, colorParent.getScene());
// For example: `tool-BOX-#ff0000`
```
**Location:** Tool meshes are created in `src/toolbox/functions/buildTool.ts`
## Tool Colors
The application uses 16 predefined colors for the toolbox:
```typescript
const colors: string[] = [
"#222222", "#8b4513", "#006400", "#778899", // Row 1: Dark gray, Brown, Dark green, Light slate gray
"#4b0082", "#ff0000", "#ffa500", "#ffff00", // Row 2: Indigo, Red, Orange, Yellow
"#00ff00", "#00ffff", "#0000ff", "#ff00ff", // Row 3: Green, Cyan, Blue, Magenta
"#1e90ff", "#98fb98", "#ffe4b5", "#ff69b4" // Row 4: Dodger blue, Pale green, Moccasin, Hot pink
]
```
## Tool Types
Available tool types from the `ToolType` enum:
- `BOX` - Cube mesh
- `SPHERE` - Sphere mesh
- `CYLINDER` - Cylinder mesh
- `CONE` - Cone mesh
- `PLANE` - Flat plane mesh
- `PERSON` - Person/avatar mesh
## Material Color Access
When accessing material colors, use this priority order to handle both current and legacy materials:
```typescript
// For StandardMaterial
const stdMat = material as StandardMaterial;
const materialColor = stdMat.emissiveColor || stdMat.diffuseColor;
// Current rendering uses emissiveColor
// Legacy materials may have diffuseColor instead
```
## Rendering Modes
Materials can be rendered in three different modes, affecting how color properties are used:
### 1. Lightmap with Lighting
- Uses `diffuseColor` + `lightmapTexture`
- `disableLighting = false`
- Most expensive performance-wise
- Provides lighting illusion with actual lighting calculations
### 2. Unlit with Emissive Texture (Default)
- Uses `emissiveColor` + `emissiveTexture` (lightmap)
- `disableLighting = true`
- Best balance of visual quality and performance
- Provides lighting illusion without lighting calculations
### 3. Flat Emissive
- Uses only `emissiveColor`
- `disableLighting = true`
- Best performance
- No lighting illusion, flat shading
## Instance Naming
Instanced meshes (created from tool templates) follow this pattern:
**Format:** `tool-instance-{toolId}`
**Example:**
```typescript
const instance = new InstancedMesh("tool-instance-" + id, newItem);
// For example: `tool-instance-BOX-#ff0000`
```
These instances share materials with their source mesh and are used for visual feedback before creating actual diagram entities.
## Related Files
- `src/toolbox/functions/buildTool.ts` - Tool mesh creation and naming
- `src/toolbox/functions/buildColor.ts` - Material creation and color management
- `src/diagram/functions/buildMeshFromDiagramEntity.ts` - Diagram entity instantiation
- `src/toolbox/types/toolType.ts` - ToolType enum definition
- `src/util/lightmapGenerator.ts` - Lightmap texture generation and caching
- `src/util/renderingMode.ts` - Rendering mode enum and labels

19
server/etc/cert.pem Normal file
View File

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDHTCCAgWgAwIBAgIJF3WqWLMk6JOlMA0GCSqGSIb3DQEBCwUAMCwxKjAoBgNV
BAMTIWRldi1nMGx0MThuZGJjcDZlYXJyLnVzLmF1dGgwLmNvbTAeFw0yNDA4MTUx
NTE1NDdaFw0zODA0MjQxNTE1NDdaMCwxKjAoBgNVBAMTIWRldi1nMGx0MThuZGJj
cDZlYXJyLnVzLmF1dGgwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
ggEBAKfmuyqb2N8Tf5W9KTAy2D6FTkYICX+mcclyZS+0Mi7fLzSB7IeSuWXmuHoR
h5FHJ/Qp6eC1ahYs9WmAjFp81HPzZ/9hEbK3XrLMSta7zVldPTQjnt5sU/Zxr/M2
xMjHH2P3G231si+G20czvDWoItnyWs8rcE2wEcyiXM+/Ixgxoh8kfc9pqpNLXTvM
IvqAuxXbPeju3XccQ6B0lshN72EwV9yW73B0s7DuHsbBA0WHKYmcvdXgnQ1dU2/L
8BR5s/gJJE0MUh2qhsnKE3yUC/hTW7A0Qn0SMEZey04hvJWePnn59kv52DPVXZpZ
ql6ISehwn3hZdhHjpsoHbE48CN0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAd
BgNVHQ4EFgQUbjF1ri0QhHovlQ2D5gPtDucLGhowDgYDVR0PAQH/BAQDAgKEMA0G
CSqGSIb3DQEBCwUAA4IBAQAxIRNANDbVrkXF6x/KHYgj5prb+4yHtmYb7tiRi51z
MNHNLkltNou3dWsS4tU/YgzHTof3SJe2CIg9xAgk0XTHZjxRtbwIY6Zc9Sgf/KKL
OxFIiNcIQIGDoKHWmv2w4qSrYBkH9hva4kCysjgIFNc+0il7DQR2ifwLOxQGl/AE
hSfexgUKjfrno12gBlNCNcP+Xyn9/G++eg9vV+RuGLLIyLX0d0Vl7/C1pGoDrNpO
m/3oxR4IRnhEfGBD+LdWvmmIuxzXM1hSbLYJbMotHqKZSh0XlEM6Mi12gMZi7sEC
lhbXs+4ecvTBFfGCWFyUISFoSwRRnpQnEM5DsZT/t/Z8
-----END CERTIFICATE-----

126
server/etc/local.ini Normal file
View File

@ -0,0 +1,126 @@
; CouchDB Configuration Settings
; Custom settings should be made in this file. They will override settings
; in default.ini, but unlike changes made to default.ini, this file won't be
; overwritten on server upgrade.
[couchdb]
database_dir = /var/snap/couchdb/common/data
view_index_dir = /var/snap/couchdb/common/data
;max_document_size = 4294967296 ; bytes
;os_process_timeout = 5000
uuid = dd27b78cfb458b894e0277173f176878
[couch_peruser]
; If enabled, couch_peruser ensures that a private per-user database
; exists for each document in _users. These databases are writable only
; by the corresponding user. Databases are in the following form:
; userdb-{hex encoded username}
enable = true
; If set to true and a user is deleted, the respective database gets
; deleted as well.
delete_dbs = true
; Set a default q value for peruser-created databases that is different from
; cluster / q
;q = 1
[log]
level = debug
[chttpd]
;port = 5984
bind_address = 127.0.0.1
authentication_handlers = {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
;authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {chttpd_auth, default_authentication_handler}
; Options for the MochiWeb HTTP server.
;server_options = [{backlog, 128}, {acceptor_pool_size, 16}]
; For more socket options, consult Erlang's module 'inet' man page.
;socket_options = [{sndbuf, 262144}, {nodelay, true}]
enable_cors = true
[httpd]
; NOTE that this only configures the "backend" node-local port, not the
; "frontend" clustered port. You probably don't want to change anything in
; this section.
; Uncomment next line to trigger basic-auth popup on unauthorized requests.
;WWW-Authenticate = Basic realm="administrator"
; Uncomment next line to set the configuration modification whitelist. Only
; whitelisted values may be changed via the /_config URLs. To allow the admin
; to change this value over HTTP, remember to include {httpd,config_whitelist}
; itself. Excluding it from the list would require editing this file to update
; the whitelist.
;config_whitelist = [{httpd,config_whitelist}, {log,level}, {etc,etc}]
[ssl]
;enable = true
;cert_file = /full/path/to/server_cert.pem
;key_file = /full/path/to/server_key.pem
;password = somepassword
; set to true to validate peer certificates
;verify_ssl_certificates = false
; Set to true to fail if the client does not send a certificate. Only used if verify_ssl_certificates is true.
;fail_if_no_peer_cert = false
; Path to file containing PEM encoded CA certificates (trusted
; certificates used for verifying a peer certificate). May be omitted if
; you do not want to verify the peer.
;cacert_file = /full/path/to/cacertf
; The verification fun (optional) if not specified, the default
; verification fun will be used.
;verify_fun = {Module, VerifyFun}
; maximum peer certificate depth
;ssl_certificate_max_depth = 1
; Reject renegotiations that do not live up to RFC 5746.
;secure_renegotiate = true
; The cipher suites that should be supported.
; Can be specified in erlang format "{ecdhe_ecdsa,aes_128_cbc,sha256}"
; or in OpenSSL format "ECDHE-ECDSA-AES128-SHA256".
;ciphers = ["ECDHE-ECDSA-AES128-SHA256", "ECDHE-ECDSA-AES128-SHA"]
; The SSL/TLS versions to support
;tls_versions = [tlsv1, 'tlsv1.1', 'tlsv1.2']
; To enable Virtual Hosts in CouchDB, add a vhost = path directive. All requests to
; the Virtual Host will be redirected to the path. In the example below all requests
; to http://example.com/ are redirected to /database.
; If you run CouchDB on a specific port, include the port number in the vhost:
; example.com:5984 = /database
[vhosts]
;example.com = /database/
; To create an admin account uncomment the '[admins]' section below and add a
; line in the format 'username = password'. When you next start CouchDB, it
; will change the password to a hash (so that your passwords don't linger
; around in plain-text files). You can add more admin accounts with more
; 'username = password' lines. Don't forget to restart CouchDB after
; changing this.
[admins]
admin = -pbkdf2-eeee185ee6142700c0e5a9e31b1d6d85ba952a49,f073f989f3201b55d953825d56acad2a,10
[chttpd_auth]
secret = a6bc1f1fd52803b4feae8f30b3944300
;authentication_handlers = {chttpd_auth, jwt_authentication_handler}
[jwt_auth]
roles_claim_path = metadata.databases
required_claims = exp,iat
;validate_claim_iss = https://dev-g0lt18ndbcp6earr.us.auth0.com/
;validate_claim_aud = sxAJub9Uo2mOE7iYCTOuQGhppGLEPWzb
[jwt_keys]
rsa:_default = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n
rsa:1R0ZY6dzJ7ttWk60bT0_V = -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY\nPoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU\nc/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi\n2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v\nYTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL\n+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI\n3QIDAQAB\n-----END PUBLIC KEY-----\n
[cors]
origins = https://www.cybersecshield.com,https://cybersecshield.com,http://localhost:5173
headers = accept, authorization, content-type, origin, referer
credentials = true
methods = GET, PUT, POST, HEAD, DELETE

9
server/etc/pubkey.pem Normal file
View File

@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+a7KpvY3xN/lb0pMDLY
PoVORggJf6ZxyXJlL7QyLt8vNIHsh5K5Zea4ehGHkUcn9Cnp4LVqFiz1aYCMWnzU
c/Nn/2ERsrdessxK1rvNWV09NCOe3mxT9nGv8zbEyMcfY/cbbfWyL4bbRzO8Nagi
2fJazytwTbARzKJcz78jGDGiHyR9z2mqk0tdO8wi+oC7Fds96O7ddxxDoHSWyE3v
YTBX3JbvcHSzsO4exsEDRYcpiZy91eCdDV1Tb8vwFHmz+AkkTQxSHaqGycoTfJQL
+FNbsDRCfRIwRl7LTiG8lZ4+efn2S/nYM9VdmlmqXohJ6HCfeFl2EeOmygdsTjwI
3QIDAQAB
-----END PUBLIC KEY-----

101
server/etc/vm.args Normal file
View File

@ -0,0 +1,101 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
# Each node in the system must have a unique name. These are specified through
# the Erlang -name flag, which takes the form:
#
# -name nodename@<FQDN>
#
# or
#
# -name nodename@<IP-ADDRESS>
#
# CouchDB recommends the following values for this flag:
#
# 1. If this is a single node, not in a cluster, use:
# -name couchdb@127.0.0.1
#
# 2. If DNS is configured for this host, use the FQDN, such as:
# -name couchdb@my.host.domain.com
#
# 3. If DNS isn't configured for this host, use IP addresses only, such as:
# -name couchdb@192.168.0.1
#
# Do not rely on tricks with /etc/hosts or libresolv to handle anything
# other than the above 3 approaches correctly. They will not work reliably.
#
# Multiple CouchDBs running on the same machine can use couchdb1@, couchdb2@,
# etc.
-name couchdb@127.0.0.1
# All nodes must share the same magic cookie for distributed Erlang to work.
# Uncomment the following line and append a securely generated random value.
-setcookie eh.RauybPRHzP4-pXv
# Which interfaces should the node listen on?
-kernel inet_dist_use_interface {127,0,0,1}
# Tell kernel and SASL not to log anything
-kernel error_logger silent
-sasl sasl_error_logger false
# This will toggle to true in Erlang 25+. However since we don't use global
# any longer, and have our own auto-connection module, we can keep the
# existing global behavior to avoid surprises. See
# https://github.com/erlang/otp/issues/6470#issuecomment-1337421210 for more
# information about possible increased coordination and messages being sent on
# disconnections when this setting is enabled.
#
-kernel prevent_overlapping_partitions false
# Increase the pool of dirty IO schedulers from 10 to 16
# Dirty IO schedulers are used for file IO.
+SDio 16
# Comment this line out to enable the interactive Erlang shell on startup
+Bd -noinput
# Set maximum SSL session lifetime to reap terminated replication readers
-ssl session_lifetime 300
## TLS Distribution
## Use TLS for connections between Erlang cluster members.
## http://erlang.org/doc/apps/ssl/ssl_distribution.html
##
## Generate Cert(PEM) File
## This is just an example command to generate a certfile (PEM).
## This is not an endorsement of specific expiration limits, key sizes, or algorithms.
## $ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem
## $ cat key.pem cert.pem > dev/erlserver.pem && rm key.pem cert.pem
##
## Generate a Config File (couch_ssl_dist.conf)
## [{server,
## [{certfile, "</path/to/erlserver.pem>"},
## {secure_renegotiate, true}]},
## {client,
## [{secure_renegotiate, true}]}].
##
## CouchDB recommends the following values for no_tls flag:
## 1. Use TCP only, set to true, such as:
## -couch_dist no_tls true
## 2. Use TLS only, set to false, such as:
## -couch_dist no_tls false
## 3. Specify which node to use TCP, such as:
## -couch_dist no_tls \"*@127.0.0.1\"
##
## To ensure search works, make sure to set 'no_tls' option for the clouseau node.
## By default that would be "clouseau@127.0.0.1".
## Don't forget to override the paths to point to your certificate(s) and key(s)!
##
#-proto_dist couch
#-couch_dist no_tls '"clouseau@127.0.0.1"'
#-ssl_dist_optfile <path/to/couch_ssl_dist.conf>

View File

@ -5,6 +5,7 @@ import {Handle} from "../objects/handle";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {Button} from "../objects/Button"; import {Button} from "../objects/Button";
import {LightmapGenerator} from "../util/lightmapGenerator"; import {LightmapGenerator} from "../util/lightmapGenerator";
import {RenderingMode, RenderingModeLabels} from "../util/renderingMode";
const colors: string[] = [ const colors: string[] = [
"#222222", "#8b4513", "#006400", "#778899", "#222222", "#8b4513", "#006400", "#778899",
@ -21,6 +22,7 @@ export class Toolbox {
private readonly _handle: Handle; private readonly _handle: Handle;
private readonly _scene: Scene; private readonly _scene: Scene;
private _xr?: WebXRDefaultExperience; private _xr?: WebXRDefaultExperience;
private _renderModeDisplay?: Button;
constructor(readyObservable: Observable<boolean>) { constructor(readyObservable: Observable<boolean>) {
this._scene = DefaultScene.Scene; this._scene = DefaultScene.Scene;
@ -149,24 +151,126 @@ export class Toolbox {
this._xr.baseExperience.onStateChangedObservable.add((state) => { this._xr.baseExperience.onStateChangedObservable.add((state) => {
if (state == 2) { // WebXRState.IN_XR if (state == 2) { // WebXRState.IN_XR
const button = Button.CreateButton("exitXr", "exitXr", this._scene, {}); // Create exit XR button
const exitButton = Button.CreateButton("exitXr", "exitXr", this._scene, {});
// Position button at bottom-right of toolbox, matching handle size and orientation // Position button at bottom-right of toolbox, matching handle size and orientation
button.transform.position.x = 0.5; // Right side exitButton.transform.position.x = 0.5; // Right side
button.transform.position.y = -0.35; // Below color grid exitButton.transform.position.y = -0.35; // Below color grid
button.transform.position.z = 0; // Coplanar with toolbox exitButton.transform.position.z = 0; // Coplanar with toolbox
button.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly exitButton.transform.rotation.y = Math.PI; // Flip 180° on local x-axis to face correctly
button.transform.scaling = new Vector3(.2, .2, .2); // Match handle height exitButton.transform.scaling = new Vector3(.2, .2, .2); // Match handle height
button.transform.parent = this._toolboxBaseNode; exitButton.transform.parent = this._toolboxBaseNode;
button.onPointerObservable.add((evt) => { exitButton.onPointerObservable.add((evt) => {
this._logger.debug(evt); this._logger.debug(evt);
if (evt.sourceEvent.type == 'pointerdown') { if (evt.sourceEvent.type == 'pointerdown') {
this._xr.baseExperience.exitXRAsync(); this._xr.baseExperience.exitXRAsync();
} }
}); });
// Create rendering mode button that cycles through modes
this.createRenderModeButton();
}
});
}
private createRenderModeButton() {
const modes = [
RenderingMode.LIGHTMAP_WITH_LIGHTING,
RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE,
RenderingMode.FLAT_EMISSIVE,
RenderingMode.DIFFUSE_WITH_LIGHTS
];
const currentMode = LightmapGenerator.getRenderingMode();
this._renderModeDisplay = Button.CreateButton(
`Mode: ${RenderingModeLabels[currentMode]}`,
`renderModeButton`,
this._scene,
{
width: 0.5,
height: 0.2,
background: Color3.FromHexString("#333333"),
color: Color3.White(),
fontSize: 240
}
);
// Position below the color grid
this._renderModeDisplay.transform.position.x = 0;
this._renderModeDisplay.transform.position.y = -.2;
this._renderModeDisplay.transform.position.z = 0;
this._renderModeDisplay.transform.rotation.y = Math.PI;
this._renderModeDisplay.transform.scaling = new Vector3(.4, .4, .4);
this._renderModeDisplay.transform.parent = this._toolboxBaseNode;
// Add click handler to cycle through modes
this._renderModeDisplay.onPointerObservable.add((evt) => {
if (evt.sourceEvent.type == 'pointerdown') {
const currentMode = LightmapGenerator.getRenderingMode();
const currentIndex = modes.indexOf(currentMode);
const nextIndex = (currentIndex + 1) % modes.length;
const nextMode = modes[nextIndex];
this._logger.info(`Cycling to rendering mode: ${nextMode}`);
LightmapGenerator.updateAllMaterials(this._scene, nextMode);
// Update button text
this.updateRenderModeButton(nextMode);
}
});
}
private updateRenderModeButton(mode: RenderingMode) {
if (this._renderModeDisplay) {
// Dispose old button and create new one with updated text
this._renderModeDisplay.dispose();
this._renderModeDisplay = Button.CreateButton(
`Mode: ${RenderingModeLabels[mode]}`,
`renderModeButton`,
this._scene,
{
width: 0.5,
height: 0.2,
background: Color3.FromHexString("#333333"),
color: Color3.White(),
fontSize: 240
}
);
this._renderModeDisplay.transform.position.x = 0;
this._renderModeDisplay.transform.position.y = -.2;
this._renderModeDisplay.transform.position.z = 0;
this._renderModeDisplay.transform.rotation.y = Math.PI;
this._renderModeDisplay.transform.scaling = new Vector3(.15, .15, .15);
this._renderModeDisplay.transform.parent = this._toolboxBaseNode;
// Re-attach the click handler
this._renderModeDisplay.onPointerObservable.add((evt) => {
if (evt.sourceEvent.type == 'pointerdown') {
const modes = [
RenderingMode.LIGHTMAP_WITH_LIGHTING,
RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE,
RenderingMode.FLAT_EMISSIVE,
RenderingMode.DIFFUSE_WITH_LIGHTS
];
const currentMode = LightmapGenerator.getRenderingMode();
const currentIndex = modes.indexOf(currentMode);
const nextIndex = (currentIndex + 1) % modes.length;
const nextMode = modes[nextIndex];
this._logger.info(`Cycling to rendering mode: ${nextMode}`);
LightmapGenerator.updateAllMaterials(this._scene, nextMode);
// Update button text
this.updateRenderModeButton(nextMode);
} }
}); });
} }
} }
}

View File

@ -2,17 +2,13 @@ import {DefaultScene} from "../../defaultScene";
export function addSceneInspector() { export function addSceneInspector() {
window.addEventListener("keydown", (ev) => { window.addEventListener("keydown", (ev) => {
if (ev.shiftKey && ev.ctrlKey && ev.altKey && ev.keyCode === 73) { // Ctrl+Shift+I to open inspector
const web = document.querySelector('#webApp'); if (ev.shiftKey && ev.ctrlKey && !ev.altKey && ev.keyCode === 73) {
(web as HTMLDivElement).style.display = 'none';
import ("@babylonjs/inspector").then((inspector) => { import ("@babylonjs/inspector").then((inspector) => {
inspector.Inspector.Show(DefaultScene.Scene, { inspector.Inspector.Show(DefaultScene.Scene, {
overlay: true, overlay: true,
showExplorer: true showExplorer: true
}); });
const web = document.querySelector('#webApp');
(web as HTMLDivElement).style.display = 'none';
}); });
/*import("@babylonjs/core/Debug").then(() => { /*import("@babylonjs/core/Debug").then(() => {
import("@babylonjs/inspector").then(() => { import("@babylonjs/inspector").then(() => {

View File

@ -1,5 +1,6 @@
import {Color3, DynamicTexture, Scene} from "@babylonjs/core"; import {Color3, DynamicTexture, HemisphericLight, PointLight, Scene, StandardMaterial, Vector3} from "@babylonjs/core";
import {DefaultScene} from "../defaultScene"; import {DefaultScene} from "../defaultScene";
import {RenderingMode} from "./renderingMode";
export class LightmapGenerator { export class LightmapGenerator {
private static lightmapCache: Map<string, DynamicTexture> = new Map(); private static lightmapCache: Map<string, DynamicTexture> = new Map();
@ -8,6 +9,13 @@ export class LightmapGenerator {
// Toggle to enable/disable lightmap usage (for performance testing) // Toggle to enable/disable lightmap usage (for performance testing)
public static ENABLED = true; public static ENABLED = true;
// Current rendering mode
private static currentMode: RenderingMode = RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE;
// Scene lights for DIFFUSE_WITH_LIGHTS mode
private static hemisphericLight?: HemisphericLight;
private static pointLight?: PointLight;
/** /**
* Generates or retrieves cached lightmap for a given color * Generates or retrieves cached lightmap for a given color
* @param color The base color for the lightmap * @param color The base color for the lightmap
@ -125,4 +133,145 @@ export class LightmapGenerator {
public static getCacheSize(): number { public static getCacheSize(): number {
return this.lightmapCache.size; return this.lightmapCache.size;
} }
/**
* Sets the rendering mode
* @param mode The rendering mode to use
*/
public static setRenderingMode(mode: RenderingMode): void {
this.currentMode = mode;
}
/**
* Gets the current rendering mode
* @returns Current rendering mode
*/
public static getRenderingMode(): RenderingMode {
return this.currentMode;
}
/**
* Applies the specified rendering mode to a material
* @param material The material to update
* @param color The base color
* @param mode The rendering mode to apply
* @param scene The BabylonJS scene
*/
public static applyRenderingModeToMaterial(
material: StandardMaterial,
color: Color3,
mode: RenderingMode,
scene: Scene
): void {
// Clear existing textures and properties
material.diffuseColor = new Color3(0, 0, 0);
material.emissiveColor = new Color3(0, 0, 0);
material.diffuseTexture = null;
material.emissiveTexture = null;
material.lightmapTexture = null;
switch (mode) {
case RenderingMode.LIGHTMAP_WITH_LIGHTING:
// Use diffuseColor + lightmapTexture with lighting enabled
material.diffuseColor = color;
material.lightmapTexture = this.generateLightmapForColor(color, scene);
material.useLightmapAsShadowmap = false;
material.disableLighting = false;
break;
case RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE:
// Use emissiveColor + emissiveTexture with lighting disabled
material.emissiveColor = color;
material.emissiveTexture = this.generateLightmapForColor(color, scene);
material.disableLighting = true;
break;
case RenderingMode.FLAT_EMISSIVE:
// Use only emissiveColor with lighting disabled
material.emissiveColor = color;
material.disableLighting = true;
break;
case RenderingMode.DIFFUSE_WITH_LIGHTS:
// Use diffuseColor with dynamic lighting enabled
material.diffuseColor = color;
material.disableLighting = false;
break;
}
}
/**
* Creates or enables scene lights for DIFFUSE_WITH_LIGHTS mode
* @param scene The BabylonJS scene
*/
private static createSceneLights(scene: Scene): void {
if (!this.hemisphericLight) {
this.hemisphericLight = new HemisphericLight("renderModeHemiLight", new Vector3(0, 1, 0), scene);
this.hemisphericLight.intensity = 0.7;
}
if (!this.pointLight) {
this.pointLight = new PointLight("renderModePointLight", new Vector3(2, 3, 2), scene);
this.pointLight.intensity = 0.8;
}
this.hemisphericLight.setEnabled(true);
this.pointLight.setEnabled(true);
}
/**
* Disables scene lights used for DIFFUSE_WITH_LIGHTS mode
*/
private static disableSceneLights(): void {
if (this.hemisphericLight) {
this.hemisphericLight.setEnabled(false);
}
if (this.pointLight) {
this.pointLight.setEnabled(false);
}
}
/**
* Updates all materials in the scene to use the specified rendering mode
* @param scene The BabylonJS scene
* @param mode The rendering mode to apply
*/
public static updateAllMaterials(scene: Scene, mode: RenderingMode): void {
this.currentMode = mode;
// Enable or disable scene lights based on mode
if (mode === RenderingMode.DIFFUSE_WITH_LIGHTS) {
this.createSceneLights(scene);
} else {
this.disableSceneLights();
}
scene.materials.forEach(material => {
if (material instanceof StandardMaterial) {
// Skip UI materials (buttons, handles, and labels use emissiveTexture with text rendering)
if (material.name === 'buttonMat' ||
material.name === 'handleMaterial' ||
material.name === 'text-mat' ||
material.id.includes('button') ||
material.id.includes('handle') ||
material.id.includes('text')) {
return;
}
// Try to determine the base color from existing material
let baseColor: Color3;
if (material.emissiveColor && material.emissiveColor.toLuminance() > 0) {
baseColor = material.emissiveColor.clone();
} else if (material.diffuseColor && material.diffuseColor.toLuminance() > 0) {
baseColor = material.diffuseColor.clone();
} else {
// Skip materials without a color set
return;
}
this.applyRenderingModeToMaterial(material, baseColor, mode, scene);
}
});
}
} }

36
src/util/renderingMode.ts Normal file
View File

@ -0,0 +1,36 @@
/**
* Rendering modes for materials in the scene
*
* LIGHTMAP_WITH_LIGHTING: Uses diffuseColor + lightmapTexture with lighting enabled
* - Provides lighting illusion with actual lighting calculations
* - Most expensive performance-wise
* - disableLighting = false
*
* UNLIT_WITH_EMISSIVE_TEXTURE: Uses emissiveColor + emissiveTexture with lighting disabled
* - Provides lighting illusion without lighting calculations (current default)
* - Best balance of visual quality and performance
* - disableLighting = true
*
* FLAT_EMISSIVE: Uses only emissiveColor with lighting disabled
* - Flat shading, no lighting illusion
* - Best performance
* - disableLighting = true
*
* DIFFUSE_WITH_LIGHTS: Uses diffuseColor with two scene lights enabled
* - Real-time lighting calculations with dynamic lights
* - Provides realistic lighting and shadows
* - disableLighting = false
*/
export enum RenderingMode {
LIGHTMAP_WITH_LIGHTING = "lightmap_with_lighting",
UNLIT_WITH_EMISSIVE_TEXTURE = "unlit_with_emissive_texture",
FLAT_EMISSIVE = "flat_emissive",
DIFFUSE_WITH_LIGHTS = "diffuse_with_lights"
}
export const RenderingModeLabels = {
[RenderingMode.LIGHTMAP_WITH_LIGHTING]: "Lightmap + Lighting",
[RenderingMode.UNLIT_WITH_EMISSIVE_TEXTURE]: "Emissive Texture",
[RenderingMode.FLAT_EMISSIVE]: "Flat Color",
[RenderingMode.DIFFUSE_WITH_LIGHTS]: "Diffuse + Lights"
};