prophetk-3d-viewer
v0.1.8
Published
A Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.
Downloads
479
Maintainers
Readme
prophetk-3d-viewer
Three.js r128 based GLB/GLTF model viewer for WeChat Mini Program and Web.
The package bundles the same Three.js r128 build for both platforms. It does not rely on the host project's Three.js version, which keeps material, lighting, GLTF parsing, and decal shader behavior consistent.
Features
- WeChat Mini Program component.
- Web viewer helper for browser projects.
- Built-in Three.js r128 and patched GLTFLoader.
- GLB/GLTF loading.
- Scene JSON loading.
- Orbit touch controls.
- Canvas-rendered button UI for Mini Program/Web overlays.
- Cylindrical decal projection on PBR materials.
- WeChat local file cache.
WeChat Mini Program
Install the npm package, run "构建 npm" in WeChat DevTools, then register the component:
{
"usingComponents": {
"model-viewer": "prophetk-3d-viewer/wechat/model-viewer"
}
}Use it in WXML:
<model-viewer
id="viewer"
canvas-id="modelCanvas"
bind:uievent="onViewerUiEvent"
/>Call the component API:
const viewer = this.selectComponent('#viewer');
await viewer.loadScene(sceneConfig);
await viewer.loadModel('https://cdn.example.com/model.glb');
viewer.updateDecal(Math.PI / 4, 0, 1, 1);Handle canvas-rendered UI events with bind:uievent:
Page({
onViewerUiEvent(e) {
const { event } = e.detail;
if (event === 'event_btn_reload_model') {
this.selectComponent('#viewer').loadScene();
}
}
});Remote model, texture, and scene URLs must be added to the Mini Program legal domain allowlist.
Web
Use the bundled Web entry:
const { createWebModelViewer } = require('prophetk-3d-viewer/web');
const viewer = createWebModelViewer({
container: document.querySelector('#viewer'),
bgColor: '#000000',
onUiEvent(detail) {
console.log(detail.event);
},
});
await viewer.loadScene(sceneConfig);The Web helper also dispatches a uiEvent DOM event on the canvas:
viewer.canvas.addEventListener('uiEvent', (e) => {
console.log(e.detail.event);
});The container must have a stable width and height:
#viewer {
width: 100%;
height: 480px;
}Core API
const { ModelRenderer, WebPlatformAdapter } = require('prophetk-3d-viewer');
const renderer = new ModelRenderer(canvas, {
platform: new WebPlatformAdapter(),
renderMode: 'continuous',
});Main methods:
loadModel(url, options)switchModel(url, options)loadScene(configOrUrl)clearModel(target)clearAllModels()applyDecal(url, options)updateDecal(options)orupdateDecal(angle, height, scaleX, scaleY)updateProjectorDecal(options)removeDecal()setUiEventHandler(handler)setUiLabelText(id, text)dispose()
Decal API
applyDecal(url, options) loads a decal texture and applies it to the current model. PNG textures with alpha are recommended.
Cylindrical decals wrap around the model on the world Y axis:
await renderer.applyDecal('https://cdn.example.com/decal.png', {
projectionMode: 'cylindrical',
angle: Math.PI / 4,
height: 0,
scaleX: 1,
scaleY: 1,
arcWidth: Math.PI / 3,
heightRange: 1,
opacity: 1
});Projector decals use an orthographic projector:
await renderer.applyDecal('https://cdn.example.com/decal.png', {
projectionMode: 'projector',
projectorPosition: [0, 0.5, 2],
projectorTarget: [0, 0.5, 0],
projectorRollDeg: 0,
projectorWidth: 1,
projectorHeight: 1,
projectorNormalThreshold: 0,
scaleX: 1,
scaleY: 1,
opacity: 1
});projectionMode can be cylindrical, projector, or orthographic. mode is also accepted as an alias. Projector options automatically switch the decal to projector mode when projectionMode is omitted.
Common decal options:
scaleX,scaleY: texture scale inside the projected region.opacity: decal blend opacity.angle: cylindrical center angle in radians.height: cylindrical center height in world units.arcWidth: cylindrical angular width in radians.heightRange: cylindrical vertical span in world units.projectorPositionorposition: projector origin.projectorTargetortargetPoint: world point the projector aims at.projectorDirectionordirection: projector direction when no target is provided.projectorRotationorrotation: Euler XYZ rotation in radians when no target is provided.projectorRollorroll: rotation around the projector direction in radians.projectorRollDegorrollDeg: rotation around the projector direction in degrees.projectorUporup: projector up direction.projectorWidthorwidth: projector width.projectorHeight: projector height.heightis also accepted in projector mode, butprojectorHeightavoids ambiguity with cylindrical height.projectorNormalThresholdornormalThreshold: surface normal visibility threshold.
updateDecal(options) updates the active decal without reloading the texture:
renderer.updateDecal({
angle: Math.PI / 2,
height: 0.3,
scaleX: 1.2,
scaleY: 1,
arcWidth: Math.PI / 4,
heightRange: 0.8,
opacity: 0.9
});The legacy positional form is still supported for cylindrical decals:
renderer.updateDecal(Math.PI / 2, 0.3, 1.2, 1);For projector decals, either pass projector options to updateDecal(options) or use updateProjectorDecal(options):
renderer.updateProjectorDecal({
projectorPosition: [0, 0.5, 2],
projectorTarget: [0, 0.5, 0],
projectorRollDeg: 0,
projectorWidth: 1,
projectorHeight: 1
});Canvas UI
The ui field in loadScene() renders interactive controls inside the same Three.js canvas. This is useful on WeChat Mini Program because ordinary Mini Program views and cover-view are not reliable overlays above a WebGL canvas.
Currently supported UI types:
buttonsliderlabel
Example:
await viewer.loadScene({
models: [
{
id: 'main',
type: 'gltf',
url: 'https://cdn.example.com/model.glb'
}
],
ui: [
{
type: 'button',
position: [0.04, 0.04],
scale: [0.24, 0.08],
text: 'Reload',
img: '',
alpha: 1,
event: 'event_btn_reload_model'
},
{
type: 'slider',
position: [0.04, 0.16],
scale: [0.42, 0.08],
text: 'decal x',
min: 0.2,
max: 3,
value: 1,
step: 0.01,
event: 'event_slider_decal_scale_x',
target: 'decal.scaleX'
},
{
type: 'label',
id: 'status_label',
position: [0.04, 0.88],
scale: [0.5, 0.06],
text: 'Ready',
color: '#ffffff',
backgroundColor: 'rgba(0, 0, 0, 0.35)'
}
]
});position and scale use normalized canvas coordinates. The origin is the canvas bottom-left corner:
position: [0, 0]places the button at the bottom-left of the canvas.position: [1, 1]starts at the top-right of the canvas.- X grows to the right.
- Y grows upward.
positionmeans the button's bottom-left corner, not its center.scale: [0.24, 0.08]means 24% of the canvas width and 8% of the canvas height.- Normalized values keep button size and placement broadly consistent across different phones and canvas resolutions.
Button fields:
type: must be'button'.position:[x, y]normalized to the canvas, bottom-left origin.scale:[width, height]normalized to the canvas.text: optional text drawn into the button texture.img: optional image URL used as the button background. If empty, a default rounded dark button is drawn.alpha: button opacity from0to1.event: string key emitted when the user taps/clicks the button.backgroundColor: optional fallback background color whenimgis empty.borderColor: optional fallback border color whenimgis empty.color: optional text color.fontSize: optional text size in pixels.
When the user presses a button, it renders a built-in pressed state by scaling down slightly and lowering opacity. If the pointer moves outside the button before release, the pressed state is cancelled and no event is emitted.
Slider fields:
type:'slider'. The alias'slide'is also accepted.position:[x, y]normalized to the canvas, bottom-left origin.scale:[width, height]normalized to the canvas.text: optional label shown above the track.min: minimum value. Default is0.max: maximum value. Default is1.value: initial value.step: optional step size.alpha: slider opacity from0to1.event: string key emitted when the slider changes.target: optional built-in target. Currentlydecal.scaleXanddecal.scaleYupdate the active decal directly.trackColor: optional track color.fillColor: optional filled track color.thumbColor: optional thumb color.thumbBorderColor: optional thumb border color.color: optional label text color.fontSize: optional label text size in pixels.
Example decal scale controls:
ui: [
{
type: 'slider',
position: [0.04, 0.16],
scale: [0.42, 0.08],
text: 'decal x',
min: 0.2,
max: 3,
value: 1,
step: 0.01,
event: 'event_slider_decal_scale_x',
target: 'decal.scaleX'
},
{
type: 'slider',
position: [0.04, 0.27],
scale: [0.42, 0.08],
text: 'decal y',
min: 0.2,
max: 3,
value: 1,
step: 0.01,
event: 'event_slider_decal_scale_y',
target: 'decal.scaleY'
}
]Slider events include the current value:
{
type: 'slider',
event: 'event_slider_decal_scale_x',
text: 'decal x',
value: 1.35
}Label fields:
type: must be'label'.id: required if the label text will be updated from code.position:[x, y]normalized to the canvas, bottom-left origin.scale:[width, height]normalized to the canvas.text: initial display text.alpha: label opacity from0to1.backgroundColor: optional label background.borderColor: optional border color.borderWidth: optional border width in pixels.radius: optional corner radius in pixels.padding: optional horizontal text padding in pixels.color: optional text color.fontSize: optional text size in pixels.align: optional text alignment, one of'left','center', or'right'.
Labels do not consume touch input. They are rendered in the canvas like buttons and sliders, but pointer/touch events pass through to model orbit controls.
Update a label after the scene is loaded:
const viewer = this.selectComponent('#viewer');
viewer.setUiLabelText('status_label', 'Loading model...');Web usage:
viewer.setUiLabelText('status_label', 'Scale X: 1.25');WeChat UI Events
Use lowercase bind:uievent in WXML:
<model-viewer
id="viewer"
canvas-id="modelCanvas"
bind:uievent="onViewerUiEvent"
/>Then route by e.detail.event:
Page({
onViewerUiEvent(e) {
if (e.detail.event === 'event_btn_reload_model') {
this.selectComponent('#viewer').loadScene();
}
}
});Event detail:
{
type: 'button',
event: 'event_btn_reload_model',
text: 'Reload',
x: 24,
y: 438
}x and y are the raw touch position in canvas CSS pixels with top-left browser/touch coordinates. Use event as the stable business key.
Web UI Events
For Web projects, pass onUiEvent to createWebModelViewer():
const viewer = createWebModelViewer({
container: document.querySelector('#viewer'),
onUiEvent(detail) {
if (detail.event === 'event_btn_reload_model') {
viewer.loadScene(sceneConfig);
}
}
});The canvas also emits a uiEvent DOM CustomEvent:
viewer.canvas.addEventListener('uiEvent', (e) => {
console.log(e.detail);
});UI Notes
- UI is rendered after the 3D scene with a separate orthographic camera.
- UI config uses normalized coordinates, then the renderer converts them to current canvas CSS pixels.
- When the Web helper resizes, the active UI config is rebuilt against the new canvas size.
- UI does not use Mini Program layout, so it will not be affected by
z-indexor native component layer limits. - UI textures are generated with an offscreen 2D canvas. On WeChat, this requires
wx.createOffscreenCanvas. - Image URLs in
imguse the same download/cache path as model and decal assets, so remote domains must be allowlisted. - Buttons and sliders consume touch input while active, so UI interaction will not rotate the model. Labels are non-interactive.
Scene Config
{
renderer: {
clearColor: '#000000',
clearAlpha: 1,
outputEncoding: 'sRGB',
toneMapping: 'ACESFilmic'
},
camera: {
position: [0, 1.5, 3],
lookAt: [0, 0, 0],
fov: 60
},
models: [
{
id: 'main',
type: 'gltf',
url: 'https://cdn.example.com/model.glb',
rotation: [0, 90, 0],
scale: [1, 1, 1]
}
],
lights: [
{ type: 'ambient', color: '#ffffff', intensity: 0.5 },
{ type: 'directional', color: '#ffffff', intensity: 0.8, position: [5, 10, 7] }
],
ui: [
{
type: 'button',
position: [0.04, 0.04],
scale: [0.24, 0.08],
text: 'Reload',
img: '',
alpha: 1,
event: 'event_btn_reload_model'
},
{
type: 'slider',
position: [0.04, 0.16],
scale: [0.42, 0.08],
text: 'decal x',
min: 0.2,
max: 3,
value: 1,
step: 0.01,
event: 'event_slider_decal_scale_x',
target: 'decal.scaleX'
},
{
type: 'slider',
position: [0.04, 0.27],
scale: [0.42, 0.08],
text: 'decal y',
min: 0.2,
max: 3,
value: 1,
step: 0.01,
event: 'event_slider_decal_scale_y',
target: 'decal.scaleY'
},
{
type: 'label',
id: 'status_label',
position: [0.04, 0.88],
scale: [0.5, 0.06],
text: 'Ready',
color: '#ffffff',
backgroundColor: 'rgba(0, 0, 0, 0.35)'
}
],
decal_projector: {
target: 'main',
url: 'https://cdn.example.com/decal.png',
scaleX: 1,
scaleY: 1,
opacity: 1,
projectorPosition: [0, 0.5, 2],
projectorTarget: [0, 0.5, 0],
projectorRollDeg: 0,
projectorWidth: 1,
projectorHeight: 1,
projectorNormalThreshold: 0,
debug: true
}
}For cylindrical decals, use the decal block:
{
decal: {
target: 'main',
url: 'https://cdn.example.com/decal.png',
decalAngleDeg: 45,
decalHeight: 0,
decalScaleX: 1,
decalScaleY: 1,
arcWidth: Math.PI / 3,
heightRange: 1,
opacity: 1
}
}For projector decals, use the top-level decal_projector block. decalProjector is also accepted. In both blocks, target is the model id; projector aim uses projectorTarget.
Model rotation values are Euler XYZ degrees.
Local Development
npm run checkThe check verifies that all package entries use bundled Three.js r128.
