Sorry folks, I moved to a new host recently and managed to break the site. Normal service will be resumed as soon as I’ve figured out what I’ve done.
MS V-Chat Worlds Rediscovered
Sorry folks, I moved to a new host recently and managed to break the site. Normal service will be resumed as soon as I’ve figured out what I’ve done.
I got to thinking that, short of a real multi-user environment, it would add interest to have some of the original avatars in the worlds – they could even wander about. I’ve done a few experiments and it’s starting to look fairly close to the original:
(apologies to JamesC for nicking his name, but I wanted a close comparison with one of the few clear images from the original application to survive on the www:
This turned out to be rather trickier than first expected or rather it is tricky to maintain the pixelated look. The Century Gothic font isn’t quite the same – maybe a bit of stretching is in order.
Also I couldn’t recall whether the whole avatar should have a billboard so it always faces the viewer, or just the floating name, or neither. Since there is a back view in the original avatar frames, I opted for having just the name billboarded. Ultimately I’ll animate the avatars.
The V-Chat worlds all automatically play their background sound loops. These were included in the originals as song1.wav and often song2.wav. After trying a few options I settled on using howler.js to play these sounds, which has proven really easy to use and works across every platform I’ve tested it on.
Today I added the original sound effects to the worlds. These are enter.wav and exit.wav, which used to play whenever anyone entered or left the chat room, and collide.wav which played whenever you bumped into the boundary of the world.
The entry and collision sounds react as you’d expect, but since there are no other users present they now play randomly.
I also reduced the volume of the background loops in several of the world, as they can get a little annoying!
Most of the V-Chat worlds remained the same between the different versions of V-Chat – the original MSN/v1.1, the v2.0beta and the final v2.0 release. However, a few of the worlds were changed between releases.
Compass had a very minor visual change between v1.1 and v2.0beta, which replaced the Practice World billboard image on the grey spike with a Lobby image.
v2.0beta introduced Homespace and Lobby, both of which changed for the final v2.0 release. Both worlds had the look of their portals changed. Homespace also got new sounds.
Despite the changes being very minor, I’ve added these alternative versions of Compass, Homespace and Lobby just to complete the collection.
After a couple of breakthroughs on converting to X3D, I have now published all of the Microsoft-released V-Chat worlds that I have. Rendering is as true to the originals as I have been able to achieve. It is particularly pleasing is when imperfections in the originals carry through to the conversions!
There are still a few issues here and there which I’ll mop up in slower time. It’s taken 18 years to get this far, so I’m in no hurry.
When I started this the best available technology was VRML97. My workflow went through converting the WDB file to VRML 1.0 with a homebrew VB utility (since all WDB transforms are in 4×4 matrices and VRML can swallow those without conversion), and using a utility to convert that to VRML97 with decomposed transforms. After leaving the project on the back burner for years X3D had matured and X3DOM had come along, so I started up again with a second stage of conversion. Ultimately I decided it was best to learn the maths to decompose the matrices and go straight from WDB to X3D. The rest was a matter of mainly trial and error to get the rendering reasonably close.
I realise that few people can remember these worlds, and very few people are interested in seeing them again. For my part the value has been in the challenge of puzzling it all out and in what I have learned on the way.
This covers the last of the structures found in the world.wdb files, which I call VCLight, though until quite recently it was called VCMystery. I’m not entirely sure it’s about lighting, I just can’t think of anything else it might be. It has the following structure:
Structure VCLight b0 As Byte 'unidentified byte, &H00, &H01, &H03, &H04 b1 As Byte 'unidentified byte, always &H00 b2 As Byte 'unidentified byte b3 As Byte 'unidentified byte b4 As Byte 'unidentified byte b5 As Byte 'unidentified byte b6 As Byte 'unidentified byte b7 As Byte 'unidentified byte, always &HFF s0 As Single 'unidentified IEEESP, always 1.0 s1 As Single 'unidentified IEEESP, always 0.0 s2 As Single 'unidentified IEEESP, always 0.0 s3 As Single 'unidentified IEEESP s4 As Single 'unidentified IEEESP End Structure
b0 is either &H00, &H01, &H03 or &H04.
b1 is always &H00.
B4, b5 and b6 usually have the same value, indicating that they might be colours.
b7 is always &HFF.
s0, s1, s2 are always (1.0, 0.0, 0.0), so look like a 3d vector.
s3, s4 are almost always (0.5, 0.4), except on three occasions:
These odd values always coincide with b0=3, though most b0=3 VCLight structures have the usual (0.5, 0.4) s3 and s4 values. They are indicated by the red digits in the table below.
There are clues in the V-Chat SDK document, which mentions “Microsoft Reality Labs general purpose rendering engine” and “RenderMorphics”. RenderMorphics Reality Lab was a 3D graphics API that was bought by Microsoft in 1995 and formed the basis for Direct3D, which first appeared as part of the DirectX 2.0 SDK. DirectX 2.0 implemented different types of light (Ambient, Directional, Parallel Point, Point and Spotlight). Trawling through the old SDK documentation unearths these _D3DRMLIGHTTYPE enumerations:
D3DRMLIGHT_AMBIENT = 0 D3DRMLIGHT_POINT = 1 D3DRMLIGHT_SPOT = 2 D3DRMLIGHT_DIRECTIONAL = 3 D3DRMLIGHT_PARALLELPOINT = 4
Its also worth noting that the VRML 1.0 standard that was around at the time had similar types of light node (DirectionalLight, PointLight and SpotLight).
As a starting point I am going to assume that these five basic types might have found their way into the .wdb file, though not necessarily with the same values.
Assuming b0 indicates the type of light, the number of each type appearing in each world is shown in the table:
Most worlds have just a single b0=0 light, and there is never more than one. Not only that, but it always lives in the first VCNode under an identity matrix, so has neither position nor location. This makes it a good candidate for an Ambient light. Only cartooncity, compass, hanami and lodge don’t have one. This is almost a fit with the VCTexDef.txb4 = &H01 worlds, with the addition of compass – though this is also the only world with a b0=1 light. So maybe this hints at some correspondence between material qualities and lighting type.
The b0=1 light in compass sits under what is in essence a translation matrix (it actually incorporates some axis-rearranging rotations, but there’s a few like that in compass). This is consistent with this being a Point or Parallel Point light i.e. its position matters, but not orientation.
The b0=3 lights sit beneath matrices with both translation and rotation. If both position and orientation matter, then this would indicate a Spot light. However, poking values into WDB files makes this look more like a DirectionalLight. In this implementation it has a position, but one that seems to be ignored by the renderer.
The b0=4 lights sit beneath translation matrices. Like the b0=1 light, this is consistent with this being a Point or Parallel Point light. There’s nothing in any of the world.wdb files that would allow the two to be distinguished. You could argue that parallel point lights are less computationally intensive, so might be used in preference to point lights. That would make b0=1 a Point light and b0=4 a Parallel Point light.
This is as far as I have got to date, but is enough to experiment with.
In the previous post I described the overall structure of the .wdb file format, including the file header, the collision fence and the scene node hierarchy. This time I will go into the VCGeometry structure.
Structure VCGeometry numPoints As UInt16 'number of 3D points in this geometry points() As VCPoint 'series of numPoints VCPoint structures numNormals As UInt16 'number of normals in this geometry normals() As VCNormal 'series of numNormals VCNormal structures numPolygons As UInt16 'number of polygons in this geometry lenIFS As UInt16 'number of bytes in the Indexed Face/Line Set polyDefs() As VCPolyDef 'series of numPolygons VCPolyDef structures finalIFS As UInt16 'final word of Indexed Face/Line Set (always 0000) w6 As UInt16 'render flags texCoords() As VCTexCoord 'series of numPoints texture coordinates ffs() As VCFFFlags 'series of numPoints 4-byte flag sets numTexDefs As Byte 'number of texture definitions to follow texDefs() As VCTexDef 'series of numTexDefs VCTexDef structures materials() As UInt16 'series of numPolygons material indices End Structure
The VCGeometry starts by defining a list of numPoints 3D points, held in the file as a series of VCPoint structures:
Structure VCPoint x As Single y As Single z As Single End Structure
Next comes a list of numNormals 3D vectors, held in the file as a series of VCNormal structures:
Structure VCNormal x As Single y As Single z As Single End Structure
The geometry is defined in the form of an Indexed Face Set, though a later flag allows the same data to be interpreted as an Indexed Line Set. The following description assumes the IFS mode.
numPolygons defines how many polygons (or line segments) are defined in the IFS. The lenIFS value is the length of the IFS table in bytes, allowing for faster parsing of the file (i.e. removing the need to unpack the whole IFS to find its end, for example allowing the correct amount of memory to be allocated before reading the IFS.
Structure VCPolyDef numVerts As UInt16 'number of vertices in this polygon vertices() As UInt16 'series of numVerts indices into points table normals() As UInt16 'series of numVerts indices into normals table End Structure
Each VCPolyDef structure defines a single polygon as a series of indices into the points() and normals() lists. The normals are therefore vertex normals – the renderer calculates face normals from the point data (points are in clockwise order when viewing the front of the face).
A zero word always follows the IFS.
The next word (w6) is at least partially a mystery. Across the set of published .wdb files this adopts one of four values: 0, 256, 768 or 1024.
w6=0 is he rarest, coinciding with “wirexx” nodes in the tabletop and lunarislands worlds (for the flower stems and walkway red edges respectively). This indicates that the geometry data should be rendered as an indexed line set.
w6=256 is by far the most common value. This seems to coincide in all cases with geometry that has a texture image.
w6=768 is used in VCGeometry nodes in outerworld, mall, lodge, lobby, homespace, hutchspace, hanami, and cartooncity and seems to be associated with flat-shaded geometry that does not use a texture image, though this is to be confirmed.
w6=1024 is used in VCGeometry nodes in compass, eurostadium, fishbowl, hanami, help, kivaunderground, lavalovelounge, littlehouse, lunarislands, nshof, paradiseisland and tabletop and appears to be associated with smooth-shaded objects that do not use texture images – again to be confirmed.
In practice, these last two values determine whether vertex normals or computed face normals are used by the renderer.
Texture coordinates are defined in texCoords(), which is a list of numPoints VCTexCoord structures. Each holds a (u,v) texture coordinate:
Structure VCTexCoord u As Single 'normalised and scaled texture u coordinate v As Single 'normalised and scaled texture v coordinate End Structure
These are normalised and scaled to repeat the texture in both u- and v-directions.
The texCoords are followed by four bytes of &HFF.
Textures and materials are defined by a series of numTexDefs VCTexDef structures. These define an RGB colour, most also include the name of a texture image, and there are a couple of other bytes which have an unknown function:
Structure VCTexDef txBlue As Byte 'face colour (blue component) txGreen As Byte 'face colour (green component) txRed As Byte 'face colour (red component) txb3 As Byte 'unknown function, always &HFF txName As String 'file name of texture image txb4 As Byte 'unknown function End Structure
txb4 is either &H00 or &H01. The &H01 value occurs much less often and only for (all) VCTexDef structures in cartooncity, hanami and lodge. They include VCTexDefs both with and without texture images, and include animated and non-animated items, so the purpose of this remains unclear. My guess at present is that this flags textures or materials that are not subject to lighting – more on this later.
Next comes a list of numPolygons material indices defining which material should be applied to each polygon. There is an oddity here in that numTexDefs is a Byte, but the members of materials() are UINT16. These nearly always range between 0 and 4, but there are a small number of entries in lodge that have the value 256 (associated with the wooden pillars). This indicates that perhaps the lower order byte identifies the material and the high order byte contains flags of unknown purpose.
That concludes the VCGeometry structure. Next time VCLight.
V-Chat worlds are held in a proprietary format that to my knowledge has never been publicly released. These are my notes on the .wdb file format, for anyone that might find them useful.
I figured most of this out after many hours detective work with a hex editor, mapping the file out starting with obvious text strings, common floating point values and gradually building up a picture through comparisons between the available .wdb files. At the time I assumed that it would approximate some of the VRML 1.0 elements (in content, if not in structure). Since then I’ve also compared with early versions of DirectX, which also helped. There are still a few mystery areas, but so far these haven’t prevented a reasonable reproduction of the worlds.
Defined here in pseudo-vb, the basic file structure is:
Structure VCWorld header As VCHeader 'the header fence As VCFence 'the fence numNodes As Integer 'how many VCNodes follow nodes() As VCNode 'a series of numNodes VCNode structures End Structure
The file starts with a 12 byte VCHeader:
Structure VCHeader id() As Byte '4 bytes, should be characters WDBV version As Byte 'always &H04 b0 As Byte 'unidentified byte, always &H00 b1 As Byte 'unidentified byte, always &H00 b2 As Byte 'unidentified byte, always &H00 bgBlue As Byte 'background colour blue bgGreen As Byte 'background colour green bgRed As Byte 'background colour red b3 As Byte 'unidentified byte, always &HFF End Structure
This identifies the file format and (I assume) format version and sets an RGB8 value for the world background. There are three bytes (b0, b1 and b2) that I can’t decipher, though they may simply be part of the version number. Also byte b3 has an unidentified purpose.
Next comes a variable length VCFence section defining what I call ‘the fence’:
Structure VCFence numBorderDefs As UInt16 'number of line segments in the fence borderDefs() As VCBorderDef 'a series of numBorderDefs VCBorderDef structures yMax As Double yMin As Double End Structure Structure VCBorderDef xCoord0 As Double zCoord0 As Double xCoord1 As Double zCoord1 As Double End Structure
The fence defines of the limits of travel within the world. It consists of a series of line segments defined by numBorderDefs (x0, z0), (x1, z1) pairs, followed by two numbers defining the minimum and maximum y coordinates of the viewer. The line segments may not be crossed, and form a collision boundary. No doubt this simplified geometry was necessary to reduce the processor loading back in 1995.
The simplest fence is in hanami, where numBorderDefs is zero, and the max and min y values are almost the same value. This results in a world where you can’t rise or fall, but there is no limit to travel otherwise. The fence for compass is similar, but allows plenty of vertical travel. At the other end of the scale there is hutchspace, which has 29 line segments defining its fence along with the vertical limits.
These are the only double precision numbers in the file format, so far as I can tell.
The world geometry is then defined by a series of VCNode structures:
Structure VCNode blockName As String 'name of the block, or none transform() As Single '4x4 transform matrix animation As VCAnimation 'Animation parameters numGeometries As UInt16 'number of VCGeometry structures geometries() As VCGeometry 'A series of numGeometries VCGeometry structures numLights As UInt16 'number of VCLight structures lights() As VCLight 'a series of numLights VCLight structures numChildren As UInt16 'number of child nodes End Structure
This starts with a string defining the node name, the first byte of which defines the number of following bytes in the string (i.e. null strings are simply an &H00 byte).
Next comes 16 single precision values which define a 4×4 transformation matrix that is applied to the geometry in this node and to all its child nodes.
The VCAnimation structure contains seven single precision values to control animation, which is limited to rotation about a single axis.
Structure VCAnimation originX As Single 'rotation centre x coord (?) originY As Single 'rotation centre y coord (?) originZ As Single 'rotation centre z coord (?) axisX As Single 'rotation axis vector x component axisY As Single 'rotation axis vector y component axisZ As Single 'rotation axis vector z component speed As Single 'rotation speed End Structure
The first three are always [0.0, 0.0, 0.0]. I am guessing that these are a rotation origin offset that was never used.
The next three are either [0.0, 0.0, 0.0] or [0.0, 1.0, 0.0]. I have taken this to be a vector defining a rotation axis. Where the axis is [0.0, 0.0, 0.0], the speed value is always 0.0 i.e. no animation. Where it is [0.0, 1.0, 0.0] the speed value is non-zero (except for certain non-animated parts of hutchspace, lodge, standingstone and waterhole).
These non-zero values all coincide with animated elements that have a node name of the form “SPINxxxx” or “spinxxxx” where xxxx is a number. The the speed value in radians equals the xxxx value in tenths of a degree. In practice these range between 0.1 and 7.0 degrees and between 345.0 and 359.9 degrees (i.e. -0.1 and -15.0 degrees), indicating rotation speeds in clockwise and anticlockwise directions.
The V-Chat SDK 1.0 Beta document (which unfortunately does not define the .wdb file format!) has this to say about spinning animation:
“Naming, speed of spin, degrees of rotation. The degrees of rotation (determined by the content provider) of the spin will occur at a constant speed, determined by the processor speed of the end user’s machine. Content providers cannot specify a speed of rotation, and user with different processor powers will see spinning objects move at different rates. Spin objects must be named: SPINXXXX, where XXXX = degree of rotation * 110 per frame about a vector which is central to the object and pointing vertically up. e.g. SPIN3550 = -5 degrees rotation.”
Next comes numGeometries, defining how many VCGeometry structures follow, then the VCGeometry structures themselves. In practice, this is either 0 or 1.
Following that is numLights, defining how many VCLight structures follow, then the VCLight structures themselves. This too is either 0 (usually, but not always, when numGeometries = 1) or 1 (when numGeometries = 0). Hence in practice a VCNode contains either a single VCGeometry, a single VCLight structure, or neither (though the file format seems to be able to support multiple instances).
The final value numChildren defines how many child VCNodes belong to the current VCNode. There is no referencing or linking within the file. The VCNode hierarchy is defined by this number and by the order of VCNodes in the file, each of which may also have child VCNodes.
That covers the file header, fence and basic structure of the file. Next time I will delve into the VCGeometry and VCLight structures.
I wanted to preserve use of the original texture images with their vertically stacked animation frames, but also to have the animation work in a standalone x3d file that is inlined into the web page.
V-Chat geometry is stored in world.wdb files which very usefully include the names of animated textures in the form AnimXXYY, where XX is the number of animation frames in the texture and YY is the frame duration in 100ths of a second e.g. Anim0430 indicates a 4 frame animation, each frame held for 0.3 seconds.
The first step is to add a TextureTransform node to the animated texture image that scales it by a factor of XX on the y axis (which means a scale factor of 1/XX):
<TextureTransform DEF='ttAnim0430' scale='1 0.25' translation='0 0' />
Next a TimeSensor node is added that repeats at the right rate. In this case 4 frames at 0.3 seconds needs a 1.2 second cycleInterval:
<TimeSensor DEF='tsAnim0430' cycleInterval='1.2' loop='true' />
The TimeSensor output gets turned into the required texture translation coordinates by using a PositionInterpolator. This outputs a three dimensional coordinate (an SFVec3f), but it turns out that this can be routed to the two dimensional translation attribute (an SFVec2F) with no ill effects provided each keyValue is given as an x y z triple. (Lesson learned: much time wasted messing around with a CoordinateInterpolator, which doesn’t work). The example here needs:
<PositionInterpolator id='piAnim0430' DEF='ciAnim0430' key='0 0.25 0.25 0.5 0.5 0.75 0.75 1' keyValue='0 0 0 0 0 0 0 1 0 0 1 0 0 2 0 0 2 0 0 3 0 0 3 0'/>
Finally, a couple of ROUTE nodes plumb everything together:
<ROUTE fromNode='tsAnim0430' fromField='fraction_changed' toNode='piAnim0430' toField='set_fraction'/> <ROUTE fromNode='piAnim0430' fromField='value_changed' toNode='ttAnim0430' toField='translation'/>
Preview of Compass added, plenty of issues to address though: navigation, texture animation, geometry animation, rendering fidelity, links, portals and sound.
Update: Navigation fixed – Viewpoint orientation had negative y value. Added Ctrl-drag for strafe and raise/lower. Trapped a potential divide by zero in the moveIt() function.