73 |
{
74 | if (open) setOpen(false);
75 | }} />
76 |
77 |
78 | {props.camera !== null && (
79 | {
86 | if (value && renderModeOpen) setRenderModeOpen(false);
87 | setOpen(value);
88 | }}
89 | up={true}
90 | />
91 | )}
92 | {
98 | if (value === currentRenderMode) return;
99 | const newRenderMode = value as RenderMode;
100 | props.onSelectRenderMode(newRenderMode);
101 | setCurrentRenderMode(newRenderMode);
102 | }}
103 | onToggle={(value: boolean) => {
104 | if (value && open) setOpen(false);
105 | if (modeOpen) setModeOpen(false);
106 | setRenderModeOpen(value);
107 | }}
108 | up={true}
109 | />
110 |
111 |
112 | );
113 | });
114 |
115 | export default CameraWindow;
116 |
--------------------------------------------------------------------------------
/src/editor/multiView/MultiView.scss:
--------------------------------------------------------------------------------
1 | $padding: 2px;
2 |
3 | .editor .multiview {
4 | display: grid;
5 | font-family: Arial, Helvetica, sans-serif;
6 | font-size: 10px;
7 | grid-template-columns: auto;
8 | position: absolute;
9 | overflow: hidden;
10 | left: 0;
11 | top: 0;
12 | right: 300px;
13 | bottom: 0;
14 | z-index: 1;
15 |
16 | canvas {
17 | pointer-events: none;
18 | }
19 |
20 | .dropdown {
21 | background-color: #222;
22 | display: inline-block;
23 | font-size: 10px;
24 | text-align: center;
25 |
26 | .dropdown-toggle {
27 | cursor: pointer;
28 | padding: 0px 10px;
29 | height: 30px;
30 | line-height: 30px;
31 | overflow: hidden;
32 | &:hover {
33 | background-color: #333;
34 | }
35 | }
36 |
37 | .dropdown-menu {
38 | overflow: hidden;
39 | position: absolute;
40 | top: 30px;
41 | left: 50%;
42 | z-index: 1;
43 | list-style: none;
44 | padding: 0;
45 | margin: 0;
46 | min-width: 100%;
47 | width: auto;
48 | transform: translateX(-50%);
49 | transition: all 0.33s cubic-bezier(0.750, 0, 0.25, 1.000);
50 |
51 | li {
52 | background-color: #222;
53 | border-top: 1px solid #191919;
54 | cursor: pointer;
55 | height: 30px;
56 | line-height: 30px;
57 | padding: 0px 10px;
58 | transition: 0.2s linear background-color;
59 | &:hover {
60 | background-color: #333;
61 | }
62 | }
63 | }
64 | }
65 |
66 | .cameras {
67 | display: grid;
68 | grid-template-columns: repeat(2, 1fr);
69 | pointer-events: visible;
70 | position: absolute;
71 | width: 100%;
72 | height: 100%;
73 |
74 | &.single {
75 | grid-template-columns: repeat(1, 1fr);
76 | }
77 |
78 | .CameraWindow {
79 | border: 1px dotted #333;
80 | border-top: none;
81 | border-left: none;
82 | pointer-events: visible;
83 | position: relative;
84 |
85 | .clickable {
86 | display: inline-block;
87 | width: 100%;
88 | height: 100%;
89 | }
90 |
91 | .options {
92 | position: absolute;
93 | height: 30px;
94 | top: initial;
95 | bottom: -1px;
96 | left: 50%;
97 | transform: translateX(-50%);
98 | width: max-content;
99 |
100 | .dropdown {
101 | position: relative;
102 | top: 0;
103 | transition: background-color 0.25s linear;
104 | &.up {
105 | background-color: #333;
106 | bottom: 0;
107 | top: initial;
108 | .dropdown-menu {
109 | top: initial;
110 | bottom: 100%;
111 | }
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 | .settings {
119 | box-shadow: rgba(0, 0, 0, 0.25) 0px 1px 1px, rgba(0, 0, 0, 0.15) 0px 2px 6px;
120 | pointer-events: visible;
121 | position: absolute;
122 | left: 50%;
123 | transform: translateX(-50%);
124 |
125 | .toggle {
126 | background-blend-mode: overlay;
127 | background-color: #222;
128 | background-position: 2px 2px;
129 | background-repeat: no-repeat;
130 | background-size: 26px 26px;
131 | display: inline-block;
132 | position: relative;
133 | left: 0;
134 | transform: none;
135 | width: 30px;
136 | height: 30px;
137 | overflow: hidden;
138 | &.selected {
139 | background-blend-mode: normal;
140 | }
141 | }
142 | }
143 |
144 | .connectionStatus {
145 | background-color: red;
146 | font-weight: bold;
147 | line-height: 30px;
148 | padding: 0 10px;
149 | text-transform: uppercase;
150 | position: absolute;
151 | left: 0;
152 | bottom: 0;
153 | z-index: 10;
154 | }
155 | }
--------------------------------------------------------------------------------
/src/editor/sidePanel/inspector/utils/InspectAnimation.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { AnimationMixer, SkeletonHelper } from 'three';
3 | import RemoteThree from '../../../../core/remote/RemoteThree';
4 | import InspectorGroup from '../InspectorGroup';
5 | import { AnimationClipInfo, RemoteObject } from '../../types';
6 | import MultiView from '../../../multiView/MultiView';
7 | import { dispose } from '../../../../utils/three';
8 |
9 | type InspectAnimationProps = {
10 | object: RemoteObject;
11 | three: RemoteThree;
12 | }
13 |
14 | export default function InspectAnimation(props: InspectAnimationProps) {
15 | const object = props.object;
16 | const three = props.three;
17 | function expandedName(): string {
18 | return `${three.name}_animation`;
19 | }
20 |
21 | const expandedValue = localStorage.getItem(expandedName());
22 | const expanded = expandedValue !== null ? expandedValue === 'open' : false;
23 |
24 | function saveExpanded(value: boolean) {
25 | localStorage.setItem(expandedName(), value ? 'open' : 'closed');
26 | }
27 |
28 | const items: any[] = [];
29 | const animations: any[] = [];
30 | let maxDuration = 0;
31 | object.animations.forEach((clipInfo: AnimationClipInfo) => {
32 | // Add animation
33 | maxDuration = Math.max(maxDuration, clipInfo.duration);
34 | if (clipInfo.duration > 0) {
35 | animations.push({
36 | title: clipInfo.name,
37 | items: [
38 | {
39 | title: 'Duration',
40 | type: 'number',
41 | value: clipInfo.duration,
42 | disabled: true,
43 | },
44 | {
45 | title: 'Blend Mode',
46 | type: 'option',
47 | disabled: true,
48 | options: [
49 | {
50 | title: 'Normal',
51 | value: 2500,
52 | },
53 | {
54 | title: 'Additive',
55 | value: 2501,
56 | },
57 | ],
58 | }
59 | ]
60 | });
61 | }
62 | });
63 | items.push({
64 | title: 'Animations',
65 | items: animations
66 | });
67 |
68 | let helper: SkeletonHelper | undefined = undefined;
69 | const scene = three.getScene(object.uuid);
70 | if (scene !== null) {
71 | const child = scene.getObjectByProperty('uuid', object.uuid);
72 | if (child !== undefined) {
73 | const mixer = child['mixer'] as AnimationMixer;
74 | const hasMixer = mixer !== undefined;
75 | if (hasMixer) {
76 | const mixerItems: any[] = [
77 | {
78 | title: 'Time Scale',
79 | type: 'range',
80 | value: mixer.timeScale,
81 | step: 0.01,
82 | min: -1,
83 | max: 2,
84 | onChange: (_: string, value: any) => {
85 | mixer.timeScale = value;
86 | three.updateObject(object.uuid, 'mixer.timeScale', value);
87 | },
88 | },
89 | ];
90 | mixerItems.push({
91 | title: 'Stop All',
92 | type: 'button',
93 | onChange: () => {
94 | mixer.stopAllAction();
95 | three.requestMethod(object.uuid, 'stopAllAction', undefined, 'mixer');
96 | }
97 | });
98 | items.push({
99 | title: 'Mixer',
100 | items: mixerItems
101 | });
102 |
103 | helper = new SkeletonHelper(child);
104 | MultiView.instance?.scene.add(helper);
105 | }
106 | }
107 | }
108 |
109 | useEffect(() => {
110 | return () => {
111 | if (helper !== undefined) dispose(helper);
112 | };
113 | }, []);
114 |
115 | return (
116 |
{
122 | saveExpanded(value);
123 | }}
124 | />
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/example/three/loader.ts:
--------------------------------------------------------------------------------
1 | import { CubeTexture, CubeTextureLoader, Group, Object3D, RepeatWrapping, Texture, TextureLoader } from 'three';
2 | // @ts-ignore
3 | import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader';
4 | import { Events, threeDispatcher } from '../constants';
5 | import Application from '../../core/Application';
6 | import RemoteTheatre from '../../core/remote/RemoteTheatre';
7 |
8 | export const cubeTextures: Map = new Map();
9 | export const json: Map = new Map();
10 | export const models: Map = new Map();
11 | export const textures: Map = new Map();
12 |
13 | export function loadCube(name: string, source: string[]): Promise {
14 | return new Promise((resolve, reject) => {
15 | new CubeTextureLoader()
16 | .setPath('images/milkyWay/')
17 | .load(source, (value: CubeTexture) => {
18 | cubeTextures.set(name, value);
19 | resolve(value);
20 | }, undefined, () => {
21 | reject();
22 | });
23 | });
24 | }
25 |
26 | export function loadModel(name: string, source: string): Promise {
27 | return new Promise((resolve, reject) => {
28 | new FBXLoader()
29 | .setPath('./models/')
30 | .loadAsync(source)
31 | .then((model: Group) => {
32 | // Shadows
33 | model.traverse((obj: Object3D) => {
34 | // @ts-ignore
35 | if (obj['isMesh']) {
36 | obj.castShadow = true;
37 | obj.receiveShadow = true;
38 | }
39 | });
40 |
41 | models.set(name, model);
42 | resolve(model);
43 | })
44 | .catch((reason: any) => {
45 | console.log(`Couldn't load:`, source);
46 | console.log(reason);
47 | reject();
48 | });
49 | });
50 | }
51 |
52 | export function loadTexture(name: string, source: string): Promise {
53 | return new Promise((resolve, reject) => {
54 | new TextureLoader()
55 | .load(source, (value: Texture) => {
56 | value.wrapS = RepeatWrapping;
57 | value.wrapT = RepeatWrapping;
58 | value.needsUpdate = true;
59 | textures.set(name, value);
60 | resolve(value);
61 | }, undefined, () => {
62 | reject();
63 | });
64 | });
65 | }
66 |
67 | export function loadJSON(name: string, source: string): Promise {
68 | return new Promise((resolve, reject) => {
69 | fetch(source)
70 | .then(response => {
71 | if (!response.ok) {
72 | throw new Error(`Network response was not ok: ${response.status}`);
73 | }
74 | return response.json();
75 | })
76 | .then(data => {
77 | json.set(name, data);
78 | resolve(data);
79 | })
80 | .catch(() => {
81 | console.log(`Couldn't load: ${source}`);
82 | reject();
83 | });
84 | });
85 | }
86 |
87 | export function loadAssets(app: Application): Promise {
88 | return new Promise((resolve, reject) => {
89 | const assets: (() => Promise)[] = [
90 | () => loadCube('environment', [
91 | 'dark-s_px.jpg',
92 | 'dark-s_nx.jpg',
93 | 'dark-s_py.jpg',
94 | 'dark-s_ny.jpg',
95 | 'dark-s_pz.jpg',
96 | 'dark-s_nz.jpg'
97 | ]),
98 | () => loadTexture('uv_grid', 'images/uv_grid_opengl.jpg'),
99 | () => loadModel('Flair', 'Flair.fbx'),
100 | () => loadJSON('animation', 'json/animation.json'),
101 | ];
102 |
103 | Promise.all(assets.map(load => load()))
104 | .then(() => {
105 | const theatre = app.components.get('theatre') as RemoteTheatre;
106 | const state = json.get('animation');
107 | theatre.loadProject('RemoteApp', state).then(() => {
108 | threeDispatcher.dispatchEvent({ type: Events.LOAD_COMPLETE });
109 | resolve();
110 | });
111 | })
112 | .catch((reason) => {
113 | console.log(reason);
114 | reject();
115 | });
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/src/editor/sidePanel/inspector/InspectImage.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { uploadLocalImage } from './utils/InspectMaterial';
3 | import { noImage } from '../../../editor/components/content';
4 | import { randomID } from '../../../editor/utils';
5 |
6 | type InspectImageProps = {
7 | title: string;
8 | prop?: string;
9 | value?: any;
10 | step?: number;
11 | onChange?: (prop: string, value: any) => void;
12 | }
13 |
14 | export default function InspectImage(props: InspectImageProps) {
15 | const step = props.step !== undefined ? props.step : 0.01;
16 | // References
17 | const imgRefRef = useRef(null);
18 | const offXRef = useRef(null);
19 | const offYRef = useRef(null);
20 | const repeatXRef = useRef(null);
21 | const repeatYRef = useRef(null);
22 |
23 | // States
24 | const [fieldValue] = useState(props.value);
25 | const [offsetX, setOffsetX] = useState(props.value.offset[0]);
26 | const [offsetY, setOffsetY] = useState(props.value.offset[1]);
27 | const [repeatX, setRepeatX] = useState(props.value.repeat[0]);
28 | const [repeatY, setRepeatY] = useState(props.value.repeat[1]);
29 |
30 | function onChange(src: string, ox: number, oy: number, rx: number, ry: number) {
31 | if (props.onChange !== undefined) {
32 | const title = props.prop !== undefined ? props.prop : props.title;
33 | props.onChange(title, {
34 | src: src,
35 | offset: [ox, oy],
36 | repeat: [rx, ry],
37 | });
38 | }
39 | }
40 |
41 | function changeInput(evt: any) {
42 | const src = imgRefRef.current!.src;
43 | const value = evt.target.value;
44 | switch (evt.target) {
45 | case offXRef.current:
46 | setOffsetX(value);
47 | onChange(src, value, offsetY, repeatX, repeatY);
48 | break;
49 | case offYRef.current:
50 | setOffsetY(value);
51 | onChange(src, offsetX, value, repeatX, repeatY);
52 | break;
53 | case repeatXRef.current:
54 | setRepeatX(value);
55 | onChange(src, offsetX, offsetY, value, repeatY);
56 | break;
57 | case repeatYRef.current:
58 | setRepeatY(value);
59 | onChange(src, offsetX, offsetY, repeatX, value);
60 | break;
61 | }
62 | }
63 |
64 | return (
65 |
66 |
![{props.title}]()
{
67 | uploadLocalImage()
68 | .then((value: string) => {
69 | imgRefRef.current!.src = value;
70 | onChange(value, offsetX, offsetY, repeatX, repeatY);
71 | });
72 | }} src={fieldValue.src.length > 0 ? fieldValue.src : noImage} />
73 |
117 |
118 | );
119 | }
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Hermes
2 |
3 | An extendable set of Web Tools controlled over a separate window for non-intereference with content (like a remote controller!)
4 |
5 | Open the [Application](https://hermes-lovat.vercel.app/) and [editor](https://hermes-lovat.vercel.app/#editor) side-by-side.
6 |
7 | ## Setup
8 |
9 | This example uses [React](https://react.dev/), [ThreeJS](https://threejs.org/), and [TheatreJS](https://theatrejs.com/).
10 |
11 | ### Create an `Application`
12 |
13 | An application isn't required, however it's nice to maintain multiple remotes. Alternatively, Remotes can be created independently.
14 |
15 | The `ThreeEditor` is used as a multi-view editor for [ThreeJS](https://threejs.org/), and should be limited to only the Editor app.
16 |
17 | ```
18 | const IS_DEV = true;
19 | const IS_EDITOR = IS_DEV && document.location.hash.search('editor') > -1;
20 |
21 | const theatre = new RemoteTheatre(IS_DEV, IS_EDITOR);
22 | const three = new RemoteThree('Hermes Example', IS_DEV, IS_EDITOR);
23 |
24 | export default function AppWrapper() {
25 | const [app, setApp] = useState(null);
26 |
27 | useEffect(() => {
28 | const instance = new Application();
29 | instance.detectSettings(IS_DEV, IS_EDITOR).then(() => {
30 | // TheatreJS
31 | instance.addComponent('theatre', theatre);
32 |
33 | // ThreeJS
34 | instance.addComponent('three', three);
35 |
36 | // Ready
37 | setApp(instance);
38 | });
39 | }, []);
40 |
41 | // MultiView requires you identify each scene so they can be instantiated by the editor
42 | const scenes: Map = new Map();
43 | scenes.set('Scene1', Scene1);
44 | scenes.set('Scene2', Scene2);
45 | scenes.set('RTTScene', RTTScene);
46 |
47 | return (
48 | <>
49 | {app !== null && (
50 | <>
51 | {IS_DEV && (
52 | <>
53 | {IS_EDITOR && (
54 | {
58 | scene.update();
59 | }}
60 | />
61 | )}
62 | >
63 | )}
64 | >
65 | )}
66 | >
67 | );
68 | }
69 | ```
70 |
71 | ### Scene setup
72 |
73 | After all object's have been added to your scene, run `hierarchyUUID(yourScene)` to update the UUIDs of every object. This helps communicate back and forth between the app and your editor.
74 |
75 | ### Custom remote commands
76 |
77 | This component is added only in debug-mode to add extra support for remote-components.
78 |
79 | In this example it's added to add custom Remote Component support for:
80 |
81 | - [TheatreJS](https://theatrejs.com/) - Communicates with the `studio` instance
82 |
83 | ```
84 | type RemoteProps = {
85 | three: RemoteThree
86 | theatre: RemoteTheatre
87 | }
88 |
89 | export default function RemoteSetup(props: RemoteProps) {
90 | // Remote Theatre setup
91 | props.theatre.studio = studio;
92 | props.theatre.handleEditorApp();
93 | return null;
94 | }
95 | ```
96 |
97 | ## Editor
98 |
99 | ### Tools for:
100 |
101 | - Customizable Navigation Dropdowns + Draggable components for Triggers/Event Dispatching
102 | - [TheatreJS](https://www.theatrejs.com/)
103 | - [ThreeJS](https://threejs.org/)
104 | - Custom ThreeJS Scene + Object Inspector
105 |
106 | ### ThreeJS Editor
107 |
108 | | Action | Keys |
109 | | ------ | ------ |
110 | | Zoom to Selected Item | CTRL + 0 |
111 | | Rotate to Front of Selected Item | CTRL + 1 |
112 | | Rotate to Top of Selected Item | CTRL + 2 |
113 | | Rotate to Right of Selected Item | CTRL + 3 |
114 | | Rotate to Back of Selected Item | CTRL + 4 |
115 | | Set Transform Controls to Rotate | r |
116 | | Set Transform Controls to Scale | s |
117 | | Set Transform Controls to Translate | t |
118 | | Toggles Transform Controls between **world** and **local** | q |
119 |
120 | ### Side Panel
121 |
122 | Holding down the **CTRL** key while dragging a number's label will multiply the delta by 10
123 |
124 | 
125 |
126 | ### Assets
127 |
128 | Animation / Models found at [Mixamo](https://www.mixamo.com/)
129 |
--------------------------------------------------------------------------------
/src/webworkers/EventHandling.ts:
--------------------------------------------------------------------------------
1 | // Transfer Events
2 |
3 | type EventHandler = (event: Event, sendFn: (data: any) => void) => void;
4 |
5 | const mouseEventHandler = makeSendPropertiesHandler([
6 | 'ctrlKey',
7 | 'metaKey',
8 | 'shiftKey',
9 | 'button',
10 | 'pointerId',
11 | 'pointerType',
12 | 'clientX',
13 | 'clientY',
14 | 'pageX',
15 | 'pageY',
16 | ]);
17 |
18 | const wheelEventHandlerImpl = makeSendPropertiesHandler([
19 | 'clientX',
20 | 'clientY',
21 | 'deltaX',
22 | 'deltaY',
23 | 'deltaMode',
24 | ]);
25 |
26 | const keydownEventHandler = makeSendPropertiesHandler([
27 | 'ctrlKey',
28 | 'metaKey',
29 | 'shiftKey',
30 | 'keyCode',
31 | ]);
32 |
33 | function wheelEventHandler(event: WheelEvent, sendFn: (data: any) => void): void {
34 | event.preventDefault();
35 | wheelEventHandlerImpl(event, sendFn);
36 | }
37 |
38 | function preventDefaultHandler(event: Event): void {
39 | event.preventDefault();
40 | }
41 |
42 | function copyProperties(
43 | src: Record,
44 | properties: string[],
45 | dst: Record
46 | ): void {
47 | for (const name of properties) {
48 | dst[name] = src[name];
49 | }
50 | }
51 |
52 | function makeSendPropertiesHandler(properties: string[]): EventHandler {
53 | return function sendProperties(event: Event, sendFn: (data: any) => void): void {
54 | const data: Record = { type: event.type };
55 | copyProperties(event as Record, properties, data);
56 | sendFn(data);
57 | };
58 | }
59 |
60 | function touchEventHandler(event: TouchEvent, sendFn: (data: any) => void): void {
61 | const touches: Array<{ pageX: number; pageY: number }> = [];
62 | const data = { type: event.type, touches };
63 |
64 | for (let i = 0; i < event.touches.length; ++i) {
65 | const touch = event.touches[i];
66 | touches.push({
67 | pageX: touch.pageX,
68 | pageY: touch.pageY,
69 | });
70 | }
71 |
72 | sendFn(data);
73 | }
74 |
75 | // The four arrow keys
76 | const orbitKeys: Record = {
77 | '37': true, // left
78 | '38': true, // up
79 | '39': true, // right
80 | '40': true, // down
81 | };
82 |
83 | function filteredKeydownEventHandler(
84 | event: KeyboardEvent,
85 | sendFn: (data: any) => void
86 | ): void {
87 | const { keyCode } = event;
88 | if (orbitKeys[keyCode]) {
89 | event.preventDefault();
90 | keydownEventHandler(event, sendFn);
91 | }
92 | }
93 |
94 | // Proxy
95 |
96 | export const WebworkerEventHandlers: Record = {
97 | contextmenu: preventDefaultHandler,
98 | mousedown: mouseEventHandler,
99 | mousemove: mouseEventHandler,
100 | mouseup: mouseEventHandler,
101 | pointerdown: mouseEventHandler,
102 | pointermove: mouseEventHandler,
103 | pointerup: mouseEventHandler,
104 | touchstart: touchEventHandler,
105 | touchmove: touchEventHandler,
106 | touchend: touchEventHandler,
107 | wheel: wheelEventHandler,
108 | keydown: filteredKeydownEventHandler,
109 | };
110 |
111 | let nextProxyId = 0;
112 |
113 | export class ElementProxy {
114 | id: number;
115 | worker: Worker;
116 |
117 | constructor(
118 | element: HTMLElement,
119 | worker: Worker,
120 | eventHandlers: Record
121 | ) {
122 | this.id = nextProxyId++;
123 | this.worker = worker;
124 |
125 | const sendEvent = (data: any): void => {
126 | this.worker.postMessage({
127 | type: 'event',
128 | id: this.id,
129 | data,
130 | });
131 | };
132 |
133 | // Register an ID
134 | worker.postMessage({
135 | type: 'makeProxy',
136 | id: this.id,
137 | });
138 |
139 | for (const [eventName, handler] of Object.entries(eventHandlers)) {
140 | element.addEventListener(eventName, (event) => {
141 | handler(event as any, sendEvent);
142 | });
143 | }
144 |
145 | function sendSize(): void {
146 | sendEvent({
147 | type: 'resize',
148 | left: 0,
149 | top: 0,
150 | width: innerWidth,
151 | height: innerHeight,
152 | });
153 | }
154 |
155 | // Really need to use ResizeObserver
156 | window.addEventListener('resize', sendSize);
157 | sendSize();
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/src/editor/sidePanel/ChildObject.tsx:
--------------------------------------------------------------------------------
1 | // Libs
2 | import { useEffect, useRef, useState } from 'react';
3 | // Models
4 | import { ChildObjectProps, RemoteObject } from './types';
5 | // Utils
6 | import { determineIcon, setItemProps } from './utils';
7 |
8 | export default function ChildObject(props: ChildObjectProps) {
9 | if (props.child === undefined) {
10 | console.log(`Hermes - No child attached`);
11 | return null;
12 | }
13 |
14 | const visibleRef = useRef(null);
15 | const [open, setOpen] = useState(false);
16 |
17 | const hasChildren = props.child.children.length > 0;
18 | const children: Array = [];
19 | if (props.child.children.length > 0) {
20 | props.child.children.map((child: RemoteObject, index: number) => {
21 | children.push();
22 | });
23 | }
24 |
25 | useEffect(() => {
26 | if (props.child) {
27 | const sceneUUID = props.child.uuid.split('.')[0];
28 | const scene = props.three.getScene(sceneUUID);
29 | if (scene !== null) {
30 | try {
31 | const child = scene.getObjectByProperty('uuid', props.child.uuid);
32 | if (child !== undefined) {
33 | visibleRef.current!.style.opacity = child.visible ? '1' : '0.25';
34 | } else {
35 | console.log(`Hermes - Can't find child: ${props.child.uuid}`);
36 | }
37 | } catch (err: any) {
38 | console.log(`Error looking for child:`, err);
39 | console.log(props.child);
40 | console.log(props.three.scenes);
41 | console.log(scene);
42 | }
43 | } else {
44 | console.log(`Hermes (ChildObject) - Can't find Scene: ${sceneUUID} with child UUID: ${props.child.uuid}`, props.three.scenes, props.three.scene, scene);
45 | }
46 | }
47 | }, [open]);
48 |
49 | return (
50 |
51 |
52 | {hasChildren ? (
53 |
62 | ) : null}
63 |
81 |
104 |
105 |
106 |
107 |
108 | {children}
109 |
110 |
111 |
112 | );
113 | }
--------------------------------------------------------------------------------
/src/editor/tools/Transform.ts:
--------------------------------------------------------------------------------
1 | // Libs
2 | import { Camera, EventDispatcher } from 'three';
3 | import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
4 | // Remote
5 | import RemoteThree, { ToolEvents } from '../../core/remote/RemoteThree';
6 | import MultiView from '../multiView/MultiView';
7 | // Utils
8 | import { dispose } from '../../utils/three';
9 |
10 | export default class Transform extends EventDispatcher {
11 | static DRAG_START = 'Transform::dragStart';
12 | static DRAG_END = 'Transform::dragEnd';
13 |
14 | private static _instance: Transform;
15 |
16 | three!: RemoteThree;
17 | activeCamera!: Camera;
18 | controls: Map = new Map();
19 |
20 | private visibility: Map = new Map();
21 |
22 | setApp(three: RemoteThree) {
23 | this.three = three;
24 | this.three.addEventListener(ToolEvents.SET_SCENE, this.setScene);
25 | }
26 |
27 | clear(): void {
28 | for (const controls of this.controls.values()) {
29 | controls.detach();
30 | controls.disconnect();
31 | const helper = controls.getHelper();
32 | dispose(helper);
33 | }
34 | this.controls = new Map();
35 | this.visibility = new Map();
36 | }
37 |
38 | add(name: string): TransformControls {
39 | let controls = this.controls.get(name);
40 | if (controls === undefined) {
41 | const element = document.querySelector('.clickable') as HTMLDivElement;
42 | controls = new TransformControls(this.activeCamera, element);
43 | controls.getHelper().name = name;
44 | controls.setSize(0.5);
45 | controls.setSpace('local');
46 | this.controls.set(name, controls);
47 | this.visibility.set(name, true);
48 |
49 | controls.addEventListener('mouseDown', () => {
50 | // @ts-ignore
51 | this.dispatchEvent({ type: Transform.DRAG_START });
52 | });
53 | controls.addEventListener('mouseUp', () => {
54 | // @ts-ignore
55 | this.dispatchEvent({ type: Transform.DRAG_END });
56 | });
57 |
58 | controls.addEventListener('dragging-changed', (evt: any) => {
59 | MultiView.instance?.toggleOrbitControls(evt.value);
60 | });
61 | }
62 | return controls;
63 | }
64 |
65 | get(name: string): TransformControls | undefined {
66 | return this.controls.get(name);
67 | }
68 |
69 | remove(name: string): boolean {
70 | const controls = this.get(name);
71 | if (controls === undefined) return false;
72 |
73 | controls.detach();
74 | controls.disconnect();
75 | dispose(controls.getHelper());
76 | this.controls.delete(name);
77 | return true;
78 | }
79 |
80 | enabled(value: boolean) {
81 | this.controls.forEach((controls: TransformControls) => {
82 | controls.enabled = value;
83 | });
84 | }
85 |
86 | updateCamera(camera: Camera, element: HTMLElement): void {
87 | this.activeCamera = camera;
88 | this.controls.forEach((controls: TransformControls) => {
89 | // Update camera
90 | if (controls.camera !== camera) {
91 | controls.camera = camera;
92 | // @ts-ignore
93 | camera.getWorldPosition(controls.cameraPosition);
94 | // @ts-ignore
95 | camera.getWorldQuaternion(controls.cameraQuaternion);
96 | }
97 |
98 | // Update element
99 | if (controls.domElement !== element) {
100 | controls.disconnect();
101 | controls.domElement = element;
102 | controls.connect(element);
103 | }
104 | });
105 | }
106 |
107 | show() {
108 | this.controls.forEach((controls: TransformControls) => {
109 | const helper = controls.getHelper();
110 | const value = this.visibility.get(helper.name);
111 | if (value !== undefined) helper.visible = value;
112 | });
113 | }
114 |
115 | hide() {
116 | this.controls.forEach((controls: TransformControls) => {
117 | const helper = controls.getHelper();
118 | this.visibility.set(helper.name, helper.visible);
119 | helper.visible = false;
120 | });
121 | }
122 |
123 | private setScene = () => {
124 | this.clear();
125 | };
126 |
127 | public static get instance(): Transform {
128 | if (!Transform._instance) {
129 | Transform._instance = new Transform();
130 | }
131 | return Transform._instance;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/editor/sidePanel/SidePanel.tsx:
--------------------------------------------------------------------------------
1 | // Libs
2 | import { useEffect, useState } from 'react';
3 | // Models
4 | import { RemoteObject, SidePanelState } from './types';
5 | // Components
6 | import '../scss/sidePanel.scss';
7 | import Accordion from './Accordion';
8 | import ContainerObject from './ContainerObject';
9 | import DebugData from './DebugData';
10 | import Inspector from './inspector/Inspector';
11 | import InspectRenderer from './inspector/utils/InspectRenderer';
12 | import { ToolEvents } from '../../core/remote/RemoteThree';
13 |
14 | export default function SidePanel(props: SidePanelState) {
15 | const [scenes] = useState([]);
16 | const [sceneComponents] = useState([]);
17 | const [lastUpdate, setLastUpdate] = useState(0);
18 |
19 | const onAddScene = (evt: any) => {
20 | const scene = evt.value;
21 | scenes.push(scene);
22 | sceneComponents.push(
23 | {
31 | props.three.refreshScene(scene.name);
32 | }}
33 | >
34 |
35 |
36 | );
37 | setLastUpdate(Date.now());
38 | };
39 |
40 | const onRefreshScene = (evt: any) => {
41 | const scene = evt.value;
42 | for (let i = 0; i < scenes.length; i++) {
43 | if (scene.uuid === scenes[i].uuid) {
44 | scenes[i] = scene;
45 | sceneComponents[i] = (
46 | {
54 | props.three.refreshScene(scene.name);
55 | }}
56 | >
57 |
58 |
59 | );
60 | setLastUpdate(Date.now());
61 | return;
62 | }
63 | }
64 | };
65 |
66 | const onRemoveScene = (evt: any) => {
67 | const scene = evt.value;
68 | for (let i = 0; i < scenes.length; i++) {
69 | if (scene.uuid === scenes[i].uuid) {
70 | scenes.splice(i, 1);
71 | sceneComponents.splice(i, 1);
72 | setLastUpdate(Date.now());
73 | return;
74 | }
75 | }
76 | };
77 |
78 | const onSetScene = (evt: any) => {
79 | const name = evt.value.name;
80 | for (let i = 0; i < scenes.length; i++) {
81 | const scene = scenes[i];
82 | const isActive = scene.name === name;
83 | sceneComponents[i] = (
84 | {
92 | props.three.refreshScene(scene.name);
93 | }}
94 | >
95 |
96 |
97 | );
98 | }
99 | setLastUpdate(Date.now());
100 | };
101 |
102 | useEffect(() => {
103 | props.three.addEventListener(ToolEvents.ADD_SCENE, onAddScene);
104 | props.three.addEventListener(ToolEvents.SET_SCENE, onSetScene);
105 | props.three.addEventListener(ToolEvents.REFRESH_SCENE, onRefreshScene);
106 | props.three.addEventListener(ToolEvents.REMOVE_SCENE, onRemoveScene);
107 | return () => {
108 | props.three.removeEventListener(ToolEvents.ADD_SCENE, onAddScene);
109 | props.three.removeEventListener(ToolEvents.SET_SCENE, onSetScene);
110 | props.three.removeEventListener(ToolEvents.REFRESH_SCENE, onRefreshScene);
111 | props.three.removeEventListener(ToolEvents.REMOVE_SCENE, onRemoveScene);
112 | };
113 | }, []);
114 |
115 | return (
116 |
117 |
118 | {sceneComponents}
119 |
120 |
121 |
122 |
123 |
124 | );
125 | }
126 |
--------------------------------------------------------------------------------
/src/example/components/App.tsx:
--------------------------------------------------------------------------------
1 | // Libs
2 | import { useEffect, useRef } from 'react';
3 | import { WebGLRenderer } from 'three';
4 | import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer.js';
5 | import Stats from 'stats-gl';
6 | // Models
7 | import Application from '../../core/Application';
8 | // Components
9 | import RemoteThree from '../../core/remote/RemoteThree';
10 | // Three
11 | import BaseScene from '../three/scenes/BaseScene';
12 | import Scene1 from '../three/scenes/Scene1';
13 | import Scene2 from '../three/scenes/Scene2';
14 | // Utils
15 | import { dispose } from '../../utils/three';
16 | import { clearComposerGroups } from '../../utils/post';
17 |
18 | let renderer: WebGLRenderer | WebGPURenderer;
19 | let currentScene: BaseScene;
20 | let sceneName = '';
21 |
22 | type AppProps = {
23 | app: Application
24 | }
25 |
26 | function App(props: AppProps) {
27 | const app = props.app;
28 | const canvasRef = useRef(null);
29 | const three = props.app.components.get('three') as RemoteThree;
30 |
31 | console.log('Settings', app.settings);
32 |
33 | // Renderer setup
34 | if (app.isApp) {
35 | useEffect(() => {
36 | const canvas = canvasRef.current!;
37 | // TODO - Add WebGPU support
38 | const useWebGPU = false;
39 | if (useWebGPU) {
40 | renderer = new WebGPURenderer({
41 | canvas,
42 | stencil: false
43 | });
44 | } else {
45 | renderer = new WebGLRenderer({
46 | canvas,
47 | stencil: false
48 | });
49 | }
50 | renderer.shadowMap.enabled = true;
51 | renderer.setPixelRatio(devicePixelRatio);
52 | renderer.setClearColor(0x000000);
53 | three.setRenderer(renderer, canvas);
54 |
55 | // ThreeJS
56 | const stats = new Stats();
57 | stats.init(renderer);
58 | document.body.appendChild(stats.dom);
59 |
60 | // Start RAF
61 | let raf = -1;
62 |
63 | const onResize = () => {
64 | const width = window.innerWidth;
65 | const height = window.innerHeight;
66 | currentScene?.resize(width, height);
67 | renderer.setSize(width, height);
68 | };
69 |
70 | const updateApp = () => {
71 | currentScene?.update();
72 | currentScene?.draw();
73 | stats.update();
74 | raf = requestAnimationFrame(updateApp);
75 | };
76 |
77 | window.addEventListener('resize', onResize);
78 | onResize();
79 | updateApp();
80 |
81 | // Dispose
82 | return () => {
83 | if (currentScene !== undefined) {
84 | three.removeCamera(currentScene.camera);
85 | three.removeScene(currentScene);
86 | dispose(currentScene);
87 | }
88 | window.removeEventListener('resize', onResize);
89 | cancelAnimationFrame(raf);
90 | raf = -1;
91 | renderer.dispose();
92 | };
93 | }, []);
94 | }
95 |
96 | // Load the scenes
97 |
98 | const createScene = () => {
99 | if (currentScene !== undefined) {
100 | if (currentScene.camera !== undefined) three.removeCamera(currentScene.camera);
101 | three.removeScene(currentScene);
102 | dispose(currentScene);
103 | clearComposerGroups(three);
104 | }
105 | if (sceneName === 'scene1') {
106 | currentScene = new Scene1();
107 | } else {
108 | currentScene = new Scene2();
109 | }
110 | currentScene.setup(app, renderer);
111 | currentScene.init();
112 | currentScene.resize(window.innerWidth, window.innerHeight);
113 | };
114 |
115 | const createScene1 = () => {
116 | if (sceneName === 'scene1') return;
117 | sceneName = 'scene1';
118 | createScene();
119 | };
120 |
121 | const createScene2 = () => {
122 | if (sceneName === 'scene2') return;
123 | sceneName = 'scene2';
124 | createScene();
125 | };
126 |
127 | return (
128 | <>
129 | {app.isApp && (
130 | <>
131 |
132 |
137 |
138 |
139 |
140 | >
141 | )}
142 | >
143 | );
144 | }
145 |
146 | export default App;
--------------------------------------------------------------------------------
/src/editor/sidePanel/inspector/Inspector.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { CoreComponentProps, RemoteObject } from '../types';
3 | // Components
4 | import './inspector.scss';
5 | import Accordion from '../Accordion';
6 | import InspectorField from './InspectorField';
7 | // Utils
8 | import { InspectCamera } from './utils/InspectCamera';
9 | import { InspectMaterial } from './utils/InspectMaterial';
10 | import { InspectTransform } from './utils/InspectTransform';
11 | import { InspectLight } from './utils/InspectLight';
12 | import InspectAnimation from './utils/InspectAnimation';
13 | import Transform from '../../../editor/tools/Transform';
14 | import { ToolEvents } from '../../../core/remote/RemoteThree';
15 |
16 | const defaultObject: RemoteObject = {
17 | name: '',
18 | uuid: '',
19 | type: '',
20 | visible: false,
21 | matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
22 | animations: [],
23 | material: undefined,
24 | perspectiveCameraInfo: undefined,
25 | orthographicCameraInfo: undefined,
26 | lightInfo: undefined,
27 | children: [],
28 | };
29 |
30 | export default function Inspector(props: CoreComponentProps) {
31 | const [currentObject, setCurrentObject] = useState(defaultObject);
32 |
33 | useEffect(() => {
34 | function onSelectItem(evt: any) {
35 | setCurrentObject(evt.value as RemoteObject);
36 | }
37 |
38 | function setScene() {
39 | setCurrentObject(defaultObject);
40 | }
41 |
42 | props.three.addEventListener(ToolEvents.CLEAR_OBJECT, setScene);
43 | props.three.addEventListener(ToolEvents.SET_SCENE, setScene);
44 | props.three.addEventListener(ToolEvents.SET_OBJECT, onSelectItem);
45 | return () => {
46 | props.three.removeEventListener(ToolEvents.CLEAR_OBJECT, setScene);
47 | props.three.removeEventListener(ToolEvents.SET_SCENE, setScene);
48 | props.three.removeEventListener(ToolEvents.SET_OBJECT, onSelectItem);
49 | };
50 | }, []);
51 |
52 | const objType = currentObject.type.toLowerCase();
53 | const hasAnimation = currentObject.animations.length > 0
54 | || currentObject['mixer'] !== undefined;
55 | const hasMaterial = objType.search('mesh') > -1
56 | || objType.search('line') > -1
57 | || objType.search('points') > -1;
58 |
59 | return (
60 | 0 ? (
66 |
70 | ) : undefined
71 | }
72 | >
73 |
74 | {currentObject.uuid.length > 0 && (
75 | <>
76 | {/* Core */}
77 | <>
78 |
85 |
92 |
99 | >
100 |
101 | {/* Data */}
102 | <>
103 | {/* Transform */}
104 |
105 | {/* Animations */}
106 | {hasAnimation ? : null}
107 | {/* Cameras */}
108 | {objType.search('camera') > -1 ? InspectCamera(currentObject, props.three) : null}
109 | {/* Lights */}
110 | {objType.search('light') > -1 ? InspectLight(currentObject, props.three) : null}
111 | {/* Material */}
112 | {hasMaterial ? InspectMaterial(currentObject, props.three) : null}
113 | >
114 | >
115 | )}
116 |
117 |
118 | );
119 | }
--------------------------------------------------------------------------------
/src/editor/sidePanel/inspector/utils/InspectTransform.tsx:
--------------------------------------------------------------------------------
1 | import { Euler, Matrix4, Vector3 } from 'three';
2 | import { Component, ReactNode } from 'react';
3 | import InspectorGroup from '../InspectorGroup';
4 | import { RemoteObject } from '../../types';
5 | import { setItemProps } from '../../utils';
6 | import MultiView from '../../../multiView/MultiView';
7 | import RemoteThree from '../../../../core/remote/RemoteThree';
8 | import { roundTo } from '../../../../utils/math';
9 |
10 | type InspectTransformProps = {
11 | object: RemoteObject;
12 | three: RemoteThree;
13 | }
14 |
15 | type InspectTransformState = {
16 | lastUpdated: number;
17 | expanded: boolean;
18 | }
19 |
20 | export class InspectTransform extends Component {
21 | static instance: InspectTransform;
22 |
23 | matrix = new Matrix4();
24 | position = new Vector3();
25 | rotation = new Euler();
26 | scale = new Vector3();
27 | open = false;
28 |
29 | constructor(props: InspectTransformProps) {
30 | super(props);
31 |
32 | const expandedValue = localStorage.getItem(this.expandedName);
33 | const expanded = expandedValue !== null ? expandedValue === 'open' : false;
34 | this.open = expanded;
35 | this.saveExpanded();
36 |
37 | this.state = {
38 | lastUpdated: 0,
39 | expanded: expanded,
40 | };
41 |
42 | // @ts-ignore
43 | this.matrix.elements = props.object.matrix;
44 | if (props.object.uuid.length > 0) {
45 | this.position.setFromMatrixPosition(this.matrix);
46 | this.rotation.setFromRotationMatrix(this.matrix);
47 | this.scale.setFromMatrixScale(this.matrix);
48 | }
49 |
50 | InspectTransform.instance = this;
51 | }
52 |
53 | update() {
54 | if (MultiView.instance) {
55 | const selectedItem = MultiView.instance.selectedItem;
56 | if (selectedItem === undefined) return;
57 | this.position.x = roundTo(selectedItem.position.x, 3);
58 | this.position.y = roundTo(selectedItem.position.y, 3);
59 | this.position.z = roundTo(selectedItem.position.z, 3);
60 | this.rotation.copy(selectedItem.rotation);
61 | this.scale.x = roundTo(selectedItem.scale.x, 3);
62 | this.scale.y = roundTo(selectedItem.scale.y, 3);
63 | this.scale.z = roundTo(selectedItem.scale.z, 3);
64 | this.setState({ lastUpdated: Date.now() });
65 | }
66 | }
67 |
68 | render(): ReactNode {
69 | return (
70 | {
107 | this.open = value;
108 | this.saveExpanded();
109 | }}
110 | />
111 | );
112 | }
113 |
114 | private updateTransform = (prop: string, value: any) => {
115 | const realValue = prop === 'rotation' ? { x: value._x, y: value._y, z: value._z } : value;
116 |
117 | // App
118 | this.props.three.updateObject(this.props.object.uuid, prop, realValue);
119 |
120 | // Editor
121 | const scene = this.props.three.getScene(this.props.object.uuid);
122 | if (scene) {
123 | const child = scene.getObjectByProperty('uuid', this.props.object.uuid);
124 | setItemProps(child, prop, realValue);
125 | }
126 | };
127 |
128 | private saveExpanded() {
129 | localStorage.setItem(this.expandedName, this.open ? 'open' : 'closed');
130 | }
131 |
132 | get expandedName(): string {
133 | return `${this.props.three.name}_transform`;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/types/editor/multiView/MultiView.d.ts:
--------------------------------------------------------------------------------
1 | import { Component, ReactNode } from 'react';
2 | import { Camera, Group, Object3D, OrthographicCamera, PerspectiveCamera, Scene, WebGLRenderer } from 'three';
3 | import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer';
4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
5 | import RemoteThree from '../../core/remote/RemoteThree';
6 | import InfiniteGridHelper from './InfiniteGridHelper';
7 | import { InteractionMode, MultiViewMode } from './MultiViewData';
8 | import './MultiView.scss';
9 | type MultiViewProps = {
10 | three: RemoteThree;
11 | scenes: Map;
12 | onSceneAdd?: (scene: Scene) => void;
13 | onSceneUpdate?: (scene: Scene) => void;
14 | onSceneResize?: (scene: Scene, width: number, height: number) => void;
15 | };
16 | type MultiViewState = {
17 | mode: MultiViewMode;
18 | modeOpen: boolean;
19 | renderModeOpen: boolean;
20 | interactionMode: InteractionMode;
21 | interactionModeOpen: boolean;
22 | lastUpdate: number;
23 | };
24 | export default class MultiView extends Component {
25 | static instance: MultiView | null;
26 | scene: Scene;
27 | renderer?: WebGLRenderer | WebGPURenderer | null;
28 | currentScene?: Scene;
29 | scenes: Map;
30 | cameras: Map;
31 | controls: Map;
32 | currentCamera: PerspectiveCamera | OrthographicCamera;
33 | currentWindow: any;
34 | helpersContainer: Group;
35 | grid: InfiniteGridHelper;
36 | private cameraHelpers;
37 | private lightHelpers;
38 | private interactionHelper;
39 | private currentTransform?;
40 | private splineEditor;
41 | private depthMaterial;
42 | private normalsMaterial;
43 | private uvMaterial;
44 | private wireframeMaterial;
45 | private playing;
46 | private rafID;
47 | private cameraControlsRafID;
48 | private width;
49 | private height;
50 | private tlCam;
51 | private trCam;
52 | private blCam;
53 | private brCam;
54 | private tlRender;
55 | private trRender;
56 | private blRender;
57 | private brRender;
58 | private cameraVisibility;
59 | private lightVisibility;
60 | private gridVisibility;
61 | selectedItem: Object3D | undefined;
62 | private debugCamera;
63 | private raycaster;
64 | private pointer;
65 | private cameraControls;
66 | private canvasRef;
67 | private containerRef;
68 | private tlWindow;
69 | private trWindow;
70 | private blWindow;
71 | private brWindow;
72 | private editorCameras;
73 | constructor(props: MultiViewProps);
74 | componentDidMount(): void;
75 | componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void;
76 | componentWillUnmount(): void;
77 | render(): ReactNode;
78 | private setupRenderer;
79 | private setupScene;
80 | private setupTools;
81 | play(): void;
82 | pause(): void;
83 | toggleOrbitControls(value: boolean): void;
84 | clear(): void;
85 | setGridVisibility(value: boolean): void;
86 | private update;
87 | private draw;
88 | private onUpdate;
89 | private enable;
90 | private disable;
91 | private resize;
92 | private addScene;
93 | private sceneUpdate;
94 | private removeScene;
95 | private addCamera;
96 | private removeCamera;
97 | private onMouseMove;
98 | private onClick;
99 | private onKey;
100 | private onSetSelectedItem;
101 | private updateSelectedItemHelper;
102 | private onUpdateTransform;
103 | private clearLightHelpers;
104 | private addLightHelpers;
105 | private createControls;
106 | private clearCamera;
107 | private killControls;
108 | private assignControls;
109 | private updateCamera;
110 | private updateCameraControls;
111 | private clearControls;
112 | private saveExpandedCameraVisibility;
113 | private saveExpandedLightVisibility;
114 | private saveExpandedGridVisibility;
115 | private getSceneOverride;
116 | private drawTo;
117 | private drawSingle;
118 | private drawDouble;
119 | private drawQuad;
120 | get appID(): string;
121 | get mode(): MultiViewMode;
122 | get three(): RemoteThree;
123 | get expandedCameraVisibility(): string;
124 | get expandedLightVisibility(): string;
125 | get expandedGridVisibility(): string;
126 | }
127 | export {};
128 |
--------------------------------------------------------------------------------
/src/example/three/CustomShaderMaterial.ts:
--------------------------------------------------------------------------------
1 | import { Color, Euler, Matrix3, Matrix4, ShaderMaterial, Texture, Vector2, Vector3, Vector4 } from 'three';
2 | import { textureFromSrc } from '../../editor/sidePanel/utils';
3 |
4 | const vertex = `varying vec2 vUv;
5 |
6 | void main() {
7 | vUv = uv;
8 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
9 | }`;
10 |
11 | const fragment = `struct Light {
12 | float intensity;
13 | vec3 position;
14 | vec3 color;
15 | };
16 |
17 | uniform float time;
18 | uniform float opacity;
19 | uniform vec2 resolution;
20 | uniform vec3 diffuse;
21 | uniform vec3 mouse;
22 | uniform sampler2D map;
23 | uniform Light light;
24 | uniform Light lights[3];
25 | varying vec2 vUv;
26 |
27 | #define MIN_ALPHA 2.0 / 255.0
28 |
29 | void main() {
30 | if (opacity < MIN_ALPHA) discard;
31 | vec2 imageUV = vUv * 10.0;
32 | vec3 image = texture2D(map, imageUV).rgb;
33 | vec3 col = image * diffuse;
34 | col += (sin(time * 0.1) * 0.5 + 0.5) * 0.2;
35 | col += vec3(resolution, 0.0);
36 | col += mouse;
37 | gl_FragColor = vec4(col, opacity);
38 | }`;
39 |
40 | // eslint-disable-next-line max-len
41 | const smile = `data:image/png;base64,R0lGODlhCAAIAIAAAP///wAAACH/C1hNUCBEYXRhWE1QPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OTc3NywgMjAyMy8wNi8yNS0yMzo1NzoxNCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjIgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NjVENjQyODM4QTYzMTFFRUJFQTBFOTJFNjA1OTQ5N0YiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NjVENjQyODQ4QTYzMTFFRUJFQTBFOTJFNjA1OTQ5N0YiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo2NUQ2NDI4MThBNjMxMUVFQkVBMEU5MkU2MDU5NDk3RiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2NUQ2NDI4MjhBNjMxMUVFQkVBMEU5MkU2MDU5NDk3RiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PgH//v38+/r5+Pf29fTz8vHw7+7t7Ovq6ejn5uXk4+Lh4N/e3dzb2tnY19bV1NPS0dDPzs3My8rJyMfGxcTDwsHAv769vLu6ubi3trW0s7KxsK+urayrqqmop6alpKOioaCfnp2cm5qZmJeWlZSTkpGQj46NjIuKiYiHhoWEg4KBgH9+fXx7enl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmFgX15dXFtaWVhXVlVUU1JRUE9OTUxLSklIR0ZFRENCQUA/Pj08Ozo5ODc2NTQzMjEwLy4tLCsqKSgnJiUkIyIhIB8eHRwbGhkYFxYVFBMSERAPDg0MCwoJCAcGBQQDAgEAACH5BAAAAAAALAAAAAAIAAgAAAIOjH8AprzRkHyspqSoNAUAOw==`;
42 |
43 | export default class CustomShaderMaterial extends ShaderMaterial {
44 | constructor() {
45 | super({
46 | vertexShader: vertex,
47 | fragmentShader: fragment,
48 | name: 'ExampleScene/SimpleShader',
49 | transparent: true,
50 | uniforms: {
51 | diffuse: {
52 | value: new Color(0xffffff)
53 | },
54 | opacity: {
55 | value: 1,
56 | },
57 | time: {
58 | value: 0,
59 | },
60 | map: {
61 | value: null,
62 | },
63 | resolution: {
64 | value: new Vector2(),
65 | },
66 | mouse: {
67 | value: new Vector3()
68 | },
69 | v4: {
70 | value: new Vector4()
71 | },
72 | euler: {
73 | value: new Euler()
74 | },
75 | testM3: {
76 | value: new Matrix3()
77 | },
78 | testM4: {
79 | value: new Matrix4()
80 | },
81 | light: {
82 | value: {
83 | position: new Vector2(5, 10),
84 | intensity: 1,
85 | color: new Color(0xff00ff),
86 | }
87 | },
88 | lights: {
89 | value: [
90 | {
91 | position: new Vector2(1, 1),
92 | intensity: 1,
93 | color: new Color(0xff0000),
94 | },
95 | {
96 | position: new Vector2(2, 2),
97 | intensity: 2,
98 | color: new Color(0x00ff00),
99 | },
100 | {
101 | position: new Vector2(3, 3),
102 | intensity: 3,
103 | color: new Color(0x0000ff),
104 | },
105 | ],
106 | },
107 | },
108 | });
109 |
110 | textureFromSrc(smile).then((texture: Texture) => {
111 | this.uniforms.map.value = texture;
112 | });
113 | }
114 |
115 | update(delta: number) {
116 | this.uniforms.opacity.value = this.opacity;
117 | this.uniforms.time.value += delta;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------