About
Hello, my name is Kryštof and i did a research project about Needle. At our company, we wanted to determine how Needle can help us in our workflow. We have one local client which focuses on reselling luxury cars. We already delivered a mobile app and VR experience using Unity. We have around 30 unique cars ready in the engine. We plan to expand the client's website with visually pleasing digital clones with more configuration options. Needle could achieve a perfect 1:1 conversion between unity and web visuals. It would be a massive benefit to our workflow. So that's what sparked our research.
Context
I'm not very well experienced with javascript, typescript or three.js, so my point of view is as a semi-experienced Unity developer trying out the simplest way how to create a web experience. For those who would suggest Unity WebGL, that sadly doesn't work and isn't flexible on mobile browsers. Needle is 💚
Lighting
Our lighting model is based on reflection probes in unity. We do not need any directional or point lights, only ambient lighting.
We're using this skybox:
Which looks like this on the paint job:
Then to add a slight detail, i've added 2 directional lights with an insignificant intensity (0.04) to create specular highlights. So before it looked like this:
But with the added directional lights it added a better dynamic. The effect could be deepened with higher intensity:
Background
The scene now looks like this:
The black background isn't very pretty. So to differentiate between visual and lighting skyboxes i've added an inverse sphere which wraps the whole map.
Regarding the gradient goes from a slight gray to a white color..
This effect could be easily made with just a proper UV mapping and a single pixel high texture which would define the gradient.
I've made an unlit shader in the shader graph:
I've noticed a color banding issue, so i've tried to implement dithering. Frankly, it didn't help the artefacts but i bet there's a simple solution to that issue. So the upper part of the shader does sample the gradient based on the Y axis in object space. And the lower part tries to negate the color banding.
By using shaders it's simpler to use and iterate the gradiant. By using Needle's Shadergraph markdown asset, it's even simpler! 🌵
Car fake movement
The scene right now is static since nothing moves. We can negate that by adding a fake feeling of motion. Let's start by adding motion to the wheels.
With a simple component called Rotator, we define an axis and speed along it.
import { class Behaviour
Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
Derive from
Behaviour
to implement your own using the provided lifecycle methods.
Components can be added to threejs objects using [addComponent](addComponent)
or [GameObject.addComponent](GameObject.addComponent)
The most common lifecycle methods are awake
, start
, onEnable
, onDisable
update
and onDestroy
.
XR specific callbacks include onEnterXR
, onLeaveXR
, onUpdateXR
, onControllerAdded
and onControllerRemoved
.
To receive pointer events implement onPointerDown
, onPointerUp
, onPointerEnter
, onPointerExit
and onPointerMove
.
Behaviour, const serializable: <T>(type?: Constructor<T> | TypeResolver<T> | (TypeResolver<T> | Constructor<any>)[] | null | undefined) => (_target: any, _propertyKey: string | {
name: string;
}) => void
The serializable attribute should be used to annotate all serialized fields / fields and members that should be serialized and exposed in an editor
serializable } from "@needle-tools/engine";
export enum enum RotationAxis
RotationAxis {
function (enum member) RotationAxis.X = 0
X, function (enum member) RotationAxis.Y = 1
Y, function (enum member) RotationAxis.Z = 2
Z
}
export class class Rotator
Rotator extends class Behaviour
Needle Engine component base class. Component's are the main building blocks of the Needle Engine.
Derive from
Behaviour
to implement your own using the provided lifecycle methods.
Components can be added to threejs objects using [addComponent](addComponent)
or [GameObject.addComponent](GameObject.addComponent)
The most common lifecycle methods are awake
, start
, onEnable
, onDisable
update
and onDestroy
.
XR specific callbacks include onEnterXR
, onLeaveXR
, onUpdateXR
, onControllerAdded
and onControllerRemoved
.
To receive pointer events implement onPointerDown
, onPointerUp
, onPointerEnter
, onPointerExit
and onPointerMove
.
Behaviour {
//@type RotationAxis
@serializable<unknown>(type?: Constructor<unknown> | TypeResolver<unknown> | (Constructor<any> | TypeResolver<unknown>)[] | null | undefined): (_target: any, _propertyKey: string | {
...;
}) => void
The serializable attribute should be used to annotate all serialized fields / fields and members that should be serialized and exposed in an editor
serializable()
Rotator.axis: RotationAxis
axis : enum RotationAxis
RotationAxis = enum RotationAxis
RotationAxis.function (enum member) RotationAxis.X = 0
X;
@serializable<unknown>(type?: Constructor<unknown> | TypeResolver<unknown> | (Constructor<any> | TypeResolver<unknown>)[] | null | undefined): (_target: any, _propertyKey: string | {
...;
}) => void
The serializable attribute should be used to annotate all serialized fields / fields and members that should be serialized and exposed in an editor
serializable()
Rotator.speed: number
speed : number = 1;
Rotator.update(): void
regular callback in a frame (called every frame when implemented)
update() {
const const angle: number
angle = this.Rotator.speed: number
speed * this.Component.context: Context
Use the context to get access to many Needle Engine features and use physics, timing, access the camera or scene
context.Context.time: Time
access timings (current frame number, deltaTime, timeScale, ...)
time.Time.deltaTime: number
The time in seconds it took to complete the last frame (Read Only).
deltaTime;
switch(this.Rotator.axis: RotationAxis
axis) {
case enum RotationAxis
RotationAxis.function (enum member) RotationAxis.X = 0
X:
this.Component.gameObject: GameObject
the object this component is attached to. Note that this is a threejs Object3D with some additional features
gameObject.Object3D<Object3DEventMap>.rotateX(angle: number): GameObject
Rotates the object around x axis in local space.
rotateX(const angle: number
angle);
break;
case enum RotationAxis
RotationAxis.function (enum member) RotationAxis.Y = 1
Y:
this.Component.gameObject: GameObject
the object this component is attached to. Note that this is a threejs Object3D with some additional features
gameObject.Object3D<Object3DEventMap>.rotateY(angle: number): GameObject
Rotates the object around y axis in local space.
rotateY(const angle: number
angle);
break;
case enum RotationAxis
RotationAxis.function (enum member) RotationAxis.Z = 2
Z:
this.Component.gameObject: GameObject
the object this component is attached to. Note that this is a threejs Object3D with some additional features
gameObject.Object3D<Object3DEventMap>.rotateZ(angle: number): GameObject
Rotates the object around z axis in local space.
rotateZ(const angle: number
angle);
break;
}
}
}
The user now sees a car driving in deep nothingness, the color doesn't resemble anything and the experience is dull. We want to ground the model and that's done by adding a grid and then shifting it so it seems the car is moving. This is what we want to achieve:
The shader for the grid was comprised of two parts. A simple tiled texture of the grid that's being multipled by a circular gradient to make the edges fade off.
Extra elements
This tech demo takes it's goal to showcase the car's capabilities.
Let's start by highlighting the wheels.
Adding this shader to a plane will result in a dashed circle which is rotating by a defined speed. Combined with world space UI with a normal Text component this can highlight some interesting capabilities or parameters of the given product.
After showcasing the wheels we want to finish with a broad information about the product. In this case, that would be the car's full name and perhaps some available configurations.
Wrap up
By using the Unity's timeline we can control when the wheel dashes and text will be shown. This is complemented by the camera animation.
Conclusion
Needle Engine seems to be a very good candidate for us!
There are a few features which we miss.
That would be for example proper support for the Lit Shader Graphs. But nothing stops us to create shaders the three.js way and create simmilar shaders in Unity for our content team to tweak the materials.
Using Needle was a blast! 🌵