本例选择了《天空之城》的25张照片,组成5x5的照片墙)。首先我们在setupContentEntity
方法中构建了一个纹理数组,将这25张照片添加到数组images
中。其中封装了setup
方法,借助于visionOS对沉浸式空间的支持,我们创建了三个平面,组成具有立体感的照片墙。
在setup
方法中调用了addChildEntities
,对images
随机打散,通过quotientAndRemainder
方法对5求商取余来设置x
和y
的值,从而生成5x5的照片,z
轴上仅以平面为基准做了小小的调整。将准备好的位置和纹理,传入makePlane
方法进行配置返回实体再分别添加到3个平面中。
为增加趣味性,这里还定义了toggleSorted()
方法,在沉浸式空间内点击时会打散(randomSetChildPositions()
方法),再次点击又会重置收起(resetChildPositions()
)。完整的ViewModel.swift
文件内容如下:
importSwiftUIimportRealityKit@ObservableclassViewModel{privateletplaneSize=CGSize(width:0.32,height:0.18)privateletmaxPlaneSize=CGSize(width:3.0,height:2.0)privatevarcontentEntity=Entity()privatevarboardPlanes:[ModelEntity]=[]privatevarimages:[MaterialParameters.Texture]=[]privatevarsorted=truefuncsetupContentEntity()->Entity{foriin1..<26{letname="laputa\(String(format:"%03d",i))"iflettexture=try?TextureResource.load(named:name){images.append(MaterialParameters.Texture(texture))}}setup()returncontentEntity}functoggleSorted(){ifsorted{sorted.toggle()randomSetChildPositions()}else{sorted.toggle()resetChildPositions()}}// MARK: - Privateprivatefuncsetup(){foriin0..<3{letboardPlane=ModelEntity(mesh:.generatePlane(width:3,height:2),materials:[SimpleMaterial(color:.clear,isMetallic:false)])boardPlane.position=SIMD3<Float>(x:0,y:2,z:-0.5-0.1*Float(i+1))contentEntity.addChild(boardPlane)boardPlanes.append(boardPlane)addChildEntities(boardPlane:boardPlane)}}privatefuncaddChildEntities(boardPlane:ModelEntity){vari:Int=0forimageinimages.shuffled().prefix(30){letdivisionResult=i.quotientAndRemainder(dividingBy:5)letx:Float=Float(divisionResult.remainder)*0.4-0.75lety:Float=Float(divisionResult.quotient)*0.25-0.5letz:Float=boardPlane.position.z+Float(i)*0.0001letentity=makePlane(name:"", position: SIMD3<Float>(x: x, y: y, z: z), texture: image)boardPlane.addChild(entity)i+=1}}privatefuncmakePlane(name:String,position:SIMD3<Float>,texture:MaterialParameters.Texture)->ModelEntity{varmaterial=SimpleMaterial()material.color=.init(texture:texture)letentity=ModelEntity(mesh:.generatePlane(width:0.32,height:0.18,cornerRadius:0.0),materials:[material],collisionShape:.generateBox(width:0.32,height:0.18,depth:0.1),mass:0.0)entity.name=nameentity.position=positionentity.components.set(InputTargetComponent(allowedInputTypes:.indirect))returnentity}privatefuncmove(entity:Entity,position:SIMD2<Float>){letmove=FromToByAnimation<Transform>(name:"move",from:.init(scale:.init(repeating:1),translation:entity.position),to:.init(scale:.init(repeating:1),translation:.init(x:position.x,y:position.y,z:entity.position.z)),duration:2.0,timing:.linear,bindTarget:.transform)letanimation=try!AnimationResource.generate(with:move)entity.playAnimation(animation,transitionDuration:2.0)}privatefuncrandomSetChildPositions(){letsize=CGSize(width:planeSize.width*1.2,height:planeSize.height*1.2)forboardPlaneinboardPlanes{letnewPoints=randomPoints(count:boardPlane.children.count,size:size)foriin0..<boardPlane.children.count{letentity=boardPlane.children[i]move(entity:entity,position:newPoints[i])}}}privatefuncresetChildPositions(){forboardPlaneinboardPlanes{vari:Int=0forentityinboardPlane.children{letdivisionResult=i.quotientAndRemainder(dividingBy:5)letx:Float=Float(divisionResult.remainder)*0.4-0.75lety:Float=Float(divisionResult.quotient)*0.25-0.5move(entity:entity,position:SIMD2<Float>(x,y))i+=1}}}privatefuncrandomPoints(count:Int,size:CGSize)->[SIMD2<Float>]{varret:[SIMD2<Float>]=[]whileret.count<count{ifletpoint=randomPoint(size:size,positions:ret){ret.append(point)}}returnret}privatefuncrandomPoint(size:CGSize,positions:[SIMD2<Float>])->SIMD2<Float>?{for_in0..<5000{letx=CGFloat.random(in:-maxPlaneSize.width...(maxPlaneSize.width/2))lety=CGFloat.random(in:-maxPlaneSize.height...(maxPlaneSize.height/2))letframe=CGRect(x:CGFloat(x),y:CGFloat(y),width:size.width,height:size.height)ifpositions.isEmpty{returnSIMD2<Float>(Float(x),Float(y))}else{varintersects=falseforpositioninpositions{letf=CGRect(x:CGFloat(position.x),y:CGFloat(position.y),width:size.width,height:size.height)iff.intersects(frame){intersects=true}}if!intersects{returnSIMD2<Float>(Float(frame.minX),Float(frame.minY))}}}returnnil}}
在ImmersiveView
中发生了Tap事件后会调用其中的toggleSorted()
方法,其它代码与此前的示例并没什么不同。
structImmersiveView:View{@Statevarmodel=ViewModel()varbody:someView{RealityView{contentincontent.add(model.setupContentEntity())}.onTapGesture{model.toggleSorted()}}}
示例代码:GitHub仓库
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记