TUserCamera


Croquet-Teapot

Comment:

TUserCamera is a TCamera that is used by the user to manipulate his position in space and render same.

Hierarchy:

ProtoObject
Object
TObject
TFrame
TGroup
TCamera
TUserCamera

Summary:

instance variables:

avatar buttonsOverlay cameraButton chatObject debugOverlay doRender downPointer lastRenderTime morphicOverlay mouseDownTime overlaySpace overlays phone pointer rearCam redButtonPressed remoteControl renderEstimate renderInterval thirdPerson thirdPersonDelta yellowButtonPressed

methods:

instance class
CIM accessing avatar chat drag and drop driving
events initialize morph import painter popUp remoteControl render transform window
no messages

Detail:

instance variables:

avatar
buttonsOverlay
cameraButton
chatObject
debugOverlay
doRender
downPointer
lastRenderTime
morphicOverlay
mouseDownTime
overlaySpace
overlays
phone
pointer
rearCam
redButtonPressed
remoteControl
renderEstimate
renderInterval
thirdPerson
thirdPersonDelta
yellowButtonPressed

instance methods:

CIM
requestMeetingAt: transform in: space from: sender

	(self confirm: sender nickname,' has invited you.
Do you accept the invitation?') ifFalse:[^self].
	avatar future: 0.0 perform:#goToPortal:transform: withArguments: {space . transform}.

accessing
addOverlay: ov


	overlays add: ov.
avatar

	^avatar
avatar: aFrame

	avatar _ aFrame.
	pointer avatar: aFrame.
croquetPlace

	"the croquetPlace of the space I'm in"
	^self root teaParty croquetPlace.
debugOverlay

	^debugOverlay
downPointer


	^ downPointer.
forwardOnY


	^ avatar forwardOnY.
"	^ forwardOnY."
forwardOnY: foy


	avatar forwardOnY: foy.
"	forwardOnY _ forwardOnY + foy.
	self frameChanged."
morphicOverlay

	^morphicOverlay
pointer


	^ pointer.
pointer:pntr


	pointer _ pntr.
removeOverlay: ov


	overlays remove: ov.
setDoRender: bool


	doRender _ bool.
thirdPerson


	^ thirdPerson.
thirdPerson: cbool


	thirdPerson_cbool.

avatar
clickOpenAvatarFile: avatarPath


	avatar loadAvatar: avatarPath.

chat
chatObject

	chatObject ifNotNil:[^chatObject].
	chatObject := TVoiceRecorder new.
	^chatObject
chatStatus

	^self chatObject statusString
endChat

	chatObject ifNotNil:[self isChatting ifTrue:[chatObject stopRecording]].
endChat: ptr

	chatObject ifNotNil:[self isChatting ifTrue:[chatObject stopRecording]].
isChatting

	^chatObject notNil and:[chatObject isRecording]
startChat

	self chatObject startRecording.
startChat: ptr

	self chatObject startRecording.

drag and drop
acceptDroppingMorph: aMorph event: anEvent
 
	| projName |
	(aMorph isKindOf: ProjectViewMorph)
		ifTrue: [projName _ aMorph project name.
			self makeLinkToProject: projName.
			^ self].
	(aMorph isKindOf: TSnapshot)
		ifTrue: [self makePortal: aMorph.
			^ self].
	"any other kind of morph we should make a local project for"
	(aMorph isKindOf: Morph)
		ifTrue: [self makeLocalProjectWithMorph: aMorph.
		^self].

events
blueButtonDown: anEvent

	| selected |
	selected := pointer selectedObject.
	(selected notNil and:[selected wantsBlueButton]) ifTrue:[
		pointer pointerDown: anEvent.
		].
blueButtonUp: anEvent

	| selected |
	selected := pointer selectedObject.
	(selected notNil and:[selected wantsBlueButton]) ifTrue:[
		pointer pointerUp: anEvent. ]
dropFiles: aFileStream event: evt

	pointer dropFiles: aFileStream event: evt.

	
goToSnapshot: item


	item verifyProperlyVeiled.	
	avatar future: 50.0 perform: #goToPortal:transform: withArguments: { item root . item globalTransform }.
	self killFrame: true.
gotoSpace: aSpace transform: aTransform

	avatar future: 50.0 perform: #goToPortal:transform: withArguments: { aSpace . aTransform }.
	self killFrame: true.
keyDown: anEvent

	pointer keyDown: anEvent.

	
keyStroke: anEvent

	anEvent keyValue = 1 ifTrue: [ thirdPersonDelta _ (thirdPerson cNot)-(thirdPerson cNotNot) * 0.1. ^ self.].
	anEvent keyValue = 2 ifTrue: [ self halt. ].
	anEvent keyValue = 4 ifTrue:[ self snapshot. ^self].
	anEvent keyValue = 18 ifTrue:[ self switchMirror. ^ self.].
	anEvent keyValue = 19 ifTrue:[
		self root ambientSoundPlaying ifTrue:[
			self root stopAmbientSound.] ifFalse:[
			self root startAmbientSound].
			^ self. ].
	(anEvent keyValue = 3) ifTrue:[
		"Need some sort of visual indication here...."
		self isChatting 
			ifTrue:[self endChat]
			 ifFalse:[self startChat].
		^ self.
	].

	anEvent keyValue = 30 ifTrue:[ self translation: self translation + (0@1@0).].
	anEvent keyValue = 31 ifTrue:[ self translation: self translation - (0@1@0).].
	(pointer wantsKeyboard: anEvent)
		ifTrue:[pointer keyStroke: anEvent]
		ifFalse:[Croquet world morph consoleMorph keyStroke: anEvent].


	
keyUp: anEvent

	pointer keyUp: anEvent.

	
mouseDown: evt

	| root |
	self pointerXY: evt cursorPoint.
	remoteControl ifNotNil:[ 
		root _ remoteControl root.
		remoteControl removeChild: self.
		root addChild: self.
		self localTransform: remoteControl globalTransform.
		self downPointer setAutomatic: true.
		self setDoRender: true.
		remoteControl _ nil.
		avatar forwardOnY: 6.
		].
	mouseDownTime _ evt timeStamp.
	(evt controlKeyPressed not and:[evt yellowButtonPressed]) ifTrue:[
		yellowButtonPressed _ true.
		avatar startDriving: evt shiftPressed.
		].
	evt redButtonPressed ifTrue:[
		redButtonPressed _ true.
		(pointer pointerDown: evt) ifFalse:[self makePopup.].
	] ifFalse:[
		pointer event2D: evt.
	].
mouseMove: evt

	"evt is a MouseMoveEvent"

	self pointerXY: evt position.
	(evt anyButtonPressed and:[ evt yellowButtonPressed not]) ifTrue:[
		self pointer pointerMove: evt asMouseMove.
	] ifFalse:[
		self pointer event2D: evt asMouseMove.
	].

mouseUp: evt


	yellowButtonPressed ifTrue:[
		evt timeStamp - mouseDownTime < 250 ifTrue:[ 
			avatar snapTrans ifNotNil:[avatar snapBack.] ifNil:[
				pointer selectedObject ifNotNil:[
					pointer selectedObject isWindow ifTrue:[ pointer selectedObject gotoWindow:pointer.].].].
			] ifFalse:[ avatar snapTrans: nil.].
		yellowButtonPressed_ false.
		avatar stopDriving.
		].
	redButtonPressed ifTrue:[ 
		pointer pointerUp: evt. 
		redButtonPressed _ false.].
	self pointerXY: evt cursorPoint.

openHalo

	| |
	Croquet world morph addHalo.
openHalo: ptr

	| |
	Croquet world morph addHalo.
pointerXY: pxy
 
	| xy |

	xy _  pxy - viewPort topLeft.
	pointer pointerXY: xy.
	pointer calcTransform: self bounds z: zScreen.
	"avatar speedControl: (self bounds center - xy)/ (self bounds extent *0.5).
	avatar pointerTransform: pointer. CHANGED SAVING A MESSAGE:"
	avatar speedControl: (self bounds center - xy)/ (self bounds extent *0.5) pointerTransform: pointer.

	^ xy.
switchMirror


	rearCam ifNil:[
		rearCam _ TOverlayRearView new initialize: self viewPort.
		overlays add: rearCam.
		] ifNotNil:[ overlays remove: rearCam. rearCam _ nil.].

initialize
frustumChanged


	overlays do:[ :ol | ol frustumChanged: self].
	self updatePointerCam.
initializeWithViewPort: vp


	| downMatrix |
	super initializeWithViewPort: vp.
	pointer _ TPointer new.
	pointer minDistance: zNear.
	self addChild: pointer.
	pointer tool: (TLaser new initialize).
	downMatrix _ B3DMatrix4x4 identity rotationAroundX: -90.
	downPointer _ TRay new.
	downPointer localTransform: downMatrix.
	downPointer downRay: true.
"	self addChild: downPointer."
	yellowButtonPressed _ false.
	redButtonPressed _ false.
	renderInterval _ 50.0.  "render every 50 milliseconds by default"
	renderEstimate _ 30.0.  "estimate of rendering time"
	doRender _ true.
	lastRenderTime _ 0.0.
	remoteControl _ nil.
	overlays _ OrderedCollection new.
	self addOverlay: (buttonsOverlay _ TOverlayButtons new camera: self).
	self addOverlay: (morphicOverlay _ TMorphicOverlay new).
	self addOverlay: (debugOverlay _ TDebugOverlay new).
	thirdPerson _ 1.0.
	thirdPersonDelta _ 0.
	self updatePointerCam.
updatePointerCam


	pointer camera bounds: self bounds.
	pointer camera clipPlanes: self clipPlanes.
	pointer camera clipPlanesTransform: self clipPlanesTransform.
	pointer camera zFar: self zFar.
	pointer camera zNear: self zNear.
	pointer camera zScreen: self zScreen.
	pointer camera localTransform: self localTransform.
	pointer camera globalTransform: self globalTransform.
	pointer camera viewAngle: self viewAngle.


morph import
makeLinkToProject: projName

	"import an existing morphic project, creating a window for it in front of the camera"
	| win tmorph htp newWorld |
	tmorph _ TMorphic new initializeOpaque: true extent: 512@512.
	htp _ Croquet world homeTeaParty.
	newWorld _ (htp global: #TMorphMonitor) new initializeWithWorld: projName opaque: true extent: tmorph targetExtent.
	newWorld eventsTo: tmorph.
	
	win _ self makeWindowInFront.
	win contents: tmorph.
	^ win.
makeLocalProjectWithMorph: aMorph

	"put a morph into a new morphic project, creating a window for it in front of the camera"
	| win tmorph newWorld |
	tmorph _ TMorphic new initializeOpaque: true extent: 512@512.
	newWorld _ TMorphMonitor createLocalProjectWithExtent: tmorph extent initialMorph: aMorph.
	newWorld eventsTo: tmorph.
	
	win _ self makeWindowInFront.
	win contents: tmorph.
	^ win.
makePicture: aFile

	| win image txtr |
	[[aFile binary.
	image _ Form fromBinaryStream: (RWBinaryOrTextStream with: aFile contents) reset]
		ensure: [aFile close].
	"make a picture from a form, and put it in a window in front of the camera"
	txtr _ TTexture new.
	txtr initializeWithForm: image
				mipmap: true
				shrinkFit: false.]
		on: Error do: [ :ex |
			txtr _ TTexture new initializeWithForm: TForm defaultTForm mipmap: true shrinkFit: true.
			ex return. ].

	win _ self makeWindowInFront.
	win contents: txtr.
	^ win
makePortal: aPortal

	"make a portal in front of me, linking to another portal"
	| win |
	win _ self makeWindowInFront.
	win contents: (TPortal new linkPortal: aPortal).
	^ win.

painter
clickOpenAliceFile: filePath

	| frame trans teaUrl model |
	teaUrl := 'http://www.reed.com/TeaLand'.
	2 to: filePath size do:[:i| teaUrl := teaUrl,'/', (filePath at: i)].
	teaUrl := (teaUrl allButLast: 4), '.tea'.
	model := CroquetData loadAliceModel: filePath url: teaUrl.

	trans _ self translation - (self lookAt * 10).
	frame _ TSpinner new.
	frame matNil.
	frame translation: trans.
	frame contents: model.

	avatar root addChild: frame.
	frame translation: trans.
	avatar root addChild: frame.
makePoohMeshFrom: aForm

	| list pts subdivision mask |
	mask := Form extent: aForm extent*2 depth: 1.
	(WarpBlt toForm: mask)
		sourceForm: aForm destRect: mask boundingBox;
		combinationRule: Form over;
		cellSize: 1;
		colorMap: (Color maskingMap: aForm depth);
		warpBits.

	subdivision := PoohSubdivision withSize: (mask boundingBox).
	list := mask traceOutlines.
	list := list collect:[:poly|
		poly collect:[:loop|
			pts _ StrokeSimplifier new.
			loop do:[:pt| pts add: pt].
			pts closeStroke.
			pts := pts finalStroke.
			pts := StrokeSimplifier smoothen: pts length: 10.
			pts := LineIntersections regularize: pts.
			pts do:[:pt| subdivision insertPoint: pt].
			pts]].
	list := list collect:[:poly|
		poly collect:[:loop| subdivision constraintOutline: loop].
	].

	list do:[:poly|
		poly keysAndValuesDo:[:index :loop|
			subdivision markExteriorEdges: (index = 1) in: loop.
		].
	].
	^subdivision build3DObject: false.
makePoohObjectFrom: aForm player: aPlayer rotateBy: rot replaceOldCostume: aBoolean

	| aSpace pts bbForm tex mat subdivision b3dMesh mesh pos scale tfm |
	"Convert the form"
	bbForm := Form extent: aForm extent asSmallerPowerOfTwo depth: 32.
	aForm displayScaledOn: bbForm in: (bbForm boundingBox insetBy: 1).
	"Smear the borders of the texture a bit to prevent problems in texture mapping"
	bbForm smearFill: 10. "pixels - less is faster but more is safer"

	aSpace := avatar root.

	"Create the texture"
	tex := TTexture new initializeWithForm: bbForm mipmap: true shrinkFit: true extension: #colorKeyZero.

	"The material"
	mat _ TMaterial new.
	mat ambientColor: #(1 1 1 1) asFloatArray.
	mat diffuseColor: #(1 1 1 1) asFloatArray.

	mat texture: tex.
	mat textureMode: GLModulate.

	"The b3d mesh"
	true ifTrue:[
		b3dMesh := self makePoohMeshFrom: aForm.
		scale := 0.01.
	] ifFalse:[
		pts _ StrokeSimplifier new.
		aForm traceOutline: Color transparent do:[:aPoint| pts add: aPoint].
		pts closeStroke.
		pts := pts finalStroke.
		pts := StrokeSimplifier smoothen: pts length: 10.
		pts := LineIntersections regularize: pts.

		subdivision _ PoohSubdivision constraintOutline: pts.
		b3dMesh _ subdivision build3DObject: false. "single sided textures"
		scale := 0.02.
	].
	tfm := (B3DRotation axis: 1@0@0 angle: 180) asMatrix4x4.
	tfm := tfm composeWith: (B3DRotation axis: 0@1@0 angle: rot) asMatrix4x4.
	tfm := tfm composeWith: (B3DMatrix4x4 withScale: scale@scale@scale).
	tfm := tfm composeWith: (B3DMatrix4x4 withOffset: (aForm width * -0.5) @ (aForm height * -0.5) @ 0).
	b3dMesh transformBy: tfm.

	"The TMesh"
	mesh := TMesh new initializeWithVertices: b3dMesh vertices 
		alias: nil 
		norms: b3dMesh vertexNormals
		textureUV: b3dMesh texCoords
		faceGroups: {1. b3dMesh zeroBasedFaceGroup}
		material: mat.
	mesh solid: false.
	mesh initBounds.

	TDragBehavior attachTo: mesh.

	aPlayer ifNotNil:[
		aBoolean 
			ifTrue:[aPlayer frame replaceUserCostume: mesh sketch: aForm]
			ifFalse:[aPlayer frame addUserCostume: mesh sketch: aForm].
		^self].

	"Position it"
	pos := avatar translation - (avatar lookAt * 10).
	mesh translation: pos.
	mesh rotationAroundY: avatar yaw + rot.
	aSpace  addChild: mesh.

	"mesh startScript:[mesh turnTo: cWorld activeCamera]."
"self tweakWorld ifNotNil:[
	mesh replaceUserCostume: mesh sketch: aForm.
	aSpace player signal: #created with: mesh player.
]."
popUpBillboard: aForm player: aPlayer


	| aSpace bbForm size billboard aPosition txtr mat |

	aSpace :=  self root.

	size := aForm extent * 0.01.
	bbForm := Form extent: aForm extent asSmallerPowerOfTwo depth: 32.
	aForm displayScaledOn: bbForm in: (bbForm boundingBox insetBy: 1).

	billboard := TBillboard new.

	txtr := TTexture new initializeWithForm: aForm mipmap: true shrinkFit: true extension: #colorKeyZero.
	txtr aspect: size y / size x asFloat. 
	txtr extent: size.
	txtr objectOwner: billboard.

	mat _ TMaterial new.
	mat ambientColor: #(1 1 1 0.99) asFloatArray.
	mat diffuseColor: #(1 1 1 0.99) asFloatArray.
	mat emissiveColor: #(1 1 1 0.99) asFloatArray.

	txtr material: mat.
	txtr extent: txtr extent * 3.
	
	TDragBehavior attachTo: billboard.
	
	billboard addChild: txtr.

	aPlayer ifNotNil:[
		aPlayer frame: billboard.
		aPlayer sketch: aForm.
		aPlayer icon: nil.
		^self].

	billboard player sketch: aForm.
	aPosition := self translation - (self lookAt * 10).
	billboard translation: aPosition.
	aSpace addChild: billboard.





"


	| pos size aSpace bbForm |
	size := aForm extent * 0.01.
	bbForm := Form extent: aForm extent asSmallerPowerOfTwo depth: 32.
	aForm displayScaledOn: bbForm in: (bbForm boundingBox insetBy: 1).
	pos := cWorld activeCamera translation + (cWorld activeCamera lookAt * 10).
	aSpace := cWorld activeCamera root.
	aSpace popUpBillboard: bbForm extent: size at: pos.
	aSpace asyncSend: #popUpBillboard:extent:at: with: bbForm with: size with: pos.

"

popUp
makeBrowser

	| win teaWorld trans browser tmorph htp |

" This is just an example of how to make new objects remotely. You should not do it this way. There will be a more robust mechanism later on."

	tmorph _ TMorphic new initializeOpaque: true extent: 256@256.
	htp _ Croquet world homeTeaParty.	
	teaWorld _ (htp global: #TMorphMonitor) new initializeWithWorld: nil opaque: true extent: tmorph targetExtent.
	browser _ (Browser inTeaParty) openEditString: nil.
	teaWorld color: browser paneColor.
	teaWorld addMorphCentered: browser.
	browser expandBoxHit.
	teaWorld eventsTo: tmorph.

	win _ TWindow new.
	trans _ self translation - (self lookAt *6).
	win translation: trans.
	win rotationAroundY: self yaw.
	self root addChild: win.
	win contents: tmorph.
	win isBrowser: true.
	^ win.
makeFloor: sp fileName: txtrName

	| stone txt |

	txt _ TTexture
				new initializeWithFileName: txtrName
				mipmap: true
				shrinkFit: false.
	txt uvScale: 16.0@16.0.

	stone _ TCube new.
	stone extentX:80 y:0.5 z: 80.
	stone translationX: 0 y: -4.0 z: 0.0.
	stone texture: txt.
	stone objectName: 'floor'.
	sp addChild: stone.
	^ stone.

makePopup

	"put the popup in an overlay that tracks the camera"
	buttonsOverlay makePopup.

remoteControl
isRemoteControl


	^ remoteControl ~= nil.
setRemoteControl: bool


	remoteControl _ bool.

render
doRender

	| timeReally |
	timeReally _ Croquet getRealTimeAsMilliseconds.
	"if we are on time or more than 0.5 sec since last time, render"
	(timeReally <= (Croquet deadline + 10.0) or: [lastRenderTime + 500.0 <= timeReally]) ifTrue: [
		Croquet world renderer render.
		Croquet teaTime commit. "commit the changes we made"
		lastRenderTime _ Croquet getRealTimeAsMilliseconds.
		timeReally _ lastRenderTime - timeReally.
		renderEstimate _ (renderEstimate + timeReally) / 2.0. "update render time estimator"
		].
	
renderFrame: ogl space: space


" Only render if we are rendering in a portal - otherwise, we interfere with the user ."

	| rval |
	doRender ifTrue:[
		inPortal ifFalse:[
			avatar ifNotNil:[
				avatar visibleTree: false. 
				rval _  super renderFrame: ogl space: space.
				avatar visibleTree: true.
				^ rval.
			].
		] ifTrue:[	
			^ super renderFrame: ogl space: space.
		].
	].
	^ 0.
renderOverlay: ogl

	overlays ifNotNil:[
		overlays do:[:ov |
			Croquet renderProtect: ov in: [ :ovx |
				"reset the buffers"
				ogl glDepthMask: GLTrue;
					glDisable: GLBlend;
					glClear: (GLDepthBufferBit bitOr: GLStencilBufferBit).
				"and render each overlay"
				ovx renderOverlay: ogl]]].
renderScheduler

	"this method runs once per renderInterval, rescheduling itself.   It controls the rendering
	frequency, responding to load.  It measures the load by measuring when it runs.
	Actual rendering is done by #renderFromMorph messages sent by this scheduler."
	| tardiness |

	"experimental code deleted: calculate a new rendering interval"
	"nowReally _ Croquet world getRealTimeAsMilliseconds."
	"advance _ when - nowReally." "how early were we"
	"frames _ (advance / 20.0) floor."  "how many integer 50 Hz frames early(+) or late(-)"
	"Adjust by that many frame times"
	"renderInterval _ ((renderInterval - (frames * 20.0)) min: 500.0) max: 20.0."

	"force a target rate of 30 fps for now, unless we are running more than 5 secs. late."
	tardiness _ Croquet tardiness.
	tardiness < 5000.0 ifTrue: [ tardiness _ 0.0 ].
	renderInterval _ 33.0 + tardiness.
	self future: renderInterval deadlineRelative: renderInterval + 0.5 deferRelative: renderInterval perform: #renderScheduler.

	"schedule rendering after tea time commits for duration."
	self future: 0.0 deadlineRelative: 0.5 deferRelative: 0.0 perform: #doRender.
	Croquet teaTime commit.
snapshot

	
	self morphicOverlay dock addSnapshotItem: (TSnapshot new initializeWithFrame: avatar).
snapshot: ptr

	
	self morphicOverlay dock addSnapshotItem: (TSnapshot new initializeWithFrame: avatar).
startRendering

	"turn on stepping and rendering"
	self future: renderInterval deadlineRelative: renderInterval + 0.5 deferRelative: renderInterval perform: #renderScheduler.
testFloor: bnds
 

	inPortal ifFalse:[ ^ downPointer pick: bnds].

transform
followAvatar


	| at ct trans bt |

	thirdPerson ~= 0.0 ifTrue:[
		at_ avatar globalTransform.
		ct _ self localTransform.

		trans _ B3DMatrix4x4 identity. 
		trans translation: (B3DVector3 x:0.0 y:0.7 z: 4)*thirdPerson.
		bt _ at composeWith: trans. "This is the target position of the camera"
		trans _ ct translation + ((bt translation - ct translation)/4.0).
"at translation ~= ct translation ifTrue:[
		self lookAt:at translation up: (B3DVector3 x:0 y:1 z:0) * thirdPerson.].
self translation: trans."
		currentSpace _ avatar root.
		at _ at asQuaternion.
		ct _ ct asQuaternion.
		ct_ct slerpTo: at at: 0.25.
		self localTransform: (ct asMatrix4x4 translation:trans).
	]ifFalse:[
		currentSpace _ avatar root.
		self localTransform: avatar globalTransform.
	].

	thirdPersonDelta ~=0 ifTrue:[
			thirdPerson _ thirdPerson + thirdPersonDelta.
			thirdPerson <= 0.0 ifTrue:[thirdPersonDelta _ 0. thirdPerson _ 0.0.].
			thirdPerson >=1.0 ifTrue:[ thirdPersonDelta _ 0. thirdPerson _ 1.0].
			].
postRender


	avatar future: 50.0 perform: #downFloor:distance:position: withArguments: {downPointer selectedObject. downPointer selectedDistance. downPointer selectedFramePosition}.
preRender


"This method gets called just before the rendering"
	avatar newSpace ifTrue:[self localTransform: avatar localTransform].
	pointer resetSelected.
	downPointer globalPosition: avatar globalPosition.
	downPointer resetSelected.
	^ self followAvatar.
snapBack


	avatar goto: avatar snapTrans count: 6.
	avatar snapTrans: nil.

window
makeWindowInFront

	"make a window in front of me"
	| win |
	win _ TWindow new.
	win translation: (self translation - (self lookAt *6) - (0@0.5@0)).
	win rotationAroundY: self yaw.
	self root addChild: win.
	^win.

class methods:

^top


- made by Dandelion -